@ixo/oracle-runtime-workers 0.23.0 → 0.24.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixo/oracle-runtime-workers",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
5
5
  "private": false,
6
6
  "publishConfig": {
package/src/core/env.ts CHANGED
@@ -129,7 +129,10 @@ export const baseEnvSchema = z.object({
129
129
  ORACLE_DID: z.string().min(1),
130
130
  /**
131
131
  * On-chain entity DID surfaced to the agent as `identity.entityDid`. Falls
132
- * back to `ORACLE_DID` when unset (see `createRuntimeCore`).
132
+ * back to `ORACLE_DID` when unset (see `createRuntimeCore`) for rooms and
133
+ * feedback. The memory plugin redeclares it as required
134
+ * (`did:ixo:entity:<32 hex>`): the Memory Engine keys every space by it and
135
+ * never receives the fallback.
133
136
  */
134
137
  ORACLE_ENTITY_DID: z.string().min(1).optional(),
135
138
  NETWORK: z.enum(['mainnet', 'testnet', 'devnet']).default('mainnet'),
@@ -381,6 +381,10 @@ import {
381
381
  fetchMemorySchemaDump,
382
382
  getMemorySchemaDump,
383
383
  } from '../plugins/memory/memory-tools';
384
+ import {
385
+ MEMORY_ENTITY_HEADER,
386
+ memoryEntityDid,
387
+ } from '../plugins/memory/memory-entity';
384
388
  import {
385
389
  contentToText,
386
390
  isSummarizationMessage,
@@ -4341,6 +4345,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4341
4345
  const v = this.core.validatedEnv['MEMORY_ENGINE_URL'];
4342
4346
  return typeof v === 'string' && v.length > 0 ? v : undefined;
4343
4347
  })(),
4348
+ // The configured entity only: `identity.entityDid` falls back to
4349
+ // `ORACLE_DID`, which the engine refuses as a space key.
4350
+ entityDid: memoryEntityDid(this.core.validatedEnv['ORACLE_ENTITY_DID']),
4344
4351
  getSession: async (id) => {
4345
4352
  const snapshot = this.historySnapshots.get(id);
4346
4353
  const row =
@@ -4429,7 +4436,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4429
4436
  this.userContextFetcher ??= new UserContextFetcher(
4430
4437
  {
4431
4438
  memoryEngineUrl: settings.engineUrl,
4432
- oracleDid: this.env.ORACLE_DID,
4439
+ // The configured entity only: `identity.entityDid` falls back to
4440
+ // `ORACLE_DID`, which the engine refuses as a space key.
4441
+ entityDid: memoryEntityDid(
4442
+ this.core.validatedEnv['ORACLE_ENTITY_DID'],
4443
+ ),
4433
4444
  // Looked up per call, not captured: a working-copy reset replaces
4434
4445
  // the ambient services while a gather may still be running.
4435
4446
  ucan: {
@@ -5383,6 +5394,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5383
5394
  if (!ambient || typeof url !== 'string' || url.length === 0) {
5384
5395
  return { error: 'MEMORY_MCP_URL is not configured' };
5385
5396
  }
5397
+ const entityDid = memoryEntityDid(
5398
+ this.core.validatedEnv['ORACLE_ENTITY_DID'],
5399
+ );
5400
+ if (!entityDid) {
5401
+ return {
5402
+ error:
5403
+ 'ORACLE_ENTITY_DID is not a domain entity DID (did:ixo:entity:<32 hex>)',
5404
+ };
5405
+ }
5386
5406
  const memoryDid = await ambient.ucan.resolveServiceDid(url);
5387
5407
  if (!memoryDid)
5388
5408
  return { error: 'memory engine DID could not be resolved' };
@@ -5400,6 +5420,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5400
5420
  Authorization: `Bearer ${invocation}`,
5401
5421
  'X-Auth-Type': 'ucan',
5402
5422
  'User-Agent': 'LangChain-MCP-Client/1.0',
5423
+ [MEMORY_ENTITY_HEADER]: entityDid,
5403
5424
  ...(room?.roomId ? { 'x-room-id': room.roomId } : {}),
5404
5425
  };
5405
5426
  try {
@@ -16,7 +16,10 @@
16
16
  * - Auth is the oracle's two-hop UCAN invocation minted from the user's
17
17
  * delegation (`ixo:memory` / `memory/*`) — no delegation, no indexing.
18
18
  * - Endpoint: `POST <MEMORY_ENGINE_URL>/messages` with `x-room-id`, as
19
- * `@ixo/common`'s `MemoryEngineService.processConversationHistory`.
19
+ * `@ixo/common`'s `MemoryEngineService.processConversationHistory`, plus
20
+ * `x-oracle-entity-did`: the engine keys the user's space by the oracle's
21
+ * domain entity (`ORACLE_ENTITY_DID`). Without one nothing is indexed
22
+ * (one warning per indexer) and the watermark stays put.
20
23
  * - A per-session lock prevents overlapping runs; a failed upload is
21
24
  * retried 3× with a 10 s pause (Node's numbers).
22
25
  *
@@ -24,6 +27,7 @@
24
27
  * preferred name is set; here the fallback is the literal "Me" straight away.
25
28
  */
26
29
  import type { Logger } from '../plugin-api/types';
30
+ import { MEMORY_ENTITY_HEADER } from '../plugins/memory/memory-entity';
27
31
 
28
32
  export interface HistoryMessage {
29
33
  /** LangChain message type: `human` | `ai` | `tool` | `system`. */
@@ -53,6 +57,12 @@ export interface SpeakerLabels {
53
57
  export interface SessionHistoryIndexerDeps {
54
58
  /** `MEMORY_ENGINE_URL`; undefined disables indexing (memory plugin absent). */
55
59
  memoryEngineUrl: string | undefined;
60
+ /**
61
+ * The configured `ORACLE_ENTITY_DID` (a well-formed domain entity DID),
62
+ * sent as `x-oracle-entity-did`. Never the `ORACLE_DID` fallback:
63
+ * undefined → every session is `skipped`.
64
+ */
65
+ entityDid: string | undefined;
56
66
  getSession(sessionId: string): Promise<IndexableSession | undefined>;
57
67
  /** The session transcript, summarisation bookkeeping already removed. */
58
68
  listMessages(sessionId: string): Promise<HistoryMessage[]>;
@@ -153,6 +163,9 @@ export class SessionHistoryIndexer {
153
163
 
154
164
  private readonly requestTimeoutMs: number;
155
165
 
166
+ /** The missing-entity warning has been logged (it is logged once). */
167
+ private warnedNoEntity = false;
168
+
156
169
  constructor(private readonly deps: SessionHistoryIndexerDeps) {
157
170
  this.fetchImpl = deps.fetchImpl ?? fetch.bind(globalThis);
158
171
  this.sleep =
@@ -187,6 +200,16 @@ export class SessionHistoryIndexer {
187
200
  );
188
201
  return 'skipped';
189
202
  }
203
+ const entityDid = this.deps.entityDid;
204
+ if (!entityDid) {
205
+ if (!this.warnedNoEntity) {
206
+ this.warnedNoEntity = true;
207
+ logger.warn(
208
+ `[history-indexer] ORACLE_ENTITY_DID is not a domain entity DID (did:ixo:entity:<32 hex>); the memory engine keys every space by it, so session history is not indexed`,
209
+ );
210
+ }
211
+ return 'skipped';
212
+ }
190
213
  if (this.inFlight.has(sessionId)) {
191
214
  logger.log(
192
215
  `[history-indexer] session ${sessionId} is already being indexed, skipping`,
@@ -197,7 +220,7 @@ export class SessionHistoryIndexer {
197
220
  try {
198
221
  for (let attempt = 1; attempt <= this.maxRetries; attempt += 1) {
199
222
  try {
200
- const outcome = await this.processOnce(sessionId);
223
+ const outcome = await this.processOnce(sessionId, entityDid);
201
224
  if (outcome === 'processed') {
202
225
  logger.log(
203
226
  `[history-indexer] indexed session ${sessionId} into the memory engine`,
@@ -223,7 +246,10 @@ export class SessionHistoryIndexer {
223
246
  }
224
247
  }
225
248
 
226
- private async processOnce(sessionId: string): Promise<IndexOutcome> {
249
+ private async processOnce(
250
+ sessionId: string,
251
+ entityDid: string,
252
+ ): Promise<IndexOutcome> {
227
253
  const { deps } = this;
228
254
  const { logger } = deps;
229
255
  const memoryEngineUrl = deps.memoryEngineUrl!.replace(/\/+$/, '');
@@ -306,6 +332,7 @@ export class SessionHistoryIndexer {
306
332
  headers: {
307
333
  Authorization: `Bearer ${invocation}`,
308
334
  'X-Auth-Type': 'ucan',
335
+ [MEMORY_ENTITY_HEADER]: entityDid,
309
336
  'x-room-id': roomId,
310
337
  'Content-Type': 'application/json',
311
338
  },
@@ -9,7 +9,8 @@
9
9
  * memory-context block before the model's first call.
10
10
  *
11
11
  * Policy:
12
- * - The result is a function of the room (and the oracle), not the session,
12
+ * - The result is a function of the room (and the oracle's domain entity,
13
+ * which keys every memory space on the engine), not the session,
13
14
  * so the cache is keyed by room: a new session in the same room reuses it.
14
15
  * Positive entries live `cacheTtlMs` (5 min) in an LRU of `maxCachedRooms`.
15
16
  * A successful batch is cached even when every slot is empty — "no
@@ -25,7 +26,9 @@
25
26
  * a memory write and after session history is indexed.
26
27
  * - A missing signing key or a failed invocation mint is a skip, never a
27
28
  * negative-cache entry: the user may delegate `ixo:memory` before the next
28
- * turn.
29
+ * turn. So is a missing domain entity (`ORACLE_ENTITY_DID`): the engine
30
+ * refuses a request that names none, so none is sent; it is warned about
31
+ * once per fetcher.
29
32
  * - One gather per room at a time; a second caller joins it. The blocking
30
33
  * `fetch` waits at most `blockingCapMs`; past the cap the gather keeps
31
34
  * running and fills the cache for later turns (the caller keeps the
@@ -41,6 +44,7 @@
41
44
  */
42
45
  import type { MemoryContextSection } from '../core/prompt-composer';
43
46
  import type { Logger, UserContextData } from '../plugin-api/types';
47
+ import { MEMORY_ENTITY_HEADER } from '../plugins/memory/memory-entity';
44
48
  import type {
45
49
  ComparisonOperator,
46
50
  EntityType,
@@ -99,15 +103,17 @@ const RECENT_WINDOW_DAYS = 90;
99
103
  * brittle), `node_labels` only where it discriminates, and the `recent`
100
104
  * slot bounded to the last 90 days explicitly (the server's
101
105
  * `recent_memory` strategy injects the same lower bound and keeps an
102
- * existing one).
106
+ * existing one). `oracle_dids` names the oracle's domain entity.
103
107
  */
104
108
  export function buildUserContextQueries(
105
- oracleDid: string,
109
+ entityDid: string,
106
110
  now: Date,
107
111
  ): UserContextQuery[] {
108
112
  const recentSince = new Date(now.getTime());
109
113
  recentSince.setUTCDate(recentSince.getUTCDate() - RECENT_WINDOW_DAYS);
110
- const oracle_dids = [oracleDid];
114
+ // The wire name predates entity keying; the engine reads its entries as
115
+ // domain entity DIDs (the knowledge spaces the queries also search).
116
+ const oracle_dids = [entityDid];
111
117
  return [
112
118
  {
113
119
  oracle_dids,
@@ -471,8 +477,13 @@ function perSlot<T>(
471
477
  export interface UserContextFetcherDeps {
472
478
  /** `MEMORY_ENGINE_URL`; undefined → every call is `skipped`, no request. */
473
479
  memoryEngineUrl: string | undefined;
474
- /** `ORACLE_DID` — the knowledge space the queries search (`oracle_dids`). */
475
- oracleDid: string;
480
+ /**
481
+ * The configured `ORACLE_ENTITY_DID` (a well-formed domain entity DID) —
482
+ * sent as `x-oracle-entity-did` and as the queries' `oracle_dids`. Never
483
+ * the `ORACLE_DID` fallback: undefined → every call is `skipped`
484
+ * (`no-entity`), no request, one warning per fetcher.
485
+ */
486
+ entityDid: string | undefined;
476
487
  /** The user's UCAN surface, bound to the user's DID by the caller. */
477
488
  ucan: {
478
489
  hasSigningKey(): boolean;
@@ -515,6 +526,7 @@ const DEFAULT_MAX_CACHED_ROOMS = 64;
515
526
 
516
527
  export type UserContextSkipReason =
517
528
  | 'no-engine-url'
529
+ | 'no-entity'
518
530
  | 'no-signing-key'
519
531
  | 'no-invocation'
520
532
  | 'recently-failed'
@@ -616,6 +628,9 @@ export class UserContextFetcher {
616
628
 
617
629
  private readonly maxCachedRooms: number;
618
630
 
631
+ /** The missing-entity warning has been logged (it is logged once). */
632
+ private warnedNoEntity = false;
633
+
619
634
  constructor(
620
635
  private readonly deps: UserContextFetcherDeps,
621
636
  options: UserContextFetcherOptions = {},
@@ -773,6 +788,16 @@ export class UserContextFetcher {
773
788
  );
774
789
  return { kind: 'skipped', reason: 'no-engine-url' };
775
790
  }
791
+ const entityDid = this.deps.entityDid;
792
+ if (!entityDid) {
793
+ if (!this.warnedNoEntity) {
794
+ this.warnedNoEntity = true;
795
+ logger.warn(
796
+ `[user-context] skipped: ORACLE_ENTITY_DID is not a domain entity DID (did:ixo:entity:<32 hex>); the memory engine keys every space by it, so no first-turn context is gathered`,
797
+ );
798
+ }
799
+ return { kind: 'skipped', reason: 'no-entity' };
800
+ }
776
801
  if (!roomId) {
777
802
  logger.warn(`[user-context] skipped: no oracle room for this turn`);
778
803
  return { kind: 'skipped', reason: 'no-room' };
@@ -807,7 +832,7 @@ export class UserContextFetcher {
807
832
 
808
833
  const entry: InFlightGather = {
809
834
  startedAt: this.now(),
810
- promise: this.gather(engineUrl, roomId).then(
835
+ promise: this.gather(engineUrl, entityDid, roomId).then(
811
836
  (result) => this.settle(roomId, entry, result),
812
837
  (err: unknown) =>
813
838
  this.settle(roomId, entry, {
@@ -870,6 +895,7 @@ export class UserContextFetcher {
870
895
  /** Mint the invocation, run the batch, map the slots. Never rejects. */
871
896
  private async gather(
872
897
  engineUrl: string,
898
+ entityDid: string,
873
899
  roomId: string,
874
900
  ): Promise<GatherResult> {
875
901
  const { logger, ucan } = this.deps;
@@ -905,14 +931,12 @@ export class UserContextFetcher {
905
931
  headers: {
906
932
  Authorization: `Bearer ${invocation}`,
907
933
  'X-Auth-Type': 'ucan',
934
+ [MEMORY_ENTITY_HEADER]: entityDid,
908
935
  'x-room-id': roomId,
909
936
  'Content-Type': 'application/json',
910
937
  },
911
938
  body: JSON.stringify({
912
- queries: buildUserContextQueries(
913
- this.deps.oracleDid,
914
- new Date(this.now()),
915
- ),
939
+ queries: buildUserContextQueries(entityDid, new Date(this.now())),
916
940
  }),
917
941
  signal: AbortSignal.timeout(this.requestTimeoutMs),
918
942
  });
@@ -12,6 +12,10 @@ export {
12
12
  type MemoryMcpFactory,
13
13
  type UpstreamMcpTool,
14
14
  } from './memory-tools';
15
+ export {
16
+ MEMORY_ENTITY_HEADER,
17
+ ORACLE_ENTITY_DID_PATTERN,
18
+ } from './memory-entity';
15
19
  export { buildMemoryHeaders } from './memory-ucan';
16
20
  export { MemoryPlugin, type MemoryPluginOptions } from './memory.plugin';
17
21
  export type * from './types';
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * The Memory Engine keys every memory space by the oracle's domain entity
5
+ * (`did:ixo:entity:<32 hex>`): the user's personal space is
6
+ * `sha256(<userDid>-<entityDid>)` and the shared knowledge spaces are the
7
+ * entity's. Every engine request names the entity in this header; the engine
8
+ * refuses a request without it and checks (on chain) that the entity lists
9
+ * the oracle's agent account as a Matrix admin.
10
+ */
11
+ export const MEMORY_ENTITY_HEADER = 'x-oracle-entity-did';
12
+
13
+ /** The shape the Memory Engine accepts for {@link MEMORY_ENTITY_HEADER}. */
14
+ export const ORACLE_ENTITY_DID_PATTERN = /^did:ixo:entity:[0-9a-f]{32}$/;
15
+
16
+ const ENTITY_REQUIRED_MESSAGE =
17
+ "ORACLE_ENTITY_DID is required by the memory plugin: set it to the oracle's domain entity DID (did:ixo:entity: followed by 32 lower-case hex characters). The Memory Engine keys every memory space by that entity and refuses requests without it.";
18
+
19
+ const ENTITY_SHAPE_MESSAGE =
20
+ "ORACLE_ENTITY_DID must be the oracle's domain entity DID (did:ixo:entity: followed by 32 lower-case hex characters); the Memory Engine refuses any other value.";
21
+
22
+ /**
23
+ * `ORACLE_ENTITY_DID` as the memory plugin requires it. The core schema keeps
24
+ * the variable optional (rooms and feedback fall back to `ORACLE_DID`); the
25
+ * memory plugin redeclares it so an oracle with `MEMORY_MCP_URL` and no
26
+ * entity fails at boot. The engine never receives the `ORACLE_DID` fallback.
27
+ */
28
+ export const oracleEntityDidSchema = z
29
+ .string({
30
+ error: (issue) =>
31
+ issue.input === undefined
32
+ ? ENTITY_REQUIRED_MESSAGE
33
+ : ENTITY_SHAPE_MESSAGE,
34
+ })
35
+ .regex(ORACLE_ENTITY_DID_PATTERN, ENTITY_SHAPE_MESSAGE);
36
+
37
+ /**
38
+ * The configured `ORACLE_ENTITY_DID` when it is a well-formed domain entity
39
+ * DID, else `undefined`. For the engine paths outside the plugin (first-turn
40
+ * prefetch, session-history indexing, the memory debug route): they read the
41
+ * merged env directly and run wherever `MEMORY_ENGINE_URL` survives
42
+ * validation, which another plugin than this one may declare, so the
43
+ * plugin's schema does not cover them.
44
+ */
45
+ export function memoryEntityDid(value: unknown): string | undefined {
46
+ return typeof value === 'string' && ORACLE_ENTITY_DID_PATTERN.test(value)
47
+ ? value
48
+ : undefined;
49
+ }
@@ -28,8 +28,9 @@ export const MEMORY_CLEAR_MCP_NAME = 'memory-engine__clear';
28
28
  * destructive `clear` stays in the default set. `add_oracle_knowledge` is in
29
29
  * the set so the controllers and owners of any oracle can grow its shared
30
30
  * knowledge from chat without a per-oracle code change: the Memory Engine
31
- * itself checks on chain that the caller controls or owns an entity linked to
32
- * the oracle and refuses everyone else, so listing the tool for every user
31
+ * itself checks on chain that the caller is a member (controller, owner,
32
+ * Matrix admin or DAO member) of the oracle's domain entity and refuses
33
+ * everyone else, so listing the tool for every user
33
34
  * costs one tool description and grants nothing. Forks that want a different
34
35
  * surface (drop `clear`, add `delete_edge`) pass an explicit list via plugin
35
36
  * options.
@@ -370,7 +371,8 @@ function buildLazyUpstreamTools(
370
371
 
371
372
  /**
372
373
  * Default factory: authenticates with the per-request UCAN headers from
373
- * {@link buildMemoryHeaders}, then serves the upstream tool list.
374
+ * {@link buildMemoryHeaders} (which name the oracle's domain entity,
375
+ * `entityDid`), then serves the upstream tool list.
374
376
  *
375
377
  * The first request per isolate connects and lists tools exactly like the
376
378
  * old always-fetch path, then snapshots the definitions. Subsequent requests
@@ -384,9 +386,10 @@ function buildLazyUpstreamTools(
384
386
  */
385
387
  export function createDefaultMemoryMcpFactory(
386
388
  memoryMcpUrl: string,
389
+ entityDid: string,
387
390
  ): MemoryMcpFactory {
388
391
  return async (runCtx) => {
389
- const headers = await buildMemoryHeaders(runCtx, memoryMcpUrl);
392
+ const headers = await buildMemoryHeaders(runCtx, memoryMcpUrl, entityDid);
390
393
  if (!headers) return null;
391
394
 
392
395
  const cached = toolDefsCache.get(memoryMcpUrl);
@@ -1,6 +1,7 @@
1
1
  import type { RuntimeContext } from '../../plugin-api/types';
2
2
  import { MEMORY_CAPABILITY } from '../delegated-capabilities';
3
3
  import { mintInvocationSafely, resolveServiceDidSafely } from '../ucan-failure';
4
+ import { MEMORY_ENTITY_HEADER } from './memory-entity';
4
5
 
5
6
  /**
6
7
  * Build the headers needed to call the Memory Engine MCP server on behalf of
@@ -9,7 +10,13 @@ import { mintInvocationSafely, resolveServiceDidSafely } from '../ucan-failure';
9
10
  * 1. Resolve the memory service DID via `runCtx.ucan.resolveServiceDid`
10
11
  * (delegates the did:web lookup + cache to the UCAN service).
11
12
  * 2. Mint an `ixo:memory` invocation using the user's signing key.
12
- * 3. Layer the resulting Bearer token plus the active `x-room-id`.
13
+ * 3. Layer the resulting Bearer token, the oracle's domain entity
14
+ * (`x-oracle-entity-did`, which keys every memory space on the engine)
15
+ * and the active `x-room-id`.
16
+ *
17
+ * `entityDid` is the configured `ORACLE_ENTITY_DID` (validated by the memory
18
+ * plugin's `configSchema`), passed in because `RuntimeContext` carries no
19
+ * oracle identity. It is never the core's `ORACLE_DID` fallback.
13
20
  *
14
21
  * UCAN-only — no Matrix-OpenID fallback. If resolution or minting fails for
15
22
  * any reason (no signing key, no cached delegation, did:web unresolved,
@@ -22,6 +29,7 @@ import { mintInvocationSafely, resolveServiceDidSafely } from '../ucan-failure';
22
29
  export async function buildMemoryHeaders(
23
30
  runCtx: RuntimeContext,
24
31
  memoryMcpUrl: string,
32
+ entityDid: string,
25
33
  ): Promise<Record<string, string> | null> {
26
34
  const memoryDid = await resolveServiceDidSafely(
27
35
  runCtx,
@@ -44,6 +52,7 @@ export async function buildMemoryHeaders(
44
52
  Authorization: `Bearer ${invocation}`,
45
53
  'X-Auth-Type': 'ucan',
46
54
  'User-Agent': 'LangChain-MCP-Client/1.0',
55
+ [MEMORY_ENTITY_HEADER]: entityDid,
47
56
  };
48
57
  if (runCtx.session.roomId) {
49
58
  headers['x-room-id'] = runCtx.session.roomId;
@@ -14,12 +14,16 @@ import {
14
14
  fetchMemoryTools,
15
15
  type MemoryMcpFactory,
16
16
  } from './memory-tools';
17
+ import { oracleEntityDidSchema } from './memory-entity';
17
18
 
18
19
  const configSchema = z.object({
19
20
  MEMORY_MCP_URL: z.string().url('MEMORY_MCP_URL must be a valid HTTP(S) URL.'),
20
21
  MEMORY_ENGINE_URL: z
21
22
  .string()
22
23
  .url('MEMORY_ENGINE_URL must be a valid HTTP(S) URL.'),
24
+ // The Memory Engine keys every space by the oracle's domain entity; an
25
+ // oracle without one gets no memory at all, so it fails at boot instead.
26
+ ORACLE_ENTITY_DID: oracleEntityDidSchema,
23
27
  });
24
28
 
25
29
  const manifest: PluginManifest = {
@@ -51,9 +55,11 @@ const manifest: PluginManifest = {
51
55
  export interface MemoryPluginOptions {
52
56
  /**
53
57
  * Override the MCP-tools factory. Defaults to an HTTP MCP client against
54
- * `MEMORY_MCP_URL` authenticated with a per-request UCAN invocation.
58
+ * `MEMORY_MCP_URL` authenticated with a per-request UCAN invocation and
59
+ * the oracle's domain entity (`ORACLE_ENTITY_DID`, the engine's
60
+ * `x-oracle-entity-did` header).
55
61
  */
56
- mcpFactory?: (memoryMcpUrl: string) => MemoryMcpFactory;
62
+ mcpFactory?: (memoryMcpUrl: string, entityDid: string) => MemoryMcpFactory;
57
63
  /**
58
64
  * Filter the upstream tool surface. Defaults to {@link DEFAULT_MEMORY_TOOLS}
59
65
  * (search + add + add-oracle-knowledge + delete-episode + clear). Pass an
@@ -62,9 +68,15 @@ export interface MemoryPluginOptions {
62
68
  selectedTools?: readonly string[];
63
69
  }
64
70
 
65
- function resolveMemoryUrl(config: MergedConfig): string {
71
+ function resolveMemoryConfig(config: MergedConfig): {
72
+ memoryMcpUrl: string;
73
+ entityDid: string;
74
+ } {
66
75
  const parsed = configSchema.parse(config);
67
- return parsed.MEMORY_MCP_URL;
76
+ return {
77
+ memoryMcpUrl: parsed.MEMORY_MCP_URL,
78
+ entityDid: parsed.ORACLE_ENTITY_DID,
79
+ };
68
80
  }
69
81
 
70
82
  /**
@@ -90,7 +102,10 @@ export class MemoryPlugin extends OraclePlugin {
90
102
 
91
103
  override readonly autoDetectHint = 'MEMORY_MCP_URL';
92
104
 
93
- private readonly mcpFactory: (memoryMcpUrl: string) => MemoryMcpFactory;
105
+ private readonly mcpFactory: (
106
+ memoryMcpUrl: string,
107
+ entityDid: string,
108
+ ) => MemoryMcpFactory;
94
109
 
95
110
  private readonly selectedTools: readonly string[];
96
111
 
@@ -107,8 +122,8 @@ export class MemoryPlugin extends OraclePlugin {
107
122
  }
108
123
 
109
124
  override async getRequestTools(rtCtx: RuntimeContext): Promise<PluginTool[]> {
110
- const memoryUrl = resolveMemoryUrl(rtCtx.config);
111
- const factory = this.mcpFactory(memoryUrl);
125
+ const { memoryMcpUrl, entityDid } = resolveMemoryConfig(rtCtx.config);
126
+ const factory = this.mcpFactory(memoryMcpUrl, entityDid);
112
127
  return fetchMemoryTools(rtCtx, factory, this.selectedTools);
113
128
  }
114
129