@tangle-network/agent-app 0.43.50 → 0.43.52

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.
@@ -1,6 +1,17 @@
1
1
  import {
2
- createSandboxFileIndexRoute
3
- } from "../chunk-LCNY3DCM.js";
2
+ ALLOWED_ATTACHMENT_SNIFFED_MIMES,
3
+ ATTACHMENT_ACCEPT,
4
+ ATTACHMENT_MAX_COUNT,
5
+ MAX_ATTACHMENT_TOTAL_BYTES,
6
+ MAX_BINARY_ATTACHMENT_BYTES,
7
+ MAX_TEXT_ATTACHMENT_BYTES,
8
+ attachmentSizeErrorMessage,
9
+ attachmentTotalSizeErrorMessage,
10
+ checkAttachmentType,
11
+ createSandboxFileIndexRoute,
12
+ sanitizeAttachmentFileName,
13
+ sniffBinary
14
+ } from "../chunk-3EKOSBYL.js";
4
15
  import {
5
16
  DEFAULT_STALE_TURN_LOCK_GRACE_MS,
6
17
  DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
@@ -759,6 +770,59 @@ function createSandboxChatProducer(options) {
759
770
  };
760
771
  }
761
772
 
773
+ // src/chat-routes/detached-turn.ts
774
+ var TERMINAL_ERROR_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);
775
+ function errorMessageOf(ev) {
776
+ const rec = ev;
777
+ const raw = rec?.data?.message ?? rec?.data?.reason ?? rec?.message;
778
+ return typeof raw === "string" && raw ? raw : "run failed";
779
+ }
780
+ function hasUsage(usage) {
781
+ return typeof usage.inputTokens === "number" && usage.inputTokens > 0;
782
+ }
783
+ async function runDetachedTurn(opts) {
784
+ const { store, turnId, scopeId } = opts;
785
+ const prior = await store.getStatus(turnId).catch(() => null);
786
+ if (prior === "complete") {
787
+ const final = opts.completedResult ? await opts.completedResult().catch(() => null) : null;
788
+ return { state: "completed", text: final?.text ?? "", usage: final?.usage ?? {}, cached: true };
789
+ }
790
+ const tap = createBufferedTurnTap({
791
+ store,
792
+ turnId,
793
+ scopeId,
794
+ coalesce: opts.coalesce ?? coalesceDeltas
795
+ });
796
+ await tap.onEvent({ type: "turn", turnId });
797
+ const producer = createSandboxChatProducer({
798
+ events: opts.events,
799
+ model: opts.model,
800
+ log: opts.log
801
+ });
802
+ let runError;
803
+ try {
804
+ for await (const ev of producer.stream) {
805
+ const type = ev.type;
806
+ if (typeof type === "string" && TERMINAL_ERROR_TYPES.has(type)) runError = errorMessageOf(ev);
807
+ await tap.onEvent(ev);
808
+ }
809
+ await tap.done(runError ? "error" : "complete");
810
+ } catch (err) {
811
+ await tap.done("error").catch(() => {
812
+ });
813
+ throw err;
814
+ }
815
+ const text = producer.finalText?.() ?? "";
816
+ let usage = producer.usage?.() ?? {};
817
+ if (!runError && !hasUsage(usage)) {
818
+ const final = opts.completedResult ? await opts.completedResult().catch(() => null) : null;
819
+ if (final?.usage) usage = { ...usage, ...final.usage };
820
+ if (!text && final?.text) return { state: "completed", text: final.text, usage, cached: false };
821
+ }
822
+ if (runError) return { state: "failed", text, usage, error: runError, cached: false };
823
+ return { state: "completed", text, usage, cached: false };
824
+ }
825
+
762
826
  // src/chat-routes/durable-projection.ts
