@joshuaswarren/openclaw-engram 9.2.5 → 9.2.7

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/index.js CHANGED
@@ -6948,7 +6948,7 @@ var GraphDashboardServer = class {
6948
6948
 
6949
6949
  // src/access-http.ts
6950
6950
  import { createServer as createServer3 } from "http";
6951
- import { randomUUID, timingSafeEqual as timingSafeEqual2 } from "crypto";
6951
+ import { randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
6952
6952
  import { AsyncLocalStorage } from "async_hooks";
6953
6953
  import { existsSync } from "fs";
6954
6954
  import { readFile as readFile14 } from "fs/promises";
@@ -6957,6 +6957,7 @@ import { fileURLToPath, URL as URL3 } from "url";
6957
6957
 
6958
6958
  // src/access-mcp.ts
6959
6959
  import { readFile as readFile13 } from "fs/promises";
6960
+ import { randomUUID } from "crypto";
6960
6961
  var MCP_PROTOCOL_VERSION = "2024-11-05";
6961
6962
  async function getMcpServerVersion() {
6962
6963
  const envVersion = readEnvVar("OPENCLAW_ENGRAM_VERSION")?.trim() || readEnvVar("npm_package_version")?.trim();
@@ -7611,6 +7612,33 @@ var EngramMcpServer = class {
7611
7612
  flushTask = null;
7612
7613
  tools;
7613
7614
  authenticatedPrincipal;
7615
+ /**
7616
+ * MCP client info keyed by server-assigned session ID. On each `initialize`
7617
+ * handshake the server generates a UUID, stores the client's clientInfo
7618
+ * against it, and returns the ID as `Mcp-Session-Id` in the response
7619
+ * metadata. Subsequent requests from the same client include this header,
7620
+ * allowing per-session clientInfo lookup without cross-session leaks.
7621
+ */
7622
+ clientInfoBySession = /* @__PURE__ */ new Map();
7623
+ /**
7624
+ * Session IDs generated during initialize, keyed by caller-supplied correlation
7625
+ * ID (unique per HTTP request) to avoid collisions when multiple clients send
7626
+ * initialize with the same JSON-RPC id concurrently.
7627
+ */
7628
+ initSessionIds = /* @__PURE__ */ new Map();
7629
+ /** Get clientInfo for a specific MCP session. Returns undefined for non-MCP requests. */
7630
+ getClientInfo(sessionId) {
7631
+ if (sessionId) {
7632
+ return this.clientInfoBySession.get(sessionId);
7633
+ }
7634
+ return void 0;
7635
+ }
7636
+ /** Pop the session ID generated during an initialize handshake, keyed by correlation ID. */
7637
+ popInitSessionId(correlationId) {
7638
+ const sid = this.initSessionIds.get(correlationId);
7639
+ if (sid !== void 0) this.initSessionIds.delete(correlationId);
7640
+ return sid;
7641
+ }
7614
7642
  async handleRequest(request, options) {
7615
7643
  const id = request.id ?? null;
7616
7644
  const method = request.method ?? "";
@@ -7619,7 +7647,20 @@ var EngramMcpServer = class {
7619
7647
  return { jsonrpc: "2.0", id, result: {} };
7620
7648
  }
7621
7649
  if (method === "initialize") {
7650
+ const params = request.params ?? {};
7651
+ const rawClientInfo = params.clientInfo;
7652
+ const newSessionId = randomUUID();
7653
+ if (rawClientInfo && typeof rawClientInfo.name === "string") {
7654
+ const info = { name: rawClientInfo.name, version: rawClientInfo.version };
7655
+ this.clientInfoBySession.set(newSessionId, info);
7656
+ if (this.clientInfoBySession.size > 1e3) {
7657
+ const firstKey = this.clientInfoBySession.keys().next().value;
7658
+ if (firstKey) this.clientInfoBySession.delete(firstKey);
7659
+ }
7660
+ }
7622
7661
  const version = await getMcpServerVersion();
7662
+ const corrId = options?.correlationId;
7663
+ if (corrId) this.initSessionIds.set(corrId, newSessionId);
7623
7664
  return {
7624
7665
  jsonrpc: "2.0",
7625
7666
  id,
@@ -8240,6 +8281,137 @@ function validateRequest(schemaName, body) {
8240
8281
  return { success: false, error: formatZodError(result.error) };
8241
8282
  }
8242
8283
 
8284
+ // src/adapters/types.ts
8285
+ function headerValue(headers, key) {
8286
+ const raw = headers[key];
8287
+ const value = Array.isArray(raw) ? raw[0] : raw;
8288
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
8289
+ }
8290
+
8291
+ // src/adapters/claude-code.ts
8292
+ var ClaudeCodeAdapter = class {
8293
+ id = "claude-code";
8294
+ matches(context) {
8295
+ if (context.clientInfo?.name === "claude-code") return true;
8296
+ const ua = headerValue(context.headers, "user-agent");
8297
+ if (ua && ua.toLowerCase().startsWith("claude-code/")) return true;
8298
+ const clientId = headerValue(context.headers, "x-engram-client-id");
8299
+ if (clientId?.toLowerCase() === "claude-code") return true;
8300
+ return false;
8301
+ }
8302
+ resolveIdentity(context) {
8303
+ const mcpSessionId = headerValue(context.headers, "mcp-session-id");
8304
+ const principal = headerValue(context.headers, "x-engram-principal") || "claude-code";
8305
+ const namespace = headerValue(context.headers, "x-engram-namespace") || "claude-code";
8306
+ return {
8307
+ namespace,
8308
+ principal,
8309
+ sessionKey: mcpSessionId ?? context.sessionKey,
8310
+ adapterId: this.id
8311
+ };
8312
+ }
8313
+ };
8314
+
8315
+ // src/adapters/codex.ts
8316
+ var CodexAdapter = class {
8317
+ id = "codex";
8318
+ matches(context) {
8319
+ if (context.clientInfo?.name === "codex-mcp-client") return true;
8320
+ const clientName = context.clientInfo?.name?.toLowerCase() ?? "";
8321
+ if (clientName.includes("codex") && clientName !== "codex-mcp-client") return true;
8322
+ const clientId = headerValue(context.headers, "x-engram-client-id");
8323
+ if (clientId?.toLowerCase() === "codex") return true;
8324
+ return false;
8325
+ }
8326
+ resolveIdentity(context) {
8327
+ const mcpSessionId = headerValue(context.headers, "mcp-session-id");
8328
+ const principal = headerValue(context.headers, "x-engram-principal") || "codex";
8329
+ const namespace = headerValue(context.headers, "x-engram-namespace") || "codex";
8330
+ return {
8331
+ namespace,
8332
+ principal,
8333
+ sessionKey: mcpSessionId ?? context.sessionKey,
8334
+ adapterId: this.id
8335
+ };
8336
+ }
8337
+ };
8338
+
8339
+ // src/adapters/replit.ts
8340
+ var ReplitAdapter = class {
8341
+ id = "replit";
8342
+ matches(context) {
8343
+ const clientId = headerValue(context.headers, "x-engram-client-id");
8344
+ if (clientId?.toLowerCase() === "replit") return true;
8345
+ const clientName = context.clientInfo?.name?.toLowerCase() ?? "";
8346
+ if (clientName.includes("replit")) return true;
8347
+ return false;
8348
+ }
8349
+ resolveIdentity(context) {
8350
+ const mcpSessionId = headerValue(context.headers, "mcp-session-id");
8351
+ const principal = headerValue(context.headers, "x-engram-principal") || "replit-agent";
8352
+ const namespace = headerValue(context.headers, "x-engram-namespace") || "replit";
8353
+ return {
8354
+ namespace,
8355
+ principal,
8356
+ sessionKey: mcpSessionId ?? context.sessionKey,
8357
+ adapterId: this.id
8358
+ };
8359
+ }
8360
+ };
8361
+
8362
+ // src/adapters/hermes.ts
8363
+ var HermesAdapter = class {
8364
+ id = "hermes";
8365
+ matches(context) {
8366
+ if (headerValue(context.headers, "x-hermes-session-id")) return true;
8367
+ const clientId = headerValue(context.headers, "x-engram-client-id");
8368
+ if (clientId?.toLowerCase() === "hermes") return true;
8369
+ const clientName = context.clientInfo?.name?.toLowerCase() ?? "";
8370
+ if (clientName.includes("hermes")) return true;
8371
+ return false;
8372
+ }
8373
+ resolveIdentity(context) {
8374
+ const sessionId = headerValue(context.headers, "x-hermes-session-id");
8375
+ const principal = headerValue(context.headers, "x-engram-principal") || "hermes-agent";
8376
+ const namespace = headerValue(context.headers, "x-engram-namespace") || "hermes";
8377
+ return {
8378
+ namespace,
8379
+ principal,
8380
+ sessionKey: sessionId ?? context.sessionKey,
8381
+ adapterId: this.id
8382
+ };
8383
+ }
8384
+ };
8385
+
8386
+ // src/adapters/registry.ts
8387
+ var AdapterRegistry = class {
8388
+ adapters;
8389
+ constructor(adapters) {
8390
+ this.adapters = adapters ?? [
8391
+ new HermesAdapter(),
8392
+ new ReplitAdapter(),
8393
+ new CodexAdapter(),
8394
+ new ClaudeCodeAdapter()
8395
+ ];
8396
+ }
8397
+ /**
8398
+ * Try each adapter in order. Return the first match, or null if
8399
+ * no adapter recognizes the request context.
8400
+ */
8401
+ resolve(context) {
8402
+ for (const adapter of this.adapters) {
8403
+ if (adapter.matches(context)) {
8404
+ return adapter.resolveIdentity(context);
8405
+ }
8406
+ }
8407
+ return null;
8408
+ }
8409
+ /** List registered adapter IDs */
8410
+ list() {
8411
+ return this.adapters.map((a) => a.id);
8412
+ }
8413
+ };
8414
+
8243
8415
  // src/access-http.ts
8244
8416
  function resolveDefaultAdminConsolePublicDir() {
8245
8417
  const candidates = [
@@ -8302,6 +8474,7 @@ var EngramAccessHttpServer = class {
8302
8474
  adminConsoleEnabled;
8303
8475
  adminConsolePublicDir;
8304
8476
  trustPrincipalHeader;
8477
+ adapterRegistry;
8305
8478
  writeRequestTimestamps = [];
8306
8479
  mcpServer;
8307
8480
  server = null;
@@ -8316,6 +8489,7 @@ var EngramAccessHttpServer = class {
8316
8489
  this.adminConsoleEnabled = options.adminConsoleEnabled !== false;
8317
8490
  this.adminConsolePublicDir = options.adminConsolePublicDir ?? defaultAdminConsolePublicDir;
8318
8491
  this.trustPrincipalHeader = options.trustPrincipalHeader === true;
8492
+ this.adapterRegistry = options.enableAdapters !== false ? options.adapterRegistry ?? new AdapterRegistry() : null;
8319
8493
  this.mcpServer = new EngramMcpServer(this.service, { principal: options.principal });
8320
8494
  }
8321
8495
  async start() {
@@ -8324,7 +8498,7 @@ var EngramAccessHttpServer = class {
8324
8498
  }
8325
8499
  if (this.server) return this.status();
8326
8500
  const server = createServer3((req, res) => {
8327
- const correlationId = randomUUID();
8501
+ const correlationId = randomUUID2();
8328
8502
  correlationIdStore.run(correlationId, () => {
8329
8503
  void this.handle(req, res, correlationId).catch((err) => {
8330
8504
  log.debug(`engram access HTTP request failed [${correlationId}]: ${err}`);
@@ -8386,18 +8560,62 @@ var EngramAccessHttpServer = class {
8386
8560
  maxBodyBytes: this.maxBodyBytes
8387
8561
  };
8388
8562
  }
8389
- resolveRequestPrincipal(req) {
8563
+ /**
8564
+ * Resolve the adapter identity for the incoming request.
8565
+ * Includes MCP clientInfo from the last initialize handshake if available.
8566
+ * Returns null if no adapter matches or adapters are disabled.
8567
+ */
8568
+ resolveAdapterIdentity(req) {
8569
+ if (!this.adapterRegistry) return null;
8570
+ const sessionId = (() => {
8571
+ const raw = req.headers["mcp-session-id"];
8572
+ return typeof raw === "string" ? raw.trim() : void 0;
8573
+ })();
8574
+ return this.adapterRegistry.resolve({
8575
+ headers: req.headers,
8576
+ clientInfo: this.mcpServer.getClientInfo(sessionId)
8577
+ });
8578
+ }
8579
+ /** Cache for per-request identity resolution (avoids double adapter resolution) */
8580
+ identityCache = /* @__PURE__ */ new WeakMap();
8581
+ /** Resolve principal and namespace from request headers and adapter identity */
8582
+ resolveRequestIdentity(req) {
8583
+ const cached = this.identityCache.get(req);
8584
+ if (cached) return cached;
8585
+ let principal;
8586
+ let namespace;
8390
8587
  if (this.trustPrincipalHeader) {
8391
- const headerValue = req.headers["x-engram-principal"];
8392
- const raw = Array.isArray(headerValue) ? headerValue[0] : headerValue;
8588
+ const headerVal = req.headers["x-engram-principal"];
8589
+ const raw = Array.isArray(headerVal) ? headerVal[0] : headerVal;
8393
8590
  if (typeof raw === "string") {
8394
8591
  const trimmed = raw.trim();
8395
8592
  if (trimmed.length > 0) {
8396
- return trimmed;
8593
+ principal = trimmed;
8397
8594
  }
8398
8595
  }
8399
8596
  }
8400
- return this.authenticatedPrincipal;
8597
+ const adapterIdentity = this.resolveAdapterIdentity(req);
8598
+ if (adapterIdentity) {
8599
+ if (!principal) {
8600
+ principal = adapterIdentity.principal;
8601
+ }
8602
+ namespace = adapterIdentity.namespace;
8603
+ }
8604
+ if (!principal) {
8605
+ principal = this.authenticatedPrincipal;
8606
+ }
8607
+ const result = { principal, namespace };
8608
+ this.identityCache.set(req, result);
8609
+ return result;
8610
+ }
8611
+ resolveRequestPrincipal(req) {
8612
+ return this.resolveRequestIdentity(req).principal;
8613
+ }
8614
+ /** Resolve namespace: only use the explicit body value. Adapter-inferred namespace
8615
+ * is intentionally NOT used as a fallback for REST requests — omitting namespace
8616
+ * should default to the server's global namespace, not silently scope to an adapter. */
8617
+ resolveNamespace(_req, bodyNamespace) {
8618
+ return bodyNamespace || void 0;
8401
8619
  }
8402
8620
  async handle(req, res, correlationId) {
8403
8621
  const parsed = new URL3(req.url ?? "/", `http://${hostToUrlAuthority2(this.host)}`);
@@ -8423,12 +8641,21 @@ var EngramAccessHttpServer = class {
8423
8641
  this.respondJson(res, 200, await this.service.health());
8424
8642
  return;
8425
8643
  }
8644
+ if (req.method === "GET" && pathname === "/engram/v1/adapters") {
8645
+ const identity = this.resolveAdapterIdentity(req);
8646
+ this.respondJson(res, 200, {
8647
+ adaptersEnabled: this.adapterRegistry !== null,
8648
+ registered: this.adapterRegistry?.list() ?? [],
8649
+ resolved: identity
8650
+ });
8651
+ return;
8652
+ }
8426
8653
  if (req.method === "POST" && pathname === "/engram/v1/recall") {
8427
8654
  const body = await this.readValidatedBody(req, "recall");
8428
8655
  const response = await this.service.recall({
8429
8656
  query: body.query ?? "",
8430
8657
  sessionKey: body.sessionKey,
8431
- namespace: body.namespace,
8658
+ namespace: this.resolveNamespace(req, body.namespace),
8432
8659
  topK: body.topK,
8433
8660
  mode: body.mode,
8434
8661
  includeDebug: body.includeDebug === true
@@ -8440,7 +8667,7 @@ var EngramAccessHttpServer = class {
8440
8667
  const body = await this.readValidatedBody(req, "recallExplain");
8441
8668
  const response = await this.service.recallExplain({
8442
8669
  sessionKey: body.sessionKey,
8443
- namespace: body.namespace
8670
+ namespace: this.resolveNamespace(req, body.namespace)
8444
8671
  });
8445
8672
  this.respondJson(res, 200, response);
8446
8673
  return;
@@ -8451,7 +8678,7 @@ var EngramAccessHttpServer = class {
8451
8678
  const response = await this.service.observe({
8452
8679
  sessionKey: body.sessionKey,
8453
8680
  messages: body.messages,
8454
- namespace: body.namespace,
8681
+ namespace: this.resolveNamespace(req, body.namespace),
8455
8682
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
8456
8683
  skipExtraction: body.skipExtraction === true
8457
8684
  });
@@ -8464,7 +8691,7 @@ var EngramAccessHttpServer = class {
8464
8691
  const response = await this.service.lcmSearch({
8465
8692
  query: body.query,
8466
8693
  sessionKey: body.sessionKey,
8467
- namespace: body.namespace,
8694
+ namespace: this.resolveNamespace(req, body.namespace),
8468
8695
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
8469
8696
  limit: body.limit
8470
8697
  });
@@ -8486,7 +8713,7 @@ var EngramAccessHttpServer = class {
8486
8713
  content: body.content,
8487
8714
  category: body.category,
8488
8715
  confidence: body.confidence,
8489
- namespace: body.namespace,
8716
+ namespace: this.resolveNamespace(req, body.namespace),
8490
8717
  tags: body.tags,
8491
8718
  entityRef: body.entityRef,
8492
8719
  ttl: body.ttl,
@@ -8514,7 +8741,7 @@ var EngramAccessHttpServer = class {
8514
8741
  content: body.content,
8515
8742
  category: body.category,
8516
8743
  confidence: body.confidence,
8517
- namespace: body.namespace,
8744
+ namespace: this.resolveNamespace(req, body.namespace),
8518
8745
  tags: body.tags,
8519
8746
  entityRef: body.entityRef,
8520
8747
  ttl: body.ttl,
@@ -8633,7 +8860,7 @@ var EngramAccessHttpServer = class {
8633
8860
  memoryId: body.memoryId,
8634
8861
  status: body.status,
8635
8862
  reasonCode: body.reasonCode,
8636
- namespace: body.namespace,
8863
+ namespace: this.resolveNamespace(req, body.namespace),
8637
8864
  authenticatedPrincipal: this.resolveRequestPrincipal(req)
8638
8865
  });
8639
8866
  if (this.shouldCountWriteRateLimit(response)) {
@@ -8655,7 +8882,7 @@ var EngramAccessHttpServer = class {
8655
8882
  recordedAt: body.recordedAt,
8656
8883
  summary: body.summary,
8657
8884
  dryRun,
8658
- namespace: body.namespace,
8885
+ namespace: this.resolveNamespace(req, body.namespace),
8659
8886
  authenticatedPrincipal: this.resolveRequestPrincipal(req)
8660
8887
  });
8661
8888
  if (this.shouldCountWriteRateLimit(response)) {
@@ -8674,7 +8901,7 @@ var EngramAccessHttpServer = class {
8674
8901
  scenario: body.scenario,
8675
8902
  recordedAt: body.recordedAt,
8676
8903
  dryRun,
8677
- namespace: body.namespace,
8904
+ namespace: this.resolveNamespace(req, body.namespace),
8678
8905
  authenticatedPrincipal: this.resolveRequestPrincipal(req)
8679
8906
  });
8680
8907
  if (this.shouldCountWriteRateLimit(response)) {
@@ -8692,8 +8919,15 @@ var EngramAccessHttpServer = class {
8692
8919
  if (isMcpWrite) {
8693
8920
  this.ensureWriteRateLimitAvailable();
8694
8921
  }
8922
+ const sessionId = (() => {
8923
+ const raw = req.headers["mcp-session-id"];
8924
+ return typeof raw === "string" ? raw.trim() : void 0;
8925
+ })();
8926
+ const mcpCorrelationId = correlationIdStore.getStore() ?? randomUUID2();
8695
8927
  const response = await this.mcpServer.handleRequest(request, {
8696
- principalOverride: this.resolveRequestPrincipal(req)
8928
+ principalOverride: this.resolveRequestPrincipal(req),
8929
+ sessionId,
8930
+ correlationId: mcpCorrelationId
8697
8931
  });
8698
8932
  if (isMcpWrite && response !== null) {
8699
8933
  const result = response.result;
@@ -8708,6 +8942,10 @@ var EngramAccessHttpServer = class {
8708
8942
  res.end();
8709
8943
  return;
8710
8944
  }
8945
+ const assignedSessionId = this.mcpServer.popInitSessionId(mcpCorrelationId);
8946
+ if (assignedSessionId) {
8947
+ res.setHeader("mcp-session-id", assignedSessionId);
8948
+ }
8711
8949
  this.respondJson(res, 200, response);
8712
8950
  }
8713
8951
  respondJson(res, status, payload) {