@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,241 @@
1
+ /**
2
+ * The "model-extract" lane — turn an attachment into text with the helper
3
+ * (vision) model through the provider's OpenAI-compatible chat endpoint, the
4
+ * way the Node `FileProcessingService` does: images are described, audio is
5
+ * transcribed (`input_audio`), video described (`video_url`), documents
6
+ * extracted verbatim (`file` part — the provider parses PDFs server-side).
7
+ * Plain text is read locally and never costs a model call.
8
+ *
9
+ * Reduced vs Node: there is no local PDF/office parser on workerd
10
+ * (`loadFileFromBuffer` is Node-only), so every non-plain-text document goes
11
+ * straight to the helper model's `file` lane instead of "local parse, then
12
+ * model on failure".
13
+ */
14
+ import { bytesToBase64 } from '../plugins/base64';
15
+ import { validateUrlTarget } from './download';
16
+ import type { FileCategory } from './magic';
17
+ import { isPlainTextType } from './magic';
18
+ import { sanitizeAttachmentFilename } from './sanitize';
19
+ import type { AttachmentInput } from './types';
20
+
21
+ /** Provider wiring for the helper model — the platform key, never a BYO one. */
22
+ export interface ExtractionProvider {
23
+ baseURL: string;
24
+ apiKey: string;
25
+ headers: Record<string, string>;
26
+ /** The `vision` role's model id. */
27
+ model: string;
28
+ }
29
+
30
+ export interface ExtractionUsage {
31
+ cost?: number;
32
+ promptTokens?: number;
33
+ completionTokens?: number;
34
+ }
35
+
36
+ export interface ExtractionResult {
37
+ content: string;
38
+ usage?: ExtractionUsage;
39
+ }
40
+
41
+ export interface ExtractOptions {
42
+ fetchImpl?: typeof fetch;
43
+ signal?: AbortSignal;
44
+ timeoutMs?: number;
45
+ }
46
+
47
+ export const MAX_TEXT_LENGTH = 50_000;
48
+ const AI_PROCESS_TIMEOUT_MS = 120_000;
49
+ const MAX_ERROR_BODY_LENGTH = 1024;
50
+
51
+ export const PROMPTS: Record<Exclude<FileCategory, 'unsupported'>, string> = {
52
+ document: 'Extract all text content from this document verbatim.',
53
+ image:
54
+ 'Describe this image in detail. Include all text, numbers, labels, and visual elements.',
55
+ audio: 'Transcribe this audio completely. Include all spoken words.',
56
+ video:
57
+ 'Describe this video in detail. Include actions, text overlays, and spoken content.',
58
+ };
59
+
60
+ export function formatContent(
61
+ label: string,
62
+ filename: string,
63
+ content: string,
64
+ ): string {
65
+ return `[${label} of ${filename}]:\n${content}`;
66
+ }
67
+
68
+ export function truncateText(text: string): string {
69
+ if (text.length <= MAX_TEXT_LENGTH) return text;
70
+ return `${text.slice(0, MAX_TEXT_LENGTH)}\n\n[...truncated]`;
71
+ }
72
+
73
+ export function getAudioFormat(
74
+ mimetype: string,
75
+ ): 'mp3' | 'wav' | 'ogg' | 'flac' | 'webm' | 'mp4' | 'aac' {
76
+ if (mimetype.includes('mp3') || mimetype.includes('mpeg')) return 'mp3';
77
+ if (mimetype.includes('wav')) return 'wav';
78
+ if (mimetype.includes('ogg')) return 'ogg';
79
+ if (mimetype.includes('flac')) return 'flac';
80
+ if (mimetype.includes('webm')) return 'webm';
81
+ if (mimetype.includes('mp4') || mimetype.includes('m4a')) return 'mp4';
82
+ if (mimetype.includes('aac')) return 'aac';
83
+ return 'mp3';
84
+ }
85
+
86
+ interface ChatCompletion {
87
+ choices?: Array<{ message?: { content?: string | null } }>;
88
+ usage?: {
89
+ prompt_tokens?: number;
90
+ completion_tokens?: number;
91
+ total_tokens?: number;
92
+ cost?: number;
93
+ };
94
+ model?: string;
95
+ }
96
+
97
+ async function chatCompletion(
98
+ provider: ExtractionProvider,
99
+ contentParts: Array<Record<string, unknown>>,
100
+ opts: ExtractOptions,
101
+ ): Promise<ExtractionResult> {
102
+ const fetchImpl = opts.fetchImpl ?? fetch;
103
+ const controller = new AbortController();
104
+ const onAbort = (): void => controller.abort();
105
+ opts.signal?.addEventListener('abort', onAbort);
106
+ if (opts.signal?.aborted) controller.abort();
107
+ const timer = setTimeout(onAbort, opts.timeoutMs ?? AI_PROCESS_TIMEOUT_MS);
108
+ try {
109
+ const response = await fetchImpl(
110
+ `${provider.baseURL.replace(/\/+$/, '')}/chat/completions`,
111
+ {
112
+ method: 'POST',
113
+ headers: {
114
+ Authorization: `Bearer ${provider.apiKey}`,
115
+ 'Content-Type': 'application/json',
116
+ ...provider.headers,
117
+ },
118
+ body: JSON.stringify({
119
+ model: provider.model,
120
+ messages: [{ role: 'user', content: contentParts }],
121
+ }),
122
+ signal: controller.signal,
123
+ },
124
+ );
125
+ if (!response.ok) {
126
+ const errorText = await response.text().catch(() => '');
127
+ throw new Error(
128
+ `AI processing failed (${response.status}): ${errorText.slice(0, MAX_ERROR_BODY_LENGTH)}`,
129
+ );
130
+ }
131
+ const raw: unknown = await response.json();
132
+ const result = raw as ChatCompletion;
133
+ return {
134
+ content: result.choices?.[0]?.message?.content ?? '',
135
+ ...(result.usage
136
+ ? {
137
+ usage: {
138
+ cost: result.usage.cost,
139
+ promptTokens: result.usage.prompt_tokens,
140
+ completionTokens: result.usage.completion_tokens,
141
+ },
142
+ }
143
+ : {}),
144
+ };
145
+ } finally {
146
+ clearTimeout(timer);
147
+ opts.signal?.removeEventListener('abort', onAbort);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Hand a public image/video URL to the provider directly (no re-upload of
153
+ * the bytes). The URL is SSRF-validated first.
154
+ */
155
+ export async function aiProcessFromUrl(
156
+ url: string,
157
+ category: 'image' | 'video',
158
+ provider: ExtractionProvider,
159
+ opts: ExtractOptions = {},
160
+ ): Promise<ExtractionResult> {
161
+ validateUrlTarget(url);
162
+ const parts: Array<Record<string, unknown>> = [
163
+ { type: 'text', text: PROMPTS[category] },
164
+ category === 'image'
165
+ ? { type: 'image_url', image_url: { url } }
166
+ : { type: 'video_url', video_url: { url } },
167
+ ];
168
+ return chatCompletion(provider, parts, opts);
169
+ }
170
+
171
+ /** Send the bytes inline (data URI / base64) with the category's prompt. */
172
+ export async function aiProcess(
173
+ bytes: Uint8Array,
174
+ mimetype: string,
175
+ category: Exclude<FileCategory, 'unsupported'>,
176
+ filename: string,
177
+ provider: ExtractionProvider,
178
+ opts: ExtractOptions = {},
179
+ ): Promise<ExtractionResult> {
180
+ const base64 = bytesToBase64(bytes);
181
+ const dataUri = `data:${mimetype};base64,${base64}`;
182
+ const parts: Array<Record<string, unknown>> = [
183
+ { type: 'text', text: PROMPTS[category] },
184
+ ];
185
+ if (category === 'image') {
186
+ parts.push({ type: 'image_url', image_url: { url: dataUri } });
187
+ } else if (category === 'audio') {
188
+ parts.push({
189
+ type: 'input_audio',
190
+ input_audio: { data: base64, format: getAudioFormat(mimetype) },
191
+ });
192
+ } else if (category === 'document') {
193
+ parts.push({ type: 'file', file: { filename, file_data: dataUri } });
194
+ } else {
195
+ parts.push({ type: 'video_url', video_url: { url: dataUri } });
196
+ }
197
+ return chatCompletion(provider, parts, opts);
198
+ }
199
+
200
+ /**
201
+ * Text for one downloaded attachment by category — the Node
202
+ * `processDocument` / `processImage` / `processAudio` / `processVideo`
203
+ * dispatch. Plain text never touches the model.
204
+ */
205
+ export async function extractText(
206
+ bytes: Uint8Array,
207
+ attachment: AttachmentInput,
208
+ category: Exclude<FileCategory, 'unsupported'>,
209
+ provider: ExtractionProvider | null,
210
+ opts: ExtractOptions = {},
211
+ ): Promise<{ text: string; usage?: ExtractionUsage }> {
212
+ const safeName = sanitizeAttachmentFilename(attachment.filename);
213
+ if (category === 'document' && isPlainTextType(attachment.mimetype)) {
214
+ const text = new TextDecoder().decode(bytes);
215
+ return { text: formatContent('Content', safeName, truncateText(text)) };
216
+ }
217
+ if (!provider) {
218
+ throw new Error(
219
+ 'no helper model configured for attachment extraction on this host',
220
+ );
221
+ }
222
+ const label =
223
+ category === 'document'
224
+ ? 'Content'
225
+ : category === 'audio'
226
+ ? 'Transcription'
227
+ : 'Description';
228
+ const { content, usage } = await aiProcess(
229
+ bytes,
230
+ attachment.mimetype,
231
+ category,
232
+ attachment.filename,
233
+ provider,
234
+ opts,
235
+ );
236
+ const body = category === 'document' ? truncateText(content) : content;
237
+ return {
238
+ text: formatContent(label, safeName, body),
239
+ ...(usage ? { usage } : {}),
240
+ };
241
+ }
@@ -0,0 +1,84 @@
1
+ export { classifyAttachment, type AttachmentKind } from './classify';
2
+ export {
3
+ buildUserMessageContent,
4
+ type NativeAttachment,
5
+ } from './content-blocks';
6
+ export {
7
+ ALLOWED_URI_SCHEMES,
8
+ downloadFromUrl,
9
+ loadAttachmentBytes,
10
+ MAX_FILE_SIZE,
11
+ MAX_TOTAL_SIZE,
12
+ validateUrlTarget,
13
+ type DownloadOptions,
14
+ type MatrixMediaSource,
15
+ } from './download';
16
+ export {
17
+ aiProcess,
18
+ aiProcessFromUrl,
19
+ extractText,
20
+ formatContent,
21
+ getAudioFormat,
22
+ MAX_TEXT_LENGTH,
23
+ PROMPTS,
24
+ truncateText,
25
+ type ExtractionProvider,
26
+ type ExtractionResult,
27
+ type ExtractionUsage,
28
+ } from './extract';
29
+ export {
30
+ categorizeFile,
31
+ detectMimeFromMagicBytes,
32
+ isDocumentType,
33
+ isPlainTextType,
34
+ verifyMagicBytes,
35
+ type FileCategory,
36
+ } from './magic';
37
+ export {
38
+ prepareAttachments,
39
+ SANDBOX_TRUNCATE_LIMIT,
40
+ type AttachmentPipelineDeps,
41
+ type ExtractedAttachment,
42
+ type PrepareAttachmentsParams,
43
+ type PreparedAttachments,
44
+ } from './pipeline';
45
+ export { routeAttachment, type AttachmentStrategy } from './route';
46
+ export {
47
+ buildAnalysisMarkdown,
48
+ uploadToSandbox,
49
+ type SandboxUploadConfig,
50
+ type SandboxUploadResult,
51
+ } from './sandbox-archive';
52
+ export {
53
+ SANDBOX_OUTPUT_PREFIX,
54
+ sanitizeAttachmentFilename,
55
+ sanitizeSandboxPath,
56
+ } from './sanitize';
57
+ export {
58
+ MAX_ATTACHMENTS,
59
+ parseAttachmentInputs,
60
+ type AttachmentInput,
61
+ } from './types';
62
+ export {
63
+ applyAttachmentRetention,
64
+ ATTACHMENT_PAYLOAD_TURNS,
65
+ ATTACHMENT_PLACEHOLDER_PREFIX,
66
+ ATTACHMENT_VIEW_SOURCE,
67
+ attachmentMetas,
68
+ attachmentRef,
69
+ buildPlaceholderText,
70
+ hasInlinePayload,
71
+ INLINE_PAYLOAD_NEEDLE,
72
+ isAttachmentPlaceholderText,
73
+ isAttachmentViewMessage,
74
+ isInlinePayloadBlock,
75
+ offloadInlinePayloads,
76
+ VIEW_ATTACHMENT_TOOL_NAME,
77
+ type AttachmentRetentionResult,
78
+ } from './retention';
79
+ export {
80
+ viewAttachment,
81
+ type AttachmentView,
82
+ type AttachmentViewSurface,
83
+ type ViewAttachmentDeps,
84
+ } from './view';
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Content sniffing + the extraction lane's coarse file category — ports of
3
+ * the Node `FileProcessingService` helpers (`detectMimeFromMagicBytes`,
4
+ * `verifyMagicBytes`, `categorizeFile`, `isDocumentType`, `isPlainTextType`).
5
+ *
6
+ * Note the two taxonomies deliberately differ, as on Node: `classify.ts`
7
+ * drives ROUTING (plain text is its own kind), while `FileCategory` drives
8
+ * EXTRACTION (plain text counts as a document that is read locally).
9
+ */
10
+
11
+ export type FileCategory =
12
+ | 'document'
13
+ | 'image'
14
+ | 'audio'
15
+ | 'video'
16
+ | 'unsupported';
17
+
18
+ const MAGIC_BYTES: Array<{ bytes: number[]; offset?: number; mime: string }> = [
19
+ // Images
20
+ { bytes: [0x89, 0x50, 0x4e, 0x47], mime: 'image/png' },
21
+ { bytes: [0xff, 0xd8, 0xff], mime: 'image/jpeg' },
22
+ { bytes: [0x47, 0x49, 0x46, 0x38], mime: 'image/gif' },
23
+ { bytes: [0x52, 0x49, 0x46, 0x46], mime: 'image/webp' }, // RIFF (WebP container)
24
+ { bytes: [0x42, 0x4d], mime: 'image/bmp' },
25
+ { bytes: [0x49, 0x49, 0x2a, 0x00], mime: 'image/tiff' },
26
+ { bytes: [0x4d, 0x4d, 0x00, 0x2a], mime: 'image/tiff' },
27
+ // Documents
28
+ { bytes: [0x25, 0x50, 0x44, 0x46], mime: 'application/pdf' },
29
+ { bytes: [0x50, 0x4b, 0x03, 0x04], mime: 'application/zip' }, // docx/xlsx/…
30
+ { bytes: [0xd0, 0xcf, 0x11, 0xe0], mime: 'application/msword' }, // OLE2
31
+ // Audio
32
+ { bytes: [0x49, 0x44, 0x33], mime: 'audio/mpeg' }, // ID3
33
+ { bytes: [0xff, 0xfb], mime: 'audio/mpeg' },
34
+ { bytes: [0xff, 0xf3], mime: 'audio/mpeg' },
35
+ { bytes: [0x4f, 0x67, 0x67, 0x53], mime: 'audio/ogg' },
36
+ { bytes: [0x66, 0x4c, 0x61, 0x43], mime: 'audio/flac' },
37
+ // Video
38
+ { bytes: [0x1a, 0x45, 0xdf, 0xa3], mime: 'video/webm' }, // EBML (WebM/MKV)
39
+ { bytes: [0x66, 0x74, 0x79, 0x70], offset: 4, mime: 'video/mp4' }, // `ftyp`
40
+ ];
41
+
42
+ const MAGIC_MIME_CATEGORIES: Record<string, FileCategory[]> = {
43
+ 'image/png': ['image'],
44
+ 'image/jpeg': ['image'],
45
+ 'image/gif': ['image'],
46
+ 'image/webp': ['image'],
47
+ 'image/bmp': ['image'],
48
+ 'image/tiff': ['image'],
49
+ 'application/pdf': ['document'],
50
+ 'application/zip': ['document'],
51
+ 'application/msword': ['document'],
52
+ 'audio/mpeg': ['audio'],
53
+ 'audio/ogg': ['audio'],
54
+ 'audio/flac': ['audio'],
55
+ 'video/webm': ['video', 'audio'],
56
+ 'video/mp4': ['video', 'audio'],
57
+ };
58
+
59
+ /** The MIME a file's magic bytes announce, or null when no signature matches. */
60
+ export function detectMimeFromMagicBytes(bytes: Uint8Array): string | null {
61
+ if (bytes.length < 4) return null;
62
+ for (const sig of MAGIC_BYTES) {
63
+ const offset = sig.offset ?? 0;
64
+ if (bytes.length < offset + sig.bytes.length) continue;
65
+ let match = true;
66
+ for (let i = 0; i < sig.bytes.length; i += 1) {
67
+ if (bytes[offset + i] !== sig.bytes[i]) {
68
+ match = false;
69
+ break;
70
+ }
71
+ }
72
+ if (match) return sig.mime;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ export function isPlainTextType(mimetype: string): boolean {
78
+ return (
79
+ mimetype.startsWith('text/') ||
80
+ mimetype === 'application/json' ||
81
+ mimetype === 'application/xml' ||
82
+ mimetype === 'application/rtf'
83
+ );
84
+ }
85
+
86
+ export function isDocumentType(mimetype: string): boolean {
87
+ return (
88
+ mimetype.startsWith('text/') ||
89
+ mimetype === 'application/pdf' ||
90
+ mimetype === 'application/msword' ||
91
+ mimetype === 'application/json' ||
92
+ mimetype === 'application/xml' ||
93
+ mimetype === 'application/rtf' ||
94
+ mimetype.startsWith('application/vnd.openxmlformats-officedocument.') ||
95
+ mimetype === 'application/vnd.ms-excel' ||
96
+ mimetype === 'application/vnd.ms-powerpoint'
97
+ );
98
+ }
99
+
100
+ export function categorizeFile(mimetype: string): FileCategory {
101
+ if (isDocumentType(mimetype)) return 'document';
102
+ if (mimetype.startsWith('image/')) return 'image';
103
+ if (mimetype.startsWith('audio/')) return 'audio';
104
+ if (mimetype.startsWith('video/')) return 'video';
105
+ return 'unsupported';
106
+ }
107
+
108
+ /**
109
+ * Verify that the bytes are consistent with the claimed category. Strict for
110
+ * binary formats; skipped for text-based mimetypes (no reliable signature).
111
+ * Throws on a mismatch (e.g. an executable claiming to be a PNG).
112
+ */
113
+ export function verifyMagicBytes(
114
+ bytes: Uint8Array,
115
+ claimedCategory: FileCategory,
116
+ attachment: { mimetype: string; filename: string },
117
+ warn?: (message: string) => void,
118
+ ): void {
119
+ if (isPlainTextType(attachment.mimetype)) return;
120
+ const detected = detectMimeFromMagicBytes(bytes);
121
+ if (!detected) {
122
+ warn?.(
123
+ `No magic bytes match for ${attachment.filename} (claimed: ${attachment.mimetype})`,
124
+ );
125
+ return;
126
+ }
127
+ const allowed = MAGIC_MIME_CATEGORIES[detected];
128
+ if (!allowed || !allowed.includes(claimedCategory)) {
129
+ throw new Error(
130
+ `File content mismatch: claimed ${attachment.mimetype} but detected ${detected}`,
131
+ );
132
+ }
133
+ }