@ixo/oracle-runtime-workers 0.2.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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Archive an attachment's original bytes (and, for media, the helper model's
3
+ * analysis) into the user's sandbox at `/workspace/output/<name>` — the Node
4
+ * `FileProcessingService.uploadToSandbox` contract: multipart `file` + `path`
5
+ * to `<SANDBOX_MCP_URL without /mcp>/artifacts/upload`, authenticated with
6
+ * the same `Authorization: Bearer <ucan>` + `X-Auth-Type: ucan` the sandbox
7
+ * MCP client sends.
8
+ */
9
+ import { sanitizeSandboxPath } from './sanitize';
10
+
11
+ export interface SandboxUploadConfig {
12
+ sandboxMcpUrl: string;
13
+ authHeaders: Record<string, string>;
14
+ }
15
+
16
+ export interface SandboxUploadResult {
17
+ path: string;
18
+ url?: string;
19
+ previewUrl?: string;
20
+ }
21
+
22
+ const MAX_ERROR_BODY_LENGTH = 1024;
23
+
24
+ export async function uploadToSandbox(
25
+ bytes: Uint8Array,
26
+ filename: string,
27
+ destPath: string,
28
+ config: SandboxUploadConfig,
29
+ mimetype: string,
30
+ fetchImpl: typeof fetch = fetch,
31
+ ): Promise<SandboxUploadResult> {
32
+ const baseUrl = config.sandboxMcpUrl.replace(/\/mcp\/?$/, '');
33
+ const safeFilename = sanitizeSandboxPath(filename);
34
+ const safePath = sanitizeSandboxPath(destPath);
35
+ const formData = new FormData();
36
+ // Copy into a fresh ArrayBuffer-backed view so the Blob never sees a
37
+ // SharedArrayBuffer-typed slice (the DOM lib types reject those).
38
+ const copy = new Uint8Array(bytes.byteLength);
39
+ copy.set(bytes);
40
+ formData.set(
41
+ 'file',
42
+ new File([copy.buffer], safeFilename, { type: mimetype }),
43
+ );
44
+ formData.set('path', safePath);
45
+ const response = await fetchImpl(`${baseUrl}/artifacts/upload`, {
46
+ method: 'POST',
47
+ headers: { ...config.authHeaders },
48
+ body: formData,
49
+ });
50
+ if (!response.ok) {
51
+ const errorText = await response.text().catch(() => '');
52
+ throw new Error(
53
+ `Sandbox upload failed (${response.status}): ${errorText.slice(0, MAX_ERROR_BODY_LENGTH)}`,
54
+ );
55
+ }
56
+ const raw: unknown = await response.json();
57
+ const result = raw as { path?: string; url?: string; previewUrl?: string };
58
+ return {
59
+ path: typeof result.path === 'string' ? result.path : safePath,
60
+ ...(result.url ? { url: result.url } : {}),
61
+ ...(result.previewUrl ? { previewUrl: result.previewUrl } : {}),
62
+ };
63
+ }
64
+
65
+ /** The `<name>-analysis.md` the extraction lane stores next to media files. */
66
+ export function buildAnalysisMarkdown(
67
+ filename: string,
68
+ mimetype: string,
69
+ sizeBytes: number,
70
+ category: 'image' | 'video' | 'audio',
71
+ content: string,
72
+ ): string {
73
+ const labels: Record<'image' | 'video' | 'audio', string> = {
74
+ image: 'Image description',
75
+ video: 'Video description',
76
+ audio: 'Audio transcription',
77
+ };
78
+ return [
79
+ `# ${labels[category]}: ${filename}`,
80
+ '',
81
+ `- **File:** ${filename}`,
82
+ `- **Type:** ${mimetype}`,
83
+ `- **Size:** ${sizeBytes} bytes`,
84
+ `- **Processed:** ${new Date().toISOString()}`,
85
+ '',
86
+ '## Analysis',
87
+ '',
88
+ content,
89
+ ].join('\n');
90
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Filename / sandbox-path hygiene — the Node runtime's `attachment-archive.ts`
3
+ * plus `FileProcessingService.sanitizeSandboxPath`.
4
+ */
5
+
6
+ /** Where every attachment a user sends is archived in their sandbox. */
7
+ export const SANDBOX_OUTPUT_PREFIX = '/workspace/output';
8
+
9
+ /**
10
+ * Strip control characters and bracket sequences from a filename to prevent
11
+ * prompt injection when interpolated into LLM context. The archive writes
12
+ * under this exact name.
13
+ */
14
+ export function sanitizeAttachmentFilename(filename: string): string {
15
+ return (
16
+ filename
17
+ // eslint-disable-next-line no-control-regex
18
+ .replace(/[\x00-\x1f\x7f]/g, '')
19
+ .replace(/[[\]]/g, '')
20
+ .slice(0, 255)
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Sanitize a filename/path for the sandbox upload endpoint: only
26
+ * alphanumerics, dots, dashes, underscores and slashes, and no `..` segments
27
+ * so the sandbox cannot be escaped.
28
+ */
29
+ export function sanitizeSandboxPath(p: string): string {
30
+ const charset = p.replace(/[^a-zA-Z0-9._\-/]/g, '_');
31
+ return charset
32
+ .split('/')
33
+ .filter((seg) => seg !== '..')
34
+ .join('/');
35
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Wire shape of a chat attachment — the Node runtime's `AttachmentDto`
3
+ * (`POST /messages/:sessionId` body `attachments[]`, also what
4
+ * `@ixo/oracles-client-sdk` sends): either an `mxcUri` (`mxc://…` or an
5
+ * `http(s)://` URL) or a Matrix `eventId` (encrypted uploads), plus the
6
+ * filename / mimetype / size the client knows.
7
+ */
8
+
9
+ export interface AttachmentInput {
10
+ /** `mxc://server/id` or `http(s)://…`. Required when `eventId` is absent. */
11
+ mxcUri?: string;
12
+ /** Matrix event id (`$…`) — encrypted media is fetched and decrypted via the event. */
13
+ eventId?: string;
14
+ filename: string;
15
+ mimetype: string;
16
+ size?: number;
17
+ }
18
+
19
+ /** Same cap as the Node DTO (`@ArrayMaxSize`). */
20
+ export const MAX_ATTACHMENTS = 10;
21
+
22
+ const URI_RE = /^(mxc|https?):\/\/.+/i;
23
+ const EVENT_ID_RE = /^\$/;
24
+ const MAX_FILENAME = 255;
25
+
26
+ function isRecord(value: unknown): value is Record<string, unknown> {
27
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
28
+ }
29
+
30
+ /**
31
+ * Validate the request's `attachments` the way the Node DTO does. Throws an
32
+ * `Error` whose message names the offending entry — callers surface it as
33
+ * the turn's error (Node answers 400 at the controller).
34
+ */
35
+ export function parseAttachmentInputs(value: unknown): AttachmentInput[] {
36
+ if (value === undefined || value === null) return [];
37
+ if (!Array.isArray(value)) throw new Error('attachments must be an array');
38
+ if (value.length > MAX_ATTACHMENTS)
39
+ throw new Error(`attachments: at most ${MAX_ATTACHMENTS} per message`);
40
+ return value.map((entry, i) => {
41
+ if (!isRecord(entry)) throw new Error(`attachments[${i}]: not an object`);
42
+ const mxcUri = entry['mxcUri'];
43
+ const eventId = entry['eventId'];
44
+ const filename = entry['filename'];
45
+ const mimetype = entry['mimetype'];
46
+ const size = entry['size'];
47
+ if (
48
+ mxcUri !== undefined &&
49
+ (typeof mxcUri !== 'string' || !URI_RE.test(mxcUri))
50
+ )
51
+ throw new Error(
52
+ `attachments[${i}].mxcUri must start with mxc://, http://, or https://`,
53
+ );
54
+ if (
55
+ eventId !== undefined &&
56
+ (typeof eventId !== 'string' || !EVENT_ID_RE.test(eventId))
57
+ )
58
+ throw new Error(`attachments[${i}].eventId must start with $`);
59
+ if (!mxcUri && !eventId)
60
+ throw new Error(
61
+ `attachments[${i}]: either mxcUri or eventId must be provided`,
62
+ );
63
+ if (
64
+ typeof filename !== 'string' ||
65
+ filename.length === 0 ||
66
+ filename.length > MAX_FILENAME
67
+ )
68
+ throw new Error(
69
+ `attachments[${i}].filename is required (≤ ${MAX_FILENAME} chars)`,
70
+ );
71
+ if (typeof mimetype !== 'string' || mimetype.length === 0)
72
+ throw new Error(`attachments[${i}].mimetype is required`);
73
+ if (
74
+ size !== undefined &&
75
+ (typeof size !== 'number' || !Number.isFinite(size) || size < 0)
76
+ )
77
+ throw new Error(`attachments[${i}].size must be a non-negative number`);
78
+ return {
79
+ ...(typeof mxcUri === 'string' ? { mxcUri } : {}),
80
+ ...(typeof eventId === 'string' ? { eventId } : {}),
81
+ filename,
82
+ mimetype,
83
+ ...(typeof size === 'number' ? { size } : {}),
84
+ };
85
+ });
86
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Re-fetch one attachment of a session on demand — the `view_attachment`
3
+ * lane behind payload retention (`retention.ts`). The same routing as the
4
+ * turn pipeline applies: when the current model reads the modality the bytes
5
+ * come back as a native content block, otherwise the helper (`vision` role)
6
+ * model turns them into text; plain text is decoded locally.
7
+ */
8
+ import { classifyAttachment } from './classify';
9
+ import type { NativeAttachment } from './content-blocks';
10
+ import { loadAttachmentBytes, type MatrixMediaSource } from './download';
11
+ import { extractText, type ExtractionProvider } from './extract';
12
+ import { categorizeFile, verifyMagicBytes } from './magic';
13
+ import { routeAttachment } from './route';
14
+ import { sanitizeAttachmentFilename } from './sanitize';
15
+ import type { AttachmentInput } from './types';
16
+ import type { ModelInputCapabilities } from '../core/llm';
17
+ import type { AttachmentMeta } from '../sqlite/serialization';
18
+ import type { Logger } from '../plugin-api/types';
19
+ import { bytesToBase64 } from '../plugins/base64';
20
+
21
+ export type AttachmentView =
22
+ | { kind: 'native'; native: NativeAttachment }
23
+ | { kind: 'text'; text: string };
24
+
25
+ export interface ViewAttachmentDeps {
26
+ source: MatrixMediaSource;
27
+ extraction: ExtractionProvider | null;
28
+ /** Capabilities of the model that will read the result. */
29
+ caps: ModelInputCapabilities;
30
+ roomId?: string;
31
+ fetchImpl?: typeof fetch;
32
+ signal?: AbortSignal;
33
+ logger: Logger;
34
+ }
35
+
36
+ /** What the `view_attachment` tool sees on the host: session-scoped access. */
37
+ export interface AttachmentViewSurface {
38
+ /** Whether any message of the session has had its inline payload offloaded. */
39
+ readonly offloaded: boolean;
40
+ /**
41
+ * Fetch one of the session's attachments again by the reference the
42
+ * placeholder lists (media event id or mxc URI). Rejects references that do
43
+ * not belong to an attachment of this session.
44
+ */
45
+ view(ref: string): Promise<{ meta: AttachmentMeta; view: AttachmentView }>;
46
+ }
47
+
48
+ function toInput(meta: AttachmentMeta): AttachmentInput {
49
+ return {
50
+ filename: meta.filename,
51
+ mimetype: meta.mimetype,
52
+ ...(meta.size !== undefined ? { size: meta.size } : {}),
53
+ ...(meta.mxcUri ? { mxcUri: meta.mxcUri } : {}),
54
+ ...(meta.eventId ? { eventId: meta.eventId } : {}),
55
+ };
56
+ }
57
+
58
+ export async function viewAttachment(
59
+ meta: AttachmentMeta,
60
+ deps: ViewAttachmentDeps,
61
+ ): Promise<AttachmentView> {
62
+ const attachment = toInput(meta);
63
+ const kind = classifyAttachment(attachment);
64
+ const strategy = routeAttachment(kind, deps.caps);
65
+ const { bytes, mimetype } = await loadAttachmentBytes(
66
+ attachment,
67
+ deps.roomId,
68
+ deps.source,
69
+ { fetchImpl: deps.fetchImpl, signal: deps.signal },
70
+ );
71
+ deps.logger.log(
72
+ `[attachments] view "${attachment.filename}" (${mimetype}) kind=${kind} → ${strategy} (${bytes.length} bytes)`,
73
+ );
74
+ if (strategy === 'send-native') {
75
+ const loaded = classifyAttachment({
76
+ mimetype,
77
+ filename: attachment.filename,
78
+ });
79
+ return {
80
+ kind: 'native',
81
+ native: {
82
+ kind: loaded === 'image' ? 'image' : 'file',
83
+ mimeType: mimetype,
84
+ base64: bytesToBase64(bytes),
85
+ filename: attachment.filename,
86
+ },
87
+ };
88
+ }
89
+ const category = categorizeFile(attachment.mimetype);
90
+ if (category === 'unsupported') {
91
+ return {
92
+ kind: 'text',
93
+ text: `[File "${sanitizeAttachmentFilename(attachment.filename)}" (${attachment.mimetype}) is not a supported file type and could not be processed]`,
94
+ };
95
+ }
96
+ verifyMagicBytes(bytes, category, attachment, (m) =>
97
+ deps.logger.warn(`[attachments] ${m}`),
98
+ );
99
+ const { text } = await extractText(
100
+ bytes,
101
+ attachment,
102
+ category,
103
+ deps.extraction,
104
+ { fetchImpl: deps.fetchImpl, signal: deps.signal },
105
+ );
106
+ return { kind: 'text', text };
107
+ }
@@ -0,0 +1,200 @@
1
+ import { z } from 'zod';
2
+ import type { OraclePlugin } from '../plugin-api/oracle-plugin';
3
+ import type { Logger } from '../plugin-api/types';
4
+ import { NOOP_LOGGER } from './utils';
5
+
6
+ /**
7
+ * Tier-0 (core) environment for the Workers runtime. Mirrors
8
+ * `OracleWorkerEnv` in `src/do/contracts.ts`: every key here is a Worker
9
+ * secret or var. Plugin-owned keys are NOT declared here — each plugin's
10
+ * `configSchema` is folded onto this base by {@link composeEnvSchema}.
11
+ *
12
+ * Deliberately absent versus the Node runtime's base schema: `NODE_ENV`,
13
+ * `PORT`, `SQLITE_DATABASE_PATH`, `MATRIX_STORE_PATH`, `SECP_MNEMONIC`,
14
+ * `RPC_URL`, `OPENAI_API_KEY`, `SESSION_TITLE_MODEL`, `ORACLE_SECRETS`,
15
+ * `LIVE_AGENT_AUTH_API_KEY` — filesystem/process concerns that do not exist
16
+ * on Workers or features this runtime does not carry. Optional features
17
+ * (Nebius, LangSmith, BYO credentials, the account-room secrets key) are
18
+ * read straight from `OracleWorkerEnv` by the code that owns them.
19
+ *
20
+ * Unknown keys are stripped by Zod, so the Durable Object namespaces and any
21
+ * other non-string bindings on the Worker `env` object never reach plugins
22
+ * through `ctx.config`.
23
+ */
24
+ export const baseEnvSchema = z.object({
25
+ // --- identity -----------------------------------------------------------
26
+ ORACLE_NAME: z.string().min(1),
27
+ /** UCAN audience — the DID users address invocations/delegations to. */
28
+ ORACLE_DID: z.string().min(1),
29
+ /**
30
+ * On-chain entity DID surfaced to the agent as `identity.entityDid`. Falls
31
+ * back to `ORACLE_DID` when unset (see `createRuntimeCore`).
32
+ */
33
+ ORACLE_ENTITY_DID: z.string().min(1).optional(),
34
+ NETWORK: z.enum(['mainnet', 'testnet', 'devnet']).default('mainnet'),
35
+
36
+ // --- auth ---------------------------------------------------------------
37
+ /** Blocksync GraphQL endpoint used to resolve `did:ixo` verification keys. */
38
+ BLOCKSYNC_GRAPHQL_URL: z.string().min(1),
39
+ /**
40
+ * Max lifetime (seconds) the oracle accepts for a user *auth* invocation.
41
+ * Bounds the replay window server-side regardless of the TTL the client
42
+ * declares. Default 15 minutes.
43
+ */
44
+ UCAN_AUTH_MAX_TTL_SECONDS: z.coerce.number().int().positive().default(900),
45
+
46
+ // --- matrix -------------------------------------------------------------
47
+ MATRIX_BASE_URL: z.string().min(1),
48
+ MATRIX_ORACLE_ADMIN_USER_ID: z.string().min(1),
49
+ /**
50
+ * The bot account's password. The gateway logs its own device in with it,
51
+ * keeps that device's token in its storage and re-logs in if the token is
52
+ * ever rejected; plugins that need their own client (editor, flows) get a
53
+ * second dedicated device from the gateway. No access token is ever
54
+ * configured: a token is a device, and a device shared with any other
55
+ * client splits its encryption state.
56
+ */
57
+ MATRIX_ORACLE_ADMIN_PASSWORD: z.string().min(1),
58
+ /** SSSS recovery passphrase — lets a fresh device restore room keys. */
59
+ MATRIX_RECOVERY_PHRASE: z.string().min(1).optional(),
60
+ /** Matrix server name used when composing per-user room aliases. */
61
+ MATRIX_HOMESERVER_NAME: z.string().min(1).optional(),
62
+
63
+ // --- storage ------------------------------------------------------------
64
+ /** Where the user-owned SQLite file lives; unset = the IXO VFS, `matrix` = legacy room media (local harness). */
65
+ OWNER_STORE: z.enum(['matrix', 'vfs']).optional(),
66
+ /** IXO VFS worker base URL when `OWNER_STORE=vfs` (defaults per NETWORK). */
67
+ VFS_BASE_URL: z.string().url().optional(),
68
+ /** UCAN store worker base URL (defaults per NETWORK). */
69
+ UCAN_STORE_URL: z.string().url().optional(),
70
+
71
+ // --- llm ----------------------------------------------------------------
72
+ OPEN_ROUTER_API_KEY: z.string().min(1),
73
+ /**
74
+ * Default model for new chats — an OpenRouter slug. When unset the runtime
75
+ * uses the catalog default. Lets an operator change the default per
76
+ * deployment without a code change.
77
+ */
78
+ DEFAULT_MODEL: z.string().min(1).optional(),
79
+ /**
80
+ * Multiplier applied to raw provider prices in `GET /models` (the price the
81
+ * user sees). Same default as the Node runtime.
82
+ */
83
+ MODEL_PRICE_MARKUP: z.coerce.number().positive().optional(),
84
+ /**
85
+ * Extended-thinking effort for the main model. Lower = faster time-to-first
86
+ * token, at some cost to hard multi-step reasoning.
87
+ */
88
+ MAIN_REASONING_EFFORT: z.enum(['low', 'medium', 'high']).default('medium'),
89
+
90
+ // --- misc ---------------------------------------------------------------
91
+ LOG_LEVEL: z.string().default('info'),
92
+ CORS_ORIGIN: z.string().default('*'),
93
+ });
94
+
95
+ export type BaseEnv = z.infer<typeof baseEnvSchema>;
96
+
97
+ export interface ComposeEnvSchemaResult {
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ schema: z.ZodObject<any>;
100
+ /**
101
+ * Map of env field name -> the plugin that owns it. Used by
102
+ * {@link validateEnv} to attribute Zod issues to a plugin.
103
+ */
104
+ pluginOwnership: Map<string, string>;
105
+ }
106
+
107
+ export interface ValidateEnvError {
108
+ /** Plugin owning the failing field, or `'core'` if from a base schema. */
109
+ plugin: string;
110
+ /** Dotted field path from Zod's issue (e.g. `MEMORY_MCP_URL`). */
111
+ field: string;
112
+ /** Human-readable message from Zod. */
113
+ message: string;
114
+ }
115
+
116
+ export interface ValidateEnvResult {
117
+ valid: boolean;
118
+ /** Parsed config object — empty when `valid` is false. */
119
+ config: Record<string, unknown>;
120
+ errors: ValidateEnvError[];
121
+ }
122
+
123
+ /**
124
+ * Fold every plugin's `configSchema` into a single Zod object via
125
+ * `.extend()`. The returned `pluginOwnership` map records which plugin
126
+ * contributed each top-level field so {@link validateEnv} can attribute
127
+ * Zod issues by plugin.
128
+ *
129
+ * Conflict policy: later wins. When two plugins declare the same field,
130
+ * the later definition replaces the earlier one and a warning is emitted
131
+ * via the supplied logger naming both plugins.
132
+ */
133
+ export function composeEnvSchema(
134
+ plugins: OraclePlugin[],
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ baseSchema?: z.ZodObject<any>,
137
+ logger: Logger = NOOP_LOGGER,
138
+ ): ComposeEnvSchemaResult {
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ let merged: z.ZodObject<any> = baseSchema ?? z.object({});
141
+ const pluginOwnership = new Map<string, string>();
142
+
143
+ if (baseSchema) {
144
+ for (const key of Object.keys(baseSchema.shape)) {
145
+ pluginOwnership.set(key, 'core');
146
+ }
147
+ }
148
+
149
+ for (const plugin of plugins) {
150
+ if (!plugin.configSchema) continue;
151
+ const shape = plugin.configSchema.shape as Record<string, unknown>;
152
+ for (const key of Object.keys(shape)) {
153
+ const previous = pluginOwnership.get(key);
154
+ if (previous !== undefined && previous !== plugin.name) {
155
+ logger.warn(
156
+ `[boot] env key '${key}' is defined by both '${previous}' and '${plugin.name}'; '${plugin.name}' wins.`,
157
+ );
158
+ }
159
+ pluginOwnership.set(key, plugin.name);
160
+ }
161
+ merged = merged.extend(shape);
162
+ }
163
+
164
+ return { schema: merged, pluginOwnership };
165
+ }
166
+
167
+ /**
168
+ * Validate `env` against the merged schema and produce structured errors
169
+ * naming the owning plugin per failing field.
170
+ *
171
+ * `env` is the Worker's `env` bindings object (or any plain record) — this
172
+ * module never reads `process.env`. On success, `config` holds the parsed
173
+ * object (with any Zod coercions/defaults applied). On failure, `config` is
174
+ * empty and every Zod issue is mapped through `pluginOwnership` to find the
175
+ * owner.
176
+ */
177
+ export function validateEnv(
178
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
+ schema: z.ZodObject<any>,
180
+ env: Record<string, unknown>,
181
+ pluginOwnership: Map<string, string>,
182
+ ): ValidateEnvResult {
183
+ const result = schema.safeParse(env);
184
+ if (result.success) {
185
+ return {
186
+ valid: true,
187
+ config: result.data,
188
+ errors: [],
189
+ };
190
+ }
191
+
192
+ const errors: ValidateEnvError[] = result.error.issues.map((issue) => {
193
+ const topField = issue.path.length > 0 ? String(issue.path[0]) : '<root>';
194
+ const fullField = issue.path.length > 0 ? issue.path.join('.') : '<root>';
195
+ const plugin = pluginOwnership.get(topField) ?? 'unknown';
196
+ return { plugin, field: fullField, message: issue.message };
197
+ });
198
+
199
+ return { valid: false, config: {}, errors };
200
+ }