@ixo/oracle-runtime-workers 0.36.0 → 0.36.1

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.36.0",
3
+ "version": "0.36.1",
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": {
@@ -67,9 +67,9 @@
67
67
  "protobufjs": "^7.6.5",
68
68
  "@ixo/topic-protocol": "1.0.0-rc.8",
69
69
  "survey-core": "2.3.11",
70
- "@ixo/ucan": "2.3.0",
71
70
  "@ixo/common": "1.7.2",
72
- "@ixo/ixo-transaction": "0.2.0"
71
+ "@ixo/ixo-transaction": "0.2.0",
72
+ "@ixo/ucan": "2.3.0"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@cloudflare/vitest-pool-workers": "^0.22.0",
@@ -443,9 +443,7 @@ export async function prepareAttachments(
443
443
  }> = [];
444
444
  for (const attachment of params.attachments) {
445
445
  const kind = classifyAttachment(attachment);
446
- const strategy = routeAttachment(kind, params.caps, {
447
- channelFile: Boolean(attachment.channelRef),
448
- });
446
+ const strategy = routeAttachment(kind, params.caps);
449
447
  deps.logger.log(
450
448
  `[attachments] "${attachment.filename}" (${attachment.mimetype}) kind=${kind} model=${params.model} → ${strategy}`,
451
449
  );
@@ -18,22 +18,11 @@ export type AttachmentStrategy =
18
18
  /**
19
19
  * Decide, for one attachment, how to get it to the selected model — the single
20
20
  * cost-aware, capability-aware routing rule. Pure and total.
21
- *
22
- * `channelFile`: a file from a chat channel (WhatsApp) never goes as raw
23
- * bytes. Its message is replayed to the model on every later turn and kept in
24
- * the user's checkpoint, so a native PDF or photo there is base64 the size of
25
- * the file, counted as tokens at chars/4 and held several times over in the
26
- * object's 128 MB isolate (a 600 KB PDF reset the isolate on every attempt).
27
- * The helper model turns it into text instead; tools still read the file
28
- * itself through `ctx.session.channel.attachments`.
29
21
  */
30
22
  export function routeAttachment(
31
23
  kind: AttachmentKind,
32
24
  caps: ModelInputCapabilities,
33
- options: { channelFile?: boolean } = {},
34
25
  ): AttachmentStrategy {
35
- if (options.channelFile)
36
- return kind === 'text' ? 'parse-local' : 'model-extract';
37
26
  switch (kind) {
38
27
  case 'text':
39
28
  return 'parse-local';
@@ -129,11 +129,7 @@ export async function viewAttachment(
129
129
  ): Promise<AttachmentView> {
130
130
  const attachment = toInput(meta);
131
131
  const kind = classifyAttachment(attachment);
132
- // A channel file comes back as text too: re-attaching its bytes would put
133
- // them in the history again (see `routeAttachment`).
134
- const strategy = routeAttachment(kind, deps.caps, {
135
- channelFile: Boolean(attachment.channelRef),
136
- });
132
+ const strategy = routeAttachment(kind, deps.caps);
137
133
  if (strategy === 'send-native') {
138
134
  const { bytes, mimetype, sniffed } = await loadAttachmentBytes(
139
135
  attachment,
@@ -1,3 +1,4 @@
1
+ import { SENDABLE_FILE_TYPES } from '../plugins/sandbox/send-file';
1
2
  import type { DeliveryProfile } from './types';
2
3
 
3
4
  /**
@@ -41,7 +42,7 @@ export function renderSurfaceSection(
41
42
  `- Ordinary answers stay in chat, including explanations, comparisons, short plans and step-by-step instructions. No headings or horizontal rules. Lists up to ${limits.maxListItems} items, code up to ${limits.maxCodeLines} lines and **bold** for the key fact are fine in chat. Write a small comparison as one short line per item, not a table.`,
42
43
  ...(files
43
44
  ? [
44
- `- Deliver every file meant for the user (a Word document, PDF, spreadsheet, slide deck, image, CSV or anything else) with \`send_file\`: ${deliversAttachments(surface) ? 'the user receives the file itself here, as an attachment' : 'a link to the file is posted here for you'}. When the user asks for a file in a particular format, build it in the sandbox and send it this way. Never paste a download link, and do not describe how to open or download it. Do not send helper scripts or intermediate files. Call it once per file, after the sandbox call that saved the file has finished.`,
45
+ `- Deliver every file meant for the user with \`send_file\` (${SENDABLE_FILE_TYPES} only): ${deliversAttachments(surface) ? 'the user receives the file itself here, as an attachment' : 'a link to the file is posted here for you'}. When the user asks for a file in a particular format, build it yourself in the sandbox and send it this way. Do not send a file you downloaded from the internet unless the user asked for that exact file. Never paste a download link, and do not describe how to open or download it. Do not send helper scripts or intermediate files. Call it once per file, after the sandbox call that saved the file has finished.`,
45
46
  ]
46
47
  : []),
47
48
  artifacts
@@ -93,7 +93,7 @@ export interface ReplyFile {
93
93
  url: string;
94
94
  /** Sanitised, from the artefact path, with its extension. */
95
95
  filename: string;
96
- /** From the extension's allow-list; `application/octet-stream` when unknown. */
96
+ /** From the extension's allow-list: `send_file` refuses any other type. */
97
97
  mimetype: string;
98
98
  /** Size, when the store reported it. */
99
99
  bytes?: number;
@@ -39,17 +39,18 @@ export const SendFileArgs = z.object({
39
39
  .describe('A short line shown with the file, at most 1,024 characters.'),
40
40
  });
41
41
 
42
- /** The types a gateway treats specially; anything else is opaque to it. */
42
+ /**
43
+ * What `send_file` sends, by extension, with its MIME type: the types a chat
44
+ * gateway delivers. A gateway refuses any other (the legacy `.doc`, `.xls`
45
+ * and `.ppt` included), so the tool refuses it before signing anything.
46
+ */
43
47
  const FILE_MIME: ReadonlyMap<string, string> = new Map([
44
48
  ['pdf', 'application/pdf'],
45
- ['doc', 'application/msword'],
46
49
  [
47
50
  'docx',
48
51
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
49
52
  ],
50
- ['xls', 'application/vnd.ms-excel'],
51
53
  ['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
52
- ['ppt', 'application/vnd.ms-powerpoint'],
53
54
  [
54
55
  'pptx',
55
56
  'application/vnd.openxmlformats-officedocument.presentationml.presentation',
@@ -63,11 +64,15 @@ const FILE_MIME: ReadonlyMap<string, string> = new Map([
63
64
  ['csv', 'text/csv'],
64
65
  ]);
65
66
 
66
- /** The MIME type of a filename, from the allow-list of extensions. */
67
- export function fileMimetype(filename: string): string {
67
+ /** The types in `FILE_MIME`, as the tool description and the prompt name them. */
68
+ export const SENDABLE_FILE_TYPES =
69
+ 'PDF, Word (.docx), Excel (.xlsx), PowerPoint (.pptx), text, CSV, Markdown, PNG or JPEG';
70
+
71
+ /** The MIME type of a filename `send_file` may send; `undefined` for any other. */
72
+ export function fileMimetype(filename: string): string | undefined {
68
73
  const dot = filename.lastIndexOf('.');
69
74
  const extension = dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
70
- return FILE_MIME.get(extension) ?? 'application/octet-stream';
75
+ return FILE_MIME.get(extension);
71
76
  }
72
77
 
73
78
  /**
@@ -191,6 +196,10 @@ function notFoundError(path: string): string {
191
196
  return `No stored file at \`${path}\`. A file is stored once the sandbox call that wrote it under \`/workspace/data/output/\` has finished. Check the path with \`artifact_list\`, or write the file first, then call \`send_file\` again.`;
192
197
  }
193
198
 
199
+ function unsupportedTypeError(path: string): string {
200
+ return `Cannot send \`${path}\`. This chat can only receive ${SENDABLE_FILE_TYPES} files. Convert the file, or describe its contents instead.`;
201
+ }
202
+
194
203
  function failedError(path: string): string {
195
204
  return `Could not prepare \`${path}\` for sending. Try once more; if it fails again, tell the user the file could not be sent. Do not offer a link.`;
196
205
  }
@@ -209,6 +218,11 @@ async function signFile(
209
218
  logger.warn(`[send_file] ${args.path}: ${kind}`);
210
219
  return { error };
211
220
  };
221
+ // Checked before signing: a file of another type is never signed.
222
+ const filename = deliveredFilename(args.path);
223
+ const mimetype = fileMimetype(filename);
224
+ if (!mimetype)
225
+ return failed('unsupported type', unsupportedTypeError(args.path));
212
226
  let signed: Record<string, unknown> | undefined;
213
227
  try {
214
228
  signed = resultObject(await presign.invoke({ path: args.path }));
@@ -227,14 +241,13 @@ async function signFile(
227
241
  const url = httpsUrl(signed?.downloadUrl) ?? httpsUrl(signed?.url);
228
242
  if (!url) return failed('no usable link returned', failedError(args.path));
229
243
 
230
- const filename = deliveredFilename(args.path);
231
244
  const { expiresIn, path, size } = signed ?? {};
232
245
  return {
233
246
  path: typeof path === 'string' && path ? path : args.path,
234
247
  file: {
235
248
  url,
236
249
  filename,
237
- mimetype: fileMimetype(filename),
250
+ mimetype,
238
251
  ...(typeof size === 'number' ? { bytes: size } : {}),
239
252
  ...(args.caption ? { caption: args.caption } : {}),
240
253
  ...(typeof expiresIn === 'number'
@@ -294,7 +307,7 @@ export function buildSendFileTool(
294
307
  },
295
308
  {
296
309
  name: SEND_FILE_TOOL,
297
- description: `Send the user a file ${attachments ? 'as an attachment in this chat' : 'as a link posted in this room'}. Use it for every file meant for the user: a Word document, PDF, spreadsheet, slide deck, image, CSV or anything else. Not for helper scripts or intermediate files. \`path\` is the stored path, for example \`/output/report.docx\`: save the file under \`/workspace/data/output/\` and let that sandbox call finish first. \`caption\` is an optional short line shown with the file. ${attachments ? 'The user receives the file itself' : 'The link is posted for you'}, so never paste a download link and do not explain how to open it. For several files, call it once per file, in the order they should arrive. If it returns an error, follow it, or tell the user the file could not be sent.`,
310
+ description: `Send the user a file ${attachments ? 'as an attachment in this chat' : 'as a link posted in this room'}. Use it for every file meant for the user; it sends ${SENDABLE_FILE_TYPES} files only. Build the file yourself in the sandbox: do not send a file you downloaded from the internet unless the user asked for that exact file. Not for helper scripts or intermediate files. \`path\` is the stored path, for example \`/output/report.docx\`: save the file under \`/workspace/data/output/\` and let that sandbox call finish first. \`caption\` is an optional short line shown with the file. ${attachments ? 'The user receives the file itself' : 'The link is posted for you'}, so never paste a download link and do not explain how to open it. For several files, call it once per file, in the order they should arrive. If it returns an error, follow it, or tell the user the file could not be sent.`,
298
311
  schema: SendFileArgs,
299
312
  effect: 'read',
300
313
  },