@ixo/oracle-runtime-workers 0.22.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/README.md +15 -13
- package/package.json +10 -4
- package/src/action-proposals/policy.ts +54 -0
- package/src/action-proposals/reconcile.ts +87 -0
- package/src/action-proposals/store.ts +863 -0
- package/src/action-proposals/topic-budget.ts +84 -0
- package/src/action-proposals/topic-chain.ts +272 -0
- package/src/action-proposals/topic-effects.ts +44 -0
- package/src/action-proposals/topic-errors.ts +79 -0
- package/src/action-proposals/topic-history.ts +267 -0
- package/src/action-proposals/topic-matrix.ts +292 -0
- package/src/action-proposals/topic-prepare.ts +299 -0
- package/src/action-proposals/topic-store.ts +999 -0
- package/src/action-proposals/topic-verify.ts +279 -0
- package/src/action-proposals/verify.ts +223 -0
- package/src/channels/auth.ts +6 -1
- package/src/channels/contract.ts +4 -0
- package/src/channels/turns.ts +1 -0
- package/src/core/env.ts +93 -6
- package/src/core/main-agent-types.ts +8 -0
- package/src/core/main-agent.ts +2 -0
- package/src/core/runtime-context.ts +12 -0
- package/src/delivery/plan.ts +43 -2
- package/src/delivery/schema.ts +25 -1
- package/src/delivery/types.ts +13 -2
- package/src/do/ambient.ts +6 -0
- package/src/do/contracts.ts +39 -0
- package/src/do/user-oracle-do.ts +1275 -8
- package/src/hub-actions/client.ts +163 -0
- package/src/hub-actions/request.ts +110 -0
- package/src/hub-actions/store.ts +485 -0
- package/src/matrix/gateway-do.ts +176 -0
- package/src/matrix/reply-parts.ts +8 -2
- package/src/memory/session-history-indexer.ts +30 -3
- package/src/memory/user-context-fetcher.ts +36 -12
- package/src/plugin-api/types.ts +54 -0
- package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
- package/src/plugins/action-proposals/evidence.ts +67 -0
- package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/memory/index.ts +4 -0
- package/src/plugins/memory/memory-entity.ts +49 -0
- package/src/plugins/memory/memory-tools.ts +7 -4
- package/src/plugins/memory/memory-ucan.ts +10 -1
- package/src/plugins/memory/memory.plugin.ts +22 -7
- package/src/shell/app.ts +195 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
operationJsonSchema,
|
|
3
|
+
parseOperation,
|
|
4
|
+
type ActionReferenceV1,
|
|
5
|
+
} from '@ixo/action-protocol';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { OraclePlugin } from '../../plugin-api/oracle-plugin';
|
|
8
|
+
import type {
|
|
9
|
+
PluginManifest,
|
|
10
|
+
PluginTool,
|
|
11
|
+
RuntimeContext,
|
|
12
|
+
} from '../../plugin-api/types';
|
|
13
|
+
|
|
14
|
+
const configSchema = z.object({
|
|
15
|
+
HUB_ACTIONS_ENABLED: z.enum(['true', 'false']).default('false'),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const operationSchema = z.fromJSONSchema({
|
|
19
|
+
...operationJsonSchema,
|
|
20
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
21
|
+
});
|
|
22
|
+
const requestSchema = z.strictObject({ operation: operationSchema });
|
|
23
|
+
const statusSchema = z.strictObject({
|
|
24
|
+
actionId: z.string().regex(/^[A-Za-z0-9_-]{32}$/),
|
|
25
|
+
});
|
|
26
|
+
const discoverSchema = z.strictObject({});
|
|
27
|
+
|
|
28
|
+
export type HubActionRequestResult =
|
|
29
|
+
| { status: 'prepared'; actionReference: ActionReferenceV1; summary: string }
|
|
30
|
+
| {
|
|
31
|
+
status: 'unavailable' | 'expired';
|
|
32
|
+
reason: string;
|
|
33
|
+
/** The earlier action that blocks this one (an unconfirmed email). */
|
|
34
|
+
priorActionId?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The idempotency key the Hub sees: a fixed-length digest of the run and
|
|
39
|
+
* tool call (the Hub allows 120 characters; a run id alone is 72).
|
|
40
|
+
*/
|
|
41
|
+
async function hubRequestKey(runId: string, toolCallId: string) {
|
|
42
|
+
const bytes = new Uint8Array(
|
|
43
|
+
await crypto.subtle.digest(
|
|
44
|
+
'SHA-256',
|
|
45
|
+
new TextEncoder().encode(`${runId}:${toolCallId}`),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function requestHubAction(
|
|
52
|
+
input: unknown,
|
|
53
|
+
ctx: RuntimeContext,
|
|
54
|
+
): Promise<HubActionRequestResult> {
|
|
55
|
+
const parsed = requestSchema.safeParse(input);
|
|
56
|
+
if (!parsed.success)
|
|
57
|
+
return { status: 'unavailable', reason: 'An operation is required.' };
|
|
58
|
+
if (ctx.config.HUB_ACTIONS_ENABLED !== 'true' || !ctx.hubActions)
|
|
59
|
+
return {
|
|
60
|
+
status: 'unavailable',
|
|
61
|
+
reason: 'Hub actions are not enabled for this oracle.',
|
|
62
|
+
};
|
|
63
|
+
const channel = ctx.session.channel;
|
|
64
|
+
if (
|
|
65
|
+
ctx.session.client !== 'channel' ||
|
|
66
|
+
!channel ||
|
|
67
|
+
!ctx.session.runId ||
|
|
68
|
+
!ctx.toolCallId
|
|
69
|
+
)
|
|
70
|
+
return {
|
|
71
|
+
status: 'unavailable',
|
|
72
|
+
reason: 'A verified channel turn is required.',
|
|
73
|
+
};
|
|
74
|
+
let operation;
|
|
75
|
+
try {
|
|
76
|
+
operation = parseOperation(parsed.data.operation);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// The protocol's own words ("Invalid fromAddress"): the model can fix
|
|
79
|
+
// the field instead of guessing.
|
|
80
|
+
return {
|
|
81
|
+
status: 'unavailable',
|
|
82
|
+
reason: `The requested operation is unsupported or incomplete${error instanceof Error && error.message ? ` (${error.message})` : ''}.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let reference: ActionReferenceV1;
|
|
86
|
+
try {
|
|
87
|
+
reference = await ctx.hubActions.request({
|
|
88
|
+
ownerDid: ctx.user.did,
|
|
89
|
+
sessionId: ctx.session.id,
|
|
90
|
+
runId: ctx.session.runId,
|
|
91
|
+
requestId: ctx.session.requestId,
|
|
92
|
+
provider: channel.provider,
|
|
93
|
+
bindingId: channel.bindingId,
|
|
94
|
+
bindingRevision: channel.bindingRevision,
|
|
95
|
+
requestKey: await hubRequestKey(ctx.session.runId, ctx.toolCallId),
|
|
96
|
+
operation,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// A cap reached, a request that expired, a run no user message
|
|
100
|
+
// started: the runtime's own words, so the model can tell the user.
|
|
101
|
+
// Anything else stays an error.
|
|
102
|
+
if (
|
|
103
|
+
error instanceof Error &&
|
|
104
|
+
error.name === 'HubActionRefused' &&
|
|
105
|
+
'kind' in error
|
|
106
|
+
)
|
|
107
|
+
return {
|
|
108
|
+
status: error.kind === 'expired' ? 'expired' : 'unavailable',
|
|
109
|
+
reason: error.message,
|
|
110
|
+
...('priorActionId' in error && typeof error.priorActionId === 'string'
|
|
111
|
+
? { priorActionId: error.priorActionId }
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
status: 'prepared',
|
|
118
|
+
actionReference: reference,
|
|
119
|
+
summary: reference.title,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function hubActionStatus(
|
|
124
|
+
input: unknown,
|
|
125
|
+
ctx: RuntimeContext,
|
|
126
|
+
): Promise<unknown> {
|
|
127
|
+
const parsed = statusSchema.safeParse(input);
|
|
128
|
+
if (
|
|
129
|
+
!parsed.success ||
|
|
130
|
+
!ctx.hubActions ||
|
|
131
|
+
ctx.config.HUB_ACTIONS_ENABLED !== 'true'
|
|
132
|
+
)
|
|
133
|
+
return { status: 'unavailable' };
|
|
134
|
+
return {
|
|
135
|
+
status: 'known',
|
|
136
|
+
outcome: await ctx.hubActions.status(ctx.user.did, parsed.data.actionId),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function hubActionCapabilities(
|
|
141
|
+
_input: unknown,
|
|
142
|
+
ctx: RuntimeContext,
|
|
143
|
+
): Promise<unknown> {
|
|
144
|
+
if (
|
|
145
|
+
ctx.config.HUB_ACTIONS_ENABLED !== 'true' ||
|
|
146
|
+
ctx.session.client !== 'channel' ||
|
|
147
|
+
!ctx.session.channel ||
|
|
148
|
+
!ctx.hubActions
|
|
149
|
+
)
|
|
150
|
+
return { status: 'unavailable' };
|
|
151
|
+
return ctx.hubActions.capabilities();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function hubActionOptions(
|
|
155
|
+
_input: unknown,
|
|
156
|
+
ctx: RuntimeContext,
|
|
157
|
+
): Promise<unknown> {
|
|
158
|
+
const channel = ctx.session.channel;
|
|
159
|
+
if (
|
|
160
|
+
ctx.config.HUB_ACTIONS_ENABLED !== 'true' ||
|
|
161
|
+
ctx.session.client !== 'channel' ||
|
|
162
|
+
!channel ||
|
|
163
|
+
!ctx.hubActions
|
|
164
|
+
)
|
|
165
|
+
return { status: 'unavailable' };
|
|
166
|
+
return ctx.hubActions.options({
|
|
167
|
+
ownerDid: ctx.user.did,
|
|
168
|
+
provider: channel.provider,
|
|
169
|
+
bindingId: channel.bindingId,
|
|
170
|
+
bindingRevision: channel.bindingRevision,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const manifest: PluginManifest = {
|
|
175
|
+
title: 'Auth Hub action review',
|
|
176
|
+
summary:
|
|
177
|
+
'Prepare a registered operation for owner review and read its verified status.',
|
|
178
|
+
whenToUse: [
|
|
179
|
+
'The user requests a registered form submission or a transfer.',
|
|
180
|
+
'The user asks to review an exact private resource or to sign a statement.',
|
|
181
|
+
'The user asks to connect Gmail or Calendar, send a reviewed email or add an event.',
|
|
182
|
+
'The user asks for document access or to save a private note.',
|
|
183
|
+
],
|
|
184
|
+
whenNotToUse: [
|
|
185
|
+
'Signing, executing, or treating a resource open as acceptance.',
|
|
186
|
+
],
|
|
187
|
+
tags: ['actions', 'channels'],
|
|
188
|
+
category: 'integration',
|
|
189
|
+
visibility: 'on-demand',
|
|
190
|
+
stability: 'experimental',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export class HubActionsPlugin extends OraclePlugin {
|
|
194
|
+
readonly name = 'hub-actions';
|
|
195
|
+
readonly version = '0.1.0';
|
|
196
|
+
readonly manifest = manifest;
|
|
197
|
+
override readonly configSchema = configSchema;
|
|
198
|
+
/** The flag alone is not enough: the owner object needs the Hub's URL and this oracle's key. */
|
|
199
|
+
override autoDetect(env: Record<string, unknown>): boolean {
|
|
200
|
+
return (
|
|
201
|
+
env.HUB_ACTIONS_ENABLED === 'true' &&
|
|
202
|
+
typeof env.AUTH_HUB_URL === 'string' &&
|
|
203
|
+
env.AUTH_HUB_URL.length > 0 &&
|
|
204
|
+
typeof env.AUTH_HUB_CHANNEL_SERVICE_KEY === 'string' &&
|
|
205
|
+
env.AUTH_HUB_CHANNEL_SERVICE_KEY.length > 0
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
override getTools(): PluginTool[] {
|
|
209
|
+
return [
|
|
210
|
+
{
|
|
211
|
+
name: 'hub_action_capabilities',
|
|
212
|
+
description:
|
|
213
|
+
"Read this authenticated oracle account's enabled operation kinds and exact registered form field schema before proposing a Hub action.",
|
|
214
|
+
schema: discoverSchema,
|
|
215
|
+
effect: 'read',
|
|
216
|
+
handler: hubActionCapabilities,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'hub_action_options',
|
|
220
|
+
description:
|
|
221
|
+
'Read this authenticated owner’s confirmed Gmail and Calendar connections, requestable documents and action resources. Use each returned service, identifier and label before preparing a related Hub review. A Gmail profile connection does not authorize sending.',
|
|
222
|
+
schema: discoverSchema,
|
|
223
|
+
effect: 'read',
|
|
224
|
+
handler: hubActionOptions,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'request_hub_action',
|
|
228
|
+
description:
|
|
229
|
+
'Prepare a supported operation for the authenticated channel owner to review in Auth Hub. Never approves, signs, or executes. For email, propose one plain-text message to one recipient using a confirmed Gmail send connection. Supply only the operation candidate; the runtime supplies identity and turn binding. Never prepare a replacement email after an unknown send without the user checking Gmail’s Sent folder and asking again.',
|
|
230
|
+
schema: requestSchema,
|
|
231
|
+
effect: 'write',
|
|
232
|
+
handler: requestHubAction,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'hub_action_status',
|
|
236
|
+
description:
|
|
237
|
+
'Read the verified status of a Hub action already prepared for this owner. Never executes.',
|
|
238
|
+
schema: statusSchema,
|
|
239
|
+
effect: 'read',
|
|
240
|
+
handler: hubActionStatus,
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { ActionProposalsPlugin } from './action-proposals/action-proposals.plugin';
|
|
2
|
+
import { HubActionsPlugin } from './hub-actions/hub-actions.plugin';
|
|
3
|
+
export { ActionProposalsPlugin } from './action-proposals/action-proposals.plugin';
|
|
4
|
+
export { HubActionsPlugin } from './hub-actions/hub-actions.plugin';
|
|
1
5
|
/**
|
|
2
6
|
* Bundled Workers-runtime plugins — ports of the Node runtime's MCP / fetch
|
|
3
7
|
* plugins. Each is gated the same way as on Node: `autoDetect` on its env
|
|
@@ -66,6 +70,8 @@ export const tasksPlugin = TasksPlugin;
|
|
|
66
70
|
* instances instead of using these singletons.
|
|
67
71
|
*/
|
|
68
72
|
export const BUNDLED_WORKERS_PLUGINS = [
|
|
73
|
+
new ActionProposalsPlugin(),
|
|
74
|
+
new HubActionsPlugin(),
|
|
69
75
|
memoryPlugin,
|
|
70
76
|
sandboxPlugin,
|
|
71
77
|
firecrawlPlugin,
|
|
@@ -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
|
|
32
|
-
*
|
|
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}
|
|
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
|
|
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
|
|
71
|
+
function resolveMemoryConfig(config: MergedConfig): {
|
|
72
|
+
memoryMcpUrl: string;
|
|
73
|
+
entityDid: string;
|
|
74
|
+
} {
|
|
66
75
|
const parsed = configSchema.parse(config);
|
|
67
|
-
return
|
|
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: (
|
|
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
|
|
111
|
-
const factory = this.mcpFactory(
|
|
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
|
|