@ixo/oracle-runtime-workers 0.23.0 → 0.25.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.
@@ -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
 
package/src/shell/app.ts CHANGED
@@ -78,22 +78,7 @@ import {
78
78
  } from '../feedback/submit';
79
79
  import { z } from 'zod';
80
80
  import { parseOutcome } from '@ixo/action-protocol';
81
-
82
- async function equalOutcomeKeys(
83
- provided: string | null,
84
- expected: string | undefined,
85
- ): Promise<boolean> {
86
- if (!provided || !expected || expected.length < 32 || provided.length > 512)
87
- return false;
88
- const digest = async (value: string) =>
89
- new Uint8Array(
90
- await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)),
91
- );
92
- const [a, b] = await Promise.all([digest(provided), digest(expected)]);
93
- let difference = 0;
94
- for (let i = 0; i < a.length; i++) difference |= (a[i] ?? 0) ^ (b[i] ?? 0);
95
- return difference === 0;
96
- }
81
+ import { verifyHubOutcomeCallback } from '../hub-actions/outcome-signature';
97
82
 
98
83
  const DebugEventBody = z.object({
99
84
  type: z
@@ -217,6 +202,22 @@ export const BYO_LLM_BODY_BYTES = 64 * 1024;
217
202
  export const DEBUG_BODY_BYTES = 64 * 1024;
218
203
 
219
204
  /** A body cap enforced before the body is buffered: over it, 413 and the handler never runs. */
205
+ /**
206
+ * The audience a signed Hub outcome callback must be signed for: the origin
207
+ * of `ORACLE_PUBLIC_URL` when it is set, else the origin of the request URL.
208
+ * An unparsable public URL yields '' (which never verifies).
209
+ */
210
+ export function callbackAudience(
211
+ publicUrl: string | undefined,
212
+ requestUrl: string,
213
+ ): string {
214
+ try {
215
+ return new URL(publicUrl || requestUrl).origin;
216
+ } catch {
217
+ return '';
218
+ }
219
+ }
220
+
220
221
  function limitBody(maxSize: number) {
221
222
  return bodyLimit({
222
223
  maxSize,
@@ -455,17 +456,32 @@ export function createShell(
455
456
  });
456
457
 
457
458
  app.use('/api/internal/actions/outcomes', limitBody(64 * 1024));
459
+ // Auth Hub's signed outcome callbacks: on whenever Hub actions are
460
+ // configured. The signature is checked against the Hub's own did:web key
461
+ // (from AUTH_HUB_URL) for this oracle's origin, then the owner object
462
+ // confirms the outcome through Auth Hub's authenticated status API.
458
463
  app.post('/api/internal/actions/outcomes', async (c) => {
459
464
  if (
460
465
  c.env.HUB_ACTIONS_ENABLED !== 'true' ||
461
- !(await equalOutcomeKeys(
462
- c.req.header('x-auth-hub-outcome-key') ?? null,
463
- c.env.AUTH_HUB_ACTION_OUTCOME_KEY,
464
- ))
466
+ !c.env.AUTH_HUB_URL ||
467
+ !c.env.AUTH_HUB_CHANNEL_SERVICE_KEY
465
468
  )
466
469
  return c.json({ code: 'unauthorized' }, 401);
467
470
  const body = await c.req.raw.text();
468
471
  if (body.length > 64_000) return c.json({ code: 'request_too_large' }, 413);
472
+ if (
473
+ !(await verifyHubOutcomeCallback({
474
+ hubUrl: c.env.AUTH_HUB_URL,
475
+ rawBody: body,
476
+ headers: c.req.raw.headers,
477
+ // The origin the Hub signs for: the configured public URL when
478
+ // set (a Worker reachable under several hostnames), else the
479
+ // origin this request came in on.
480
+ audience: callbackAudience(c.env.ORACLE_PUBLIC_URL, c.req.url),
481
+ ...(c.env.AUTH_HUB ? { fetcher: c.env.AUTH_HUB } : {}),
482
+ }))
483
+ )
484
+ return c.json({ code: 'unauthorized' }, 401);
469
485
  try {
470
486
  const outcome = parseOutcome(JSON.parse(body));
471
487
  await userStub(c.env, outcome.ownerDid).recordHubActionOutcome(outcome);