763
827
  function errorMessage(error) {
764
828
  return error instanceof Error ? error.message : String(error);
@@ -896,12 +960,7 @@ function createUploadRoute(options) {
896
960
  }
897
961
 
898
962
  // src/chat-routes/resolve-attachments.ts
899
- var ATTACHMENT_MAX_COUNT = 10;
900
- var MAX_ATTACHMENT_TOTAL_BYTES = 25 * 1024 * 1024;
901
963
  var MAX_ATTACHMENT_NAME_LENGTH = 256;
902
- function attachmentTotalSizeErrorMessage(totalBytes, limitBytes) {
903
- return `Attachments total ${formatBytes(totalBytes)}; each message is limited to ${formatBytes(limitBytes)}`;
904
- }
905
964
  function isAttachmentKind(value) {
906
965
  return value === "image" || value === "file";
907
966
  }
@@ -995,164 +1054,6 @@ async function resolveChatAttachments(value, options) {
995
1054
  return { succeeded: true, value: inputs.map(attachmentInputToPart) };
996
1055
  }
997
1056
 
998
- // src/chat-routes/dispatch-parts.ts
999
- function byteLen(value) {
1000
- return new TextEncoder().encode(value).length;
1001
- }
1002
- async function readSandboxMention(box, absolutePath, options) {
1003
- const stat = await statSandboxFileSize(box, absolutePath);
1004
- if (!stat.succeeded) {
1005
- return { succeeded: false, error: `mentioned sandbox file missing or unreadable: ${absolutePath} \u2014 ${stat.error}` };
1006
- }
1007
- if (!options.readBytes) return { succeeded: true, value: { size: stat.value } };
1008
- const read = await readSandboxBinaryBytes(box, absolutePath, stat.value);
1009
- if (!read.succeeded) {
1010
- return { succeeded: false, error: `mentioned sandbox file read failed: ${absolutePath} \u2014 ${read.error}` };
1011
- }
1012
- return { succeeded: true, value: { size: stat.value, base64: bytesToBase64(read.value.bytes) } };
1013
- }
1014
- function violatesUrlPathXor(part) {
1015
- if (part.type === "text") return false;
1016
- const hasUrl = typeof part.url === "string" && part.url.startsWith("data:");
1017
- const hasPath = typeof part.path === "string" && part.path.startsWith("/");
1018
- return hasUrl === hasPath;
1019
- }
1020
- function readResultToBase64(read) {
1021
- if (typeof read.base64 === "string") return read.base64;
1022
- if (read.bytes) return bytesToBase64(read.bytes);
1023
- return void 0;
1024
- }
1025
- async function buildDispatchParts(input) {
1026
- const readMention = input.readSandboxMention ?? readSandboxMention;
1027
- const resolveMentionPath = input.resolveMentionPath ?? input.resolveAttachmentPath;
1028
- const forcePath = input.forcePath ?? false;
1029
- const mentions = input.mentions ?? [];
1030
- const requestMaxBytes = input.requestMaxBytes ?? DISPATCH_REQUEST_MAX_BYTES;
1031
- const structuralReserveBytes = input.structuralReserveBytes ?? DISPATCH_STRUCTURAL_RESERVE_BYTES;
1032
- const maxParts = input.maxParts ?? DISPATCH_MAX_PARTS;
1033
- const parts = [{ type: "text", text: input.text }];
1034
- const emittedAbsPaths = /* @__PURE__ */ new Set();
1035
- const flattenedForSizing = flattenHistory(input.text, input.history);
1036
- const inlineBudget = requestMaxBytes - base64WireLen(byteLen(flattenedForSizing)) - byteLen(JSON.stringify(input.systemPrompt)) - input.profileWireBytes - structuralReserveBytes;
1037
- let runningInline = 0;
1038
- for (const attachment of input.attachments) {
1039
- if (!attachment.path) {
1040
- return { succeeded: false, error: `attachment path must be non-empty: ${attachment.name}` };
1041
- }
1042
- let read;
1043
- try {
1044
- read = await input.readAttachment(input.scopeId, attachment.path);
1045
- } catch (err) {
1046
- return {
1047
- succeeded: false,
1048
- error: `attachment store read failed: ${attachment.path} \u2014 ${err instanceof Error ? err.message : String(err)}`
1049
- };
1050
- }
1051
- if (!read.ok) return { succeeded: false, error: read.reason };
1052
- const base64 = readResultToBase64(read);
1053
- if (base64 === void 0) {
1054
- return { succeeded: false, error: `attachment store read produced no content: ${attachment.path}` };
1055
- }
1056
- const mediaType = attachment.mediaType ?? read.mediaType;
1057
- if (attachment.type === "image" && !mediaType) {
1058
- return { succeeded: false, error: `attachment is missing a mediaType required for an image data URI: ${attachment.path}` };
1059
- }
1060
- const absPath = input.resolveAttachmentPath(attachment.path);
1061
- emittedAbsPaths.add(absPath);
1062
- if (attachment.type === "image") {
1063
- const inlinePart2 = {
1064
- type: "image",
1065
- filename: attachment.name,
1066
- mediaType,
1067
- url: `data:${mediaType};base64,${base64}`
1068
- };
1069
- const cost2 = byteLen(JSON.stringify(inlinePart2));
1070
- if (!forcePath && runningInline + cost2 <= inlineBudget) {
1071
- parts.push(inlinePart2);
1072
- runningInline += cost2;
1073
- } else {
1074
- parts.push({ type: "image", filename: attachment.name, mediaType, path: absPath });
1075
- }
1076
- continue;
1077
- }
1078
- const fileMediaType = mediaType ?? "application/octet-stream";
1079
- const inlinePart = {
1080
- type: "file",
1081
- filename: attachment.name,
1082
- mediaType: fileMediaType,
1083
- url: `data:${fileMediaType};base64,${base64}`
1084
- };
1085
- const cost = byteLen(JSON.stringify(inlinePart));
1086
- if (!forcePath && runningInline + cost <= inlineBudget) {
1087
- parts.push(inlinePart);
1088
- runningInline += cost;
1089
- } else {
1090
- parts.push({ type: "file", path: absPath });
1091
- }
1092
- }
1093
- if (mentions.length > 0 && !input.box) {
1094
- return { succeeded: false, error: "internal error: sandbox mentions require a box to read from" };
1095
- }
1096
- for (const mention of mentions) {
1097
- if (!mention.path) {
1098
- return { succeeded: false, error: `mention path must be non-empty: ${mention.name}` };
1099
- }
1100
- const absPath = resolveMentionPath(mention.path);
1101
- if (emittedAbsPaths.has(absPath)) continue;
1102
- emittedAbsPaths.add(absPath);
1103
- const isImage = mention.mentionKind === "image";
1104
- const mediaType = isImage ? mediaTypeForMentionPath(mention.path) : void 0;
1105
- let stat;
1106
- try {
1107
- stat = await readMention(input.box, absPath, { readBytes: false });
1108
- } catch (err) {
1109
- return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
1110
- }
1111
- if (!stat.succeeded) return { succeeded: false, error: stat.error };
1112
- const projectedInlineCost = base64WireLen(stat.value.size) + byteLen(JSON.stringify({ type: "image", filename: mention.name, mediaType: mediaType ?? "", url: "" }));
1113
- if (isImage && mediaType && !forcePath && runningInline + projectedInlineCost <= inlineBudget) {
1114
- let read;
1115
- try {
1116
- read = await readMention(input.box, absPath, { readBytes: true });
1117
- } catch (err) {
1118
- return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
1119
- }
1120
- if (!read.succeeded) return { succeeded: false, error: read.error };
1121
- if (!read.value.base64) return { succeeded: false, error: `mentioned image produced no bytes: ${absPath}` };
1122
- const inlinePart = {
1123
- type: "image",
1124
- filename: mention.name,
1125
- mediaType,
1126
- url: `data:${mediaType};base64,${read.value.base64}`
1127
- };
1128
- const cost = byteLen(JSON.stringify(inlinePart));
1129
- if (runningInline + cost <= inlineBudget) {
1130
- parts.push(inlinePart);
1131
- runningInline += cost;
1132
- continue;
1133
- }
1134
- }
1135
- parts.push(
1136
- isImage && mediaType ? { type: "image", filename: mention.name, mediaType, path: absPath } : { type: "file", path: absPath }
1137
- );
1138
- }
1139
- for (const part of parts) {
1140
- if (violatesUrlPathXor(part)) {
1141
- return { succeeded: false, error: "internal error: emitted media part violates the url/path exclusivity invariant" };
1142
- }
1143
- }
1144
- const textPartSize = base64WireLen(byteLen(flattenedForSizing));
1145
- const mediaPartsSize = parts.slice(1).reduce((total, part) => total + byteLen(JSON.stringify(part)), 0);
1146
- const systemPromptSize = byteLen(JSON.stringify(input.systemPrompt));
1147
- if (textPartSize + mediaPartsSize + systemPromptSize + input.profileWireBytes + structuralReserveBytes > requestMaxBytes) {
1148
- return { succeeded: false, error: "dispatch parts exceed the sandbox proxy request cap even after path demotion" };
1149
- }
1150
- if (parts.length > maxParts) {
1151
- return { succeeded: false, error: `dispatch parts exceed the sidecar per-request cap of ${maxParts}` };
1152
- }
1153
- return { succeeded: true, value: parts };
1154
- }
1155
-
1156
1057
  // src/chat-routes/promote-file-part.ts
1157
1058
  var PROMOTE_MAX_FILE_BYTES = 10 * 1024 * 1024;
1158
1059
  var EXT_TO_MIME = {
@@ -1192,10 +1093,6 @@ function sniffMimeFromName(filename) {
1192
1093
  if (!ext) return "text/plain";
1193
1094
  return EXT_TO_MIME[ext] ?? "text/plain";
1194
1095
  }
1195
- function sanitizeAttachmentFileName(name) {
1196
- const sanitized = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
1197
- return sanitized || "file";
1198
- }
1199
1096
  function base64ToBytes(base64) {
1200
1097
  const binary = atob(base64);
1201
1098
  const bytes = new Uint8Array(binary.length);
@@ -1328,7 +1225,295 @@ async function promoteAgentFilePart(options) {
1328
1225
  }
1329
1226
  };
1330
1227
  }
1228
+
1229
+ // src/chat-routes/attachment-upload.ts
1230
+ function attachmentUploadError(status, code, message, path) {
1231
+ return Response.json(
1232
+ { error: path === void 0 ? { code, message } : { code, message, path } },
1233
+ { status }
1234
+ );
1235
+ }
1236
+ function createAttachmentUploadRoute(options) {
1237
+ const maxCount = options.limits?.maxCount ?? ATTACHMENT_MAX_COUNT;
1238
+ const maxBinaryBytes = options.limits?.maxBinaryBytes ?? MAX_BINARY_ATTACHMENT_BYTES;
1239
+ const maxTextBytes = options.limits?.maxTextBytes ?? MAX_TEXT_ATTACHMENT_BYTES;
1240
+ const maxTotalBytes = options.limits?.maxTotalBytes ?? MAX_ATTACHMENT_TOTAL_BYTES;
1241
+ const allowedKinds = options.allowedKinds ?? ["image", "file"];
1242
+ const allowedSniffedMimes = options.allowedSniffedMimes ?? ALLOWED_ATTACHMENT_SNIFFED_MIMES;
1243
+ const pathFor = options.pathFor ?? ((name) => name);
1244
+ const validatePath = options.validatePath ?? defaultValidateAttachmentPath;
1245
+ const sniffMime = options.sniffMime ?? sniffMimeFromName;
1246
+ return async function attachmentUpload(request) {
1247
+ const auth = await options.authorize({ request });
1248
+ if (!auth.ok) return auth.response;
1249
+ const write = auth.writeAttachment ?? options.writeAttachment;
1250
+ let form;
1251
+ try {
1252
+ form = await request.formData();
1253
+ } catch {
1254
+ return attachmentUploadError(400, "invalid_upload", "Expected a multipart/form-data body with file fields");
1255
+ }
1256
+ const files = [];
1257
+ form.forEach((value) => {
1258
+ if (value instanceof File) files.push(value);
1259
+ });
1260
+ if (files.length === 0) {
1261
+ return attachmentUploadError(400, "invalid_upload", "No files in the upload body");
1262
+ }
1263
+ if (files.length > maxCount) {
1264
+ return attachmentUploadError(
1265
+ 400,
1266
+ "attachment_count_exceeded",
1267
+ `Too many files \u2014 the ${maxCount}-file limit was exceeded`
1268
+ );
1269
+ }
1270
+ const advisoryTotal = files.reduce((sum, file) => sum + file.size, 0);
1271
+ if (advisoryTotal > maxTotalBytes) {
1272
+ return attachmentUploadError(
1273
+ 413,
1274
+ "attachments_total_too_large",
1275
+ attachmentTotalSizeErrorMessage(advisoryTotal, maxTotalBytes)
1276
+ );
1277
+ }
1278
+ const prepared = [];
1279
+ const seenPaths = /* @__PURE__ */ new Set();
1280
+ let totalBytes = 0;
1281
+ for (const file of files) {
1282
+ const bytes = new Uint8Array(await file.arrayBuffer());
1283
+ const sniff = sniffBinary(bytes);
1284
+ const name = sanitizeAttachmentFileName(file.name);
1285
+ const typeCheck = checkAttachmentType(name, sniff, allowedSniffedMimes);
1286
+ if (!typeCheck.succeeded) {
1287
+ return attachmentUploadError(
1288
+ typeCheck.code === "attachment_type_mismatch" ? 400 : 415,
1289
+ typeCheck.code,
1290
+ typeCheck.message
1291
+ );
1292
+ }
1293
+ const kind = attachmentKindForMime(sniff.mime ?? "");
1294
+ if (!allowedKinds.includes(kind)) {
1295
+ return attachmentUploadError(
1296
+ 415,
1297
+ "attachment_kind_not_allowed",
1298
+ `${name} is a "${kind}" attachment, which this upload route does not accept`
1299
+ );
1300
+ }
1301
+ const limit = sniff.binary ? maxBinaryBytes : maxTextBytes;
1302
+ if (bytes.length > limit) {
1303
+ return attachmentUploadError(
1304
+ 413,
1305
+ "attachment_too_large",
1306
+ attachmentSizeErrorMessage(name, bytes.length, limit)
1307
+ );
1308
+ }
1309
+ const path = pathFor(name);
1310
+ const pathCheck = validatePath(path);
1311
+ if (!pathCheck.succeeded) {
1312
+ return attachmentUploadError(400, "invalid_attachment_path", pathCheck.error, path);
1313
+ }
1314
+ if (seenPaths.has(path)) {
1315
+ return attachmentUploadError(
1316
+ 400,
1317
+ "attachment_duplicate_path",
1318
+ `attachments must not repeat a path within one upload: ${path}`,
1319
+ path
1320
+ );
1321
+ }
1322
+ seenPaths.add(path);
1323
+ totalBytes += bytes.length;
1324
+ if (totalBytes > maxTotalBytes) {
1325
+ return attachmentUploadError(
1326
+ 413,
1327
+ "attachments_total_too_large",
1328
+ attachmentTotalSizeErrorMessage(totalBytes, maxTotalBytes)
1329
+ );
1330
+ }
1331
+ const mediaType = sniff.mime ?? sniffMime(name);
1332
+ prepared.push({ path, name, bytes, originalName: file.name, size: bytes.length, mediaType, kind });
1333
+ }
1334
+ const uploaded = [];
1335
+ for (const input of prepared) {
1336
+ const written = await write(auth.scopeId, input.path, input.bytes, {
1337
+ mediaType: input.mediaType,
1338
+ name: input.name,
1339
+ originalName: input.originalName,
1340
+ size: input.size
1341
+ });
1342
+ if (!written.ok) {
1343
+ return attachmentUploadError(413, "attachment_write_failed", written.reason, input.path);
1344
+ }
1345
+ uploaded.push({
1346
+ path: input.path,
1347
+ name: input.name,
1348
+ size: input.size,
1349
+ mediaType: input.mediaType,
1350
+ kind: input.kind
1351
+ });
1352
+ }
1353
+ return Response.json({ files: uploaded });
1354
+ };
1355
+ }
1356
+
1357
+ // src/chat-routes/dispatch-parts.ts
1358
+ function byteLen(value) {
1359
+ return new TextEncoder().encode(value).length;
1360
+ }
1361
+ async function readSandboxMention(box, absolutePath, options) {
1362
+ const stat = await statSandboxFileSize(box, absolutePath);
1363
+ if (!stat.succeeded) {
1364
+ return { succeeded: false, error: `mentioned sandbox file missing or unreadable: ${absolutePath} \u2014 ${stat.error}` };
1365
+ }
1366
+ if (!options.readBytes) return { succeeded: true, value: { size: stat.value } };
1367
+ const read = await readSandboxBinaryBytes(box, absolutePath, stat.value);
1368
+ if (!read.succeeded) {
1369
+ return { succeeded: false, error: `mentioned sandbox file read failed: ${absolutePath} \u2014 ${read.error}` };
1370
+ }
1371
+ return { succeeded: true, value: { size: stat.value, base64: bytesToBase64(read.value.bytes) } };
1372
+ }
1373
+ function violatesUrlPathXor(part) {
1374
+ if (part.type === "text") return false;
1375
+ const hasUrl = typeof part.url === "string" && part.url.startsWith("data:");
1376
+ const hasPath = typeof part.path === "string" && part.path.startsWith("/");
1377
+ return hasUrl === hasPath;
1378
+ }
1379
+ function readResultToBase64(read) {
1380
+ if (typeof read.base64 === "string") return read.base64;
1381
+ if (read.bytes) return bytesToBase64(read.bytes);
1382
+ return void 0;
1383
+ }
1384
+ async function buildDispatchParts(input) {
1385
+ const readMention = input.readSandboxMention ?? readSandboxMention;
1386
+ const resolveMentionPath = input.resolveMentionPath ?? input.resolveAttachmentPath;
1387
+ const forcePath = input.forcePath ?? false;
1388
+ const mentions = input.mentions ?? [];
1389
+ const requestMaxBytes = input.requestMaxBytes ?? DISPATCH_REQUEST_MAX_BYTES;
1390
+ const structuralReserveBytes = input.structuralReserveBytes ?? DISPATCH_STRUCTURAL_RESERVE_BYTES;
1391
+ const maxParts = input.maxParts ?? DISPATCH_MAX_PARTS;
1392
+ const parts = [{ type: "text", text: input.text }];
1393
+ const emittedAbsPaths = /* @__PURE__ */ new Set();
1394
+ const flattenedForSizing = flattenHistory(input.text, input.history);
1395
+ const inlineBudget = requestMaxBytes - base64WireLen(byteLen(flattenedForSizing)) - byteLen(JSON.stringify(input.systemPrompt)) - input.profileWireBytes - structuralReserveBytes;
1396
+ let runningInline = 0;
1397
+ for (const attachment of input.attachments) {
1398
+ if (!attachment.path) {
1399
+ return { succeeded: false, error: `attachment path must be non-empty: ${attachment.name}` };
1400
+ }
1401
+ let read;
1402
+ try {
1403
+ read = await input.readAttachment(input.scopeId, attachment.path);
1404
+ } catch (err) {
1405
+ return {
1406
+ succeeded: false,
1407
+ error: `attachment store read failed: ${attachment.path} \u2014 ${err instanceof Error ? err.message : String(err)}`
1408
+ };
1409
+ }
1410
+ if (!read.ok) return { succeeded: false, error: read.reason };
1411
+ const base64 = readResultToBase64(read);
1412
+ if (base64 === void 0) {
1413
+ return { succeeded: false, error: `attachment store read produced no content: ${attachment.path}` };
1414
+ }
1415
+ const mediaType = attachment.mediaType ?? read.mediaType;
1416
+ if (attachment.type === "image" && !mediaType) {
1417
+ return { succeeded: false, error: `attachment is missing a mediaType required for an image data URI: ${attachment.path}` };
1418
+ }
1419
+ const absPath = input.resolveAttachmentPath(attachment.path);
1420
+ emittedAbsPaths.add(absPath);
1421
+ if (attachment.type === "image") {
1422
+ const inlinePart2 = {
1423
+ type: "image",
1424
+ filename: attachment.name,
1425
+ mediaType,
1426
+ url: `data:${mediaType};base64,${base64}`
1427
+ };
1428
+ const cost2 = byteLen(JSON.stringify(inlinePart2));
1429
+ if (!forcePath && runningInline + cost2 <= inlineBudget) {
1430
+ parts.push(inlinePart2);
1431
+ runningInline += cost2;
1432
+ } else {
1433
+ parts.push({ type: "image", filename: attachment.name, mediaType, path: absPath });
1434
+ }
1435
+ continue;
1436
+ }
1437
+ const fileMediaType = mediaType ?? "application/octet-stream";
1438
+ const inlinePart = {
1439
+ type: "file",
1440
+ filename: attachment.name,
1441
+ mediaType: fileMediaType,
1442
+ url: `data:${fileMediaType};base64,${base64}`
1443
+ };
1444
+ const cost = byteLen(JSON.stringify(inlinePart));
1445
+ if (!forcePath && runningInline + cost <= inlineBudget) {
1446
+ parts.push(inlinePart);
1447
+ runningInline += cost;
1448
+ } else {
1449
+ parts.push({ type: "file", path: absPath });
1450
+ }
1451
+ }
1452
+ if (mentions.length > 0 && !input.box) {
1453
+ return { succeeded: false, error: "internal error: sandbox mentions require a box to read from" };
1454
+ }
1455
+ for (const mention of mentions) {
1456
+ if (!mention.path) {
1457
+ return { succeeded: false, error: `mention path must be non-empty: ${mention.name}` };
1458
+ }
1459
+ const absPath = resolveMentionPath(mention.path);
1460
+ if (emittedAbsPaths.has(absPath)) continue;
1461
+ emittedAbsPaths.add(absPath);
1462
+ const isImage = mention.mentionKind === "image";
1463
+ const mediaType = isImage ? mediaTypeForMentionPath(mention.path) : void 0;
1464
+ let stat;
1465
+ try {
1466
+ stat = await readMention(input.box, absPath, { readBytes: false });
1467
+ } catch (err) {
1468
+ return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
1469
+ }
1470
+ if (!stat.succeeded) return { succeeded: false, error: stat.error };
1471
+ const projectedInlineCost = base64WireLen(stat.value.size) + byteLen(JSON.stringify({ type: "image", filename: mention.name, mediaType: mediaType ?? "", url: "" }));
1472
+ if (isImage && mediaType && !forcePath && runningInline + projectedInlineCost <= inlineBudget) {
1473
+ let read;
1474
+ try {
1475
+ read = await readMention(input.box, absPath, { readBytes: true });
1476
+ } catch (err) {
1477
+ return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
1478
+ }
1479
+ if (!read.succeeded) return { succeeded: false, error: read.error };
1480
+ if (!read.value.base64) return { succeeded: false, error: `mentioned image produced no bytes: ${absPath}` };
1481
+ const inlinePart = {
1482
+ type: "image",
1483
+ filename: mention.name,
1484
+ mediaType,
1485
+ url: `data:${mediaType};base64,${read.value.base64}`
1486
+ };
1487
+ const cost = byteLen(JSON.stringify(inlinePart));
1488
+ if (runningInline + cost <= inlineBudget) {
1489
+ parts.push(inlinePart);
1490
+ runningInline += cost;
1491
+ continue;
1492
+ }
1493
+ }
1494
+ parts.push(
1495
+ isImage && mediaType ? { type: "image", filename: mention.name, mediaType, path: absPath } : { type: "file", path: absPath }
1496
+ );
1497
+ }
1498
+ for (const part of parts) {
1499
+ if (violatesUrlPathXor(part)) {
1500
+ return { succeeded: false, error: "internal error: emitted media part violates the url/path exclusivity invariant" };
1501
+ }
1502
+ }
1503
+ const textPartSize = base64WireLen(byteLen(flattenedForSizing));
1504
+ const mediaPartsSize = parts.slice(1).reduce((total, part) => total + byteLen(JSON.stringify(part)), 0);
1505
+ const systemPromptSize = byteLen(JSON.stringify(input.systemPrompt));
1506
+ if (textPartSize + mediaPartsSize + systemPromptSize + input.profileWireBytes + structuralReserveBytes > requestMaxBytes) {
1507
+ return { succeeded: false, error: "dispatch parts exceed the sandbox proxy request cap even after path demotion" };
1508
+ }
1509
+ if (parts.length > maxParts) {
1510
+ return { succeeded: false, error: `dispatch parts exceed the sidecar per-request cap of ${maxParts}` };
1511
+ }
1512
+ return { succeeded: true, value: parts };
1513
+ }
1331
1514
  export {
1515
+ ALLOWED_ATTACHMENT_SNIFFED_MIMES,
1516
+ ATTACHMENT_ACCEPT,
1332
1517
  ATTACHMENT_MAX_COUNT,
1333
1518
  ChatTurnInputError,
1334
1519
  DEFAULT_STALE_TURN_LOCK_GRACE_MS,
@@ -1339,17 +1524,22 @@ export {
1339
1524
  DISPATCH_STRUCTURAL_RESERVE_BYTES,
1340
1525
  INLINE_PARTS_MAX_BYTES,
1341
1526
  MAX_ATTACHMENT_TOTAL_BYTES,
1527
+ MAX_BINARY_ATTACHMENT_BYTES,
1528
+ MAX_TEXT_ATTACHMENT_BYTES,
1342
1529
  MENTION_MAX_COUNT,
1343
1530
  PROMOTE_MAX_FILE_BYTES,
1344
1531
  UPLOAD_INLINE_MAX_BYTES,
1345
1532
  UPLOAD_MAX_FILE_BYTES,
1346
1533
  assertPromptPartsWithinCap,
1534
+ attachmentSizeErrorMessage,
1347
1535
  attachmentTotalSizeErrorMessage,
1348
1536
  base64WireLen,
1349
1537
  buildDispatchParts,
1350
1538
  buildMentionPromptBlock,
1351
1539
  bytesToBase64,
1352
1540
  chatTurnRequestInit,
1541
+ checkAttachmentType,
1542
+ createAttachmentUploadRoute,
1353
1543
  createChatTurnRoutes,
1354
1544
  createSandboxChatProducer,
1355
1545
  createSandboxFileIndexRoute,
@@ -1365,7 +1555,10 @@ export {
1365
1555
  promptPartsByteSize,
1366
1556
  reconcileStaleTurnLock,
1367
1557
  resolveChatAttachments,
1558
+ runDetachedTurn,
1559
+ sanitizeAttachmentFileName,
1368
1560
  sanitizeUploadFilename,
1561
+ sniffBinary,
1369
1562
  sniffMimeFromName,
1370
1563
  validateSandboxMentionPath,
1371
1564
  withDurableChatProjection