@tangle-network/agent-app 0.43.49 → 0.43.51

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,
@@ -896,12 +907,7 @@ function createUploadRoute(options) {
896
907
  }
897
908
 
898
909
  // src/chat-routes/resolve-attachments.ts
899
- var ATTACHMENT_MAX_COUNT = 10;
900
- var MAX_ATTACHMENT_TOTAL_BYTES = 25 * 1024 * 1024;
901
910
  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
911
  function isAttachmentKind(value) {
906
912
  return value === "image" || value === "file";
907
913
  }
@@ -995,164 +1001,6 @@ async function resolveChatAttachments(value, options) {
995
1001
  return { succeeded: true, value: inputs.map(attachmentInputToPart) };
996
1002
  }
997
1003
 
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
1004
  // src/chat-routes/promote-file-part.ts
1157
1005
  var PROMOTE_MAX_FILE_BYTES = 10 * 1024 * 1024;
1158
1006
  var EXT_TO_MIME = {
@@ -1192,10 +1040,6 @@ function sniffMimeFromName(filename) {
1192
1040
  if (!ext) return "text/plain";
1193
1041
  return EXT_TO_MIME[ext] ?? "text/plain";
1194
1042
  }
1195
- function sanitizeAttachmentFileName(name) {
1196
- const sanitized = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
1197
- return sanitized || "file";
1198
- }
1199
1043
  function base64ToBytes(base64) {
1200
1044
  const binary = atob(base64);
1201
1045
  const bytes = new Uint8Array(binary.length);
@@ -1328,7 +1172,295 @@ async function promoteAgentFilePart(options) {
1328
1172
  }
1329
1173
  };
1330
1174
  }
1175
+
1176
+ // src/chat-routes/attachment-upload.ts
1177
+ function attachmentUploadError(status, code, message, path) {
1178
+ return Response.json(
1179
+ { error: path === void 0 ? { code, message } : { code, message, path } },
1180
+ { status }
1181
+ );
1182
+ }
1183
+ function createAttachmentUploadRoute(options) {
1184
+ const maxCount = options.limits?.maxCount ?? ATTACHMENT_MAX_COUNT;
1185
+ const maxBinaryBytes = options.limits?.maxBinaryBytes ?? MAX_BINARY_ATTACHMENT_BYTES;
1186
+ const maxTextBytes = options.limits?.maxTextBytes ?? MAX_TEXT_ATTACHMENT_BYTES;
1187
+ const maxTotalBytes = options.limits?.maxTotalBytes ?? MAX_ATTACHMENT_TOTAL_BYTES;
1188
+ const allowedKinds = options.allowedKinds ?? ["image", "file"];
1189
+ const allowedSniffedMimes = options.allowedSniffedMimes ?? ALLOWED_ATTACHMENT_SNIFFED_MIMES;
1190
+ const pathFor = options.pathFor ?? ((name) => name);
1191
+ const validatePath = options.validatePath ?? defaultValidateAttachmentPath;
1192
+ const sniffMime = options.sniffMime ?? sniffMimeFromName;
1193
+ return async function attachmentUpload(request) {
1194
+ const auth = await options.authorize({ request });
1195
+ if (!auth.ok) return auth.response;
1196
+ const write = auth.writeAttachment ?? options.writeAttachment;
1197
+ let form;
1198
+ try {
1199
+ form = await request.formData();
1200
+ } catch {
1201
+ return attachmentUploadError(400, "invalid_upload", "Expected a multipart/form-data body with file fields");
1202
+ }
1203
+ const files = [];
1204
+ form.forEach((value) => {
1205
+ if (value instanceof File) files.push(value);
1206
+ });
1207
+ if (files.length === 0) {
1208
+ return attachmentUploadError(400, "invalid_upload", "No files in the upload body");
1209
+ }
1210
+ if (files.length > maxCount) {
1211
+ return attachmentUploadError(
1212
+ 400,
1213
+ "attachment_count_exceeded",
1214
+ `Too many files \u2014 the ${maxCount}-file limit was exceeded`
1215
+ );
1216
+ }
1217
+ const advisoryTotal = files.reduce((sum, file) => sum + file.size, 0);
1218
+ if (advisoryTotal > maxTotalBytes) {
1219
+ return attachmentUploadError(
1220
+ 413,
1221
+ "attachments_total_too_large",
1222
+ attachmentTotalSizeErrorMessage(advisoryTotal, maxTotalBytes)
1223
+ );
1224
+ }
1225
+ const prepared = [];
1226
+ const seenPaths = /* @__PURE__ */ new Set();
1227
+ let totalBytes = 0;
1228
+ for (const file of files) {
1229
+ const bytes = new Uint8Array(await file.arrayBuffer());
1230
+ const sniff = sniffBinary(bytes);
1231
+ const name = sanitizeAttachmentFileName(file.name);
1232
+ const typeCheck = checkAttachmentType(name, sniff, allowedSniffedMimes);
1233
+ if (!typeCheck.succeeded) {
1234
+ return attachmentUploadError(
1235
+ typeCheck.code === "attachment_type_mismatch" ? 400 : 415,
1236
+ typeCheck.code,
1237
+ typeCheck.message
1238
+ );
1239
+ }
1240
+ const kind = attachmentKindForMime(sniff.mime ?? "");
1241
+ if (!allowedKinds.includes(kind)) {
1242
+ return attachmentUploadError(
1243
+ 415,
1244
+ "attachment_kind_not_allowed",
1245
+ `${name} is a "${kind}" attachment, which this upload route does not accept`
1246
+ );
1247
+ }
1248
+ const limit = sniff.binary ? maxBinaryBytes : maxTextBytes;
1249
+ if (bytes.length > limit) {
1250
+ return attachmentUploadError(
1251
+ 413,
1252
+ "attachment_too_large",
1253
+ attachmentSizeErrorMessage(name, bytes.length, limit)
1254
+ );
1255
+ }
1256
+ const path = pathFor(name);
1257
+ const pathCheck = validatePath(path);
1258
+ if (!pathCheck.succeeded) {
1259
+ return attachmentUploadError(400, "invalid_attachment_path", pathCheck.error, path);
1260
+ }
1261
+ if (seenPaths.has(path)) {
1262
+ return attachmentUploadError(
1263
+ 400,
1264
+ "attachment_duplicate_path",
1265
+ `attachments must not repeat a path within one upload: ${path}`,
1266
+ path
1267
+ );
1268
+ }
1269
+ seenPaths.add(path);
1270
+ totalBytes += bytes.length;
1271
+ if (totalBytes > maxTotalBytes) {
1272
+ return attachmentUploadError(
1273
+ 413,
1274
+ "attachments_total_too_large",
1275
+ attachmentTotalSizeErrorMessage(totalBytes, maxTotalBytes)
1276
+ );
1277
+ }
1278
+ const mediaType = sniff.mime ?? sniffMime(name);
1279
+ prepared.push({ path, name, bytes, originalName: file.name, size: bytes.length, mediaType, kind });
1280
+ }
1281
+ const uploaded = [];
1282
+ for (const input of prepared) {
1283
+ const written = await write(auth.scopeId, input.path, input.bytes, {
1284
+ mediaType: input.mediaType,
1285
+ name: input.name,
1286
+ originalName: input.originalName,
1287
+ size: input.size
1288
+ });
1289
+ if (!written.ok) {
1290
+ return attachmentUploadError(413, "attachment_write_failed", written.reason, input.path);
1291
+ }
1292
+ uploaded.push({
1293
+ path: input.path,
1294
+ name: input.name,
1295
+ size: input.size,
1296
+ mediaType: input.mediaType,
1297
+ kind: input.kind
1298
+ });
1299
+ }
1300
+ return Response.json({ files: uploaded });
1301
+ };
1302
+ }
1303
+
1304
+ // src/chat-routes/dispatch-parts.ts
1305
+ function byteLen(value) {
1306
+ return new TextEncoder().encode(value).length;
1307
+ }
1308
+ async function readSandboxMention(box, absolutePath, options) {
1309
+ const stat = await statSandboxFileSize(box, absolutePath);
1310
+ if (!stat.succeeded) {
1311
+ return { succeeded: false, error: `mentioned sandbox file missing or unreadable: ${absolutePath} \u2014 ${stat.error}` };
1312
+ }
1313
+ if (!options.readBytes) return { succeeded: true, value: { size: stat.value } };
1314
+ const read = await readSandboxBinaryBytes(box, absolutePath, stat.value);
1315
+ if (!read.succeeded) {
1316
+ return { succeeded: false, error: `mentioned sandbox file read failed: ${absolutePath} \u2014 ${read.error}` };
1317
+ }
1318
+ return { succeeded: true, value: { size: stat.value, base64: bytesToBase64(read.value.bytes) } };
1319
+ }
1320
+ function violatesUrlPathXor(part) {
1321
+ if (part.type === "text") return false;
1322
+ const hasUrl = typeof part.url === "string" && part.url.startsWith("data:");
1323
+ const hasPath = typeof part.path === "string" && part.path.startsWith("/");
1324
+ return hasUrl === hasPath;
1325
+ }
1326
+ function readResultToBase64(read) {
1327
+ if (typeof read.base64 === "string") return read.base64;
1328
+ if (read.bytes) return bytesToBase64(read.bytes);
1329
+ return void 0;
1330
+ }
1331
+ async function buildDispatchParts(input) {
1332
+ const readMention = input.readSandboxMention ?? readSandboxMention;
1333
+ const resolveMentionPath = input.resolveMentionPath ?? input.resolveAttachmentPath;
1334
+ const forcePath = input.forcePath ?? false;
1335
+ const mentions = input.mentions ?? [];
1336
+ const requestMaxBytes = input.requestMaxBytes ?? DISPATCH_REQUEST_MAX_BYTES;
1337
+ const structuralReserveBytes = input.structuralReserveBytes ?? DISPATCH_STRUCTURAL_RESERVE_BYTES;
1338
+ const maxParts = input.maxParts ?? DISPATCH_MAX_PARTS;
1339
+ const parts = [{ type: "text", text: input.text }];
1340
+ const emittedAbsPaths = /* @__PURE__ */ new Set();
1341
+ const flattenedForSizing = flattenHistory(input.text, input.history);
1342
+ const inlineBudget = requestMaxBytes - base64WireLen(byteLen(flattenedForSizing)) - byteLen(JSON.stringify(input.systemPrompt)) - input.profileWireBytes - structuralReserveBytes;
1343
+ let runningInline = 0;
1344
+ for (const attachment of input.attachments) {
1345
+ if (!attachment.path) {
1346
+ return { succeeded: false, error: `attachment path must be non-empty: ${attachment.name}` };
1347
+ }
1348
+ let read;
1349
+ try {
1350
+ read = await input.readAttachment(input.scopeId, attachment.path);
1351
+ } catch (err) {
1352
+ return {
1353
+ succeeded: false,
1354
+ error: `attachment store read failed: ${attachment.path} \u2014 ${err instanceof Error ? err.message : String(err)}`
1355
+ };
1356
+ }
1357
+ if (!read.ok) return { succeeded: false, error: read.reason };
1358
+ const base64 = readResultToBase64(read);
1359
+ if (base64 === void 0) {
1360
+ return { succeeded: false, error: `attachment store read produced no content: ${attachment.path}` };
1361
+ }
1362
+ const mediaType = attachment.mediaType ?? read.mediaType;
1363
+ if (attachment.type === "image" && !mediaType) {
1364
+ return { succeeded: false, error: `attachment is missing a mediaType required for an image data URI: ${attachment.path}` };
1365
+ }
1366
+ const absPath = input.resolveAttachmentPath(attachment.path);
1367
+ emittedAbsPaths.add(absPath);
1368
+ if (attachment.type === "image") {
1369
+ const inlinePart2 = {
1370
+ type: "image",
1371
+ filename: attachment.name,
1372
+ mediaType,
1373
+ url: `data:${mediaType};base64,${base64}`
1374
+ };
1375
+ const cost2 = byteLen(JSON.stringify(inlinePart2));
1376
+ if (!forcePath && runningInline + cost2 <= inlineBudget) {
1377
+ parts.push(inlinePart2);
1378
+ runningInline += cost2;
1379
+ } else {
1380
+ parts.push({ type: "image", filename: attachment.name, mediaType, path: absPath });
1381
+ }
1382
+ continue;
1383
+ }
1384
+ const fileMediaType = mediaType ?? "application/octet-stream";
1385
+ const inlinePart = {
1386
+ type: "file",
1387
+ filename: attachment.name,
1388
+ mediaType: fileMediaType,
1389
+ url: `data:${fileMediaType};base64,${base64}`
1390
+ };
1391
+ const cost = byteLen(JSON.stringify(inlinePart));
1392
+ if (!forcePath && runningInline + cost <= inlineBudget) {
1393
+ parts.push(inlinePart);
1394
+ runningInline += cost;
1395
+ } else {
1396
+ parts.push({ type: "file", path: absPath });
1397
+ }
1398
+ }
1399
+ if (mentions.length > 0 && !input.box) {
1400
+ return { succeeded: false, error: "internal error: sandbox mentions require a box to read from" };
1401
+ }
1402
+ for (const mention of mentions) {
1403
+ if (!mention.path) {
1404
+ return { succeeded: false, error: `mention path must be non-empty: ${mention.name}` };
1405
+ }
1406
+ const absPath = resolveMentionPath(mention.path);
1407
+ if (emittedAbsPaths.has(absPath)) continue;
1408
+ emittedAbsPaths.add(absPath);
1409
+ const isImage = mention.mentionKind === "image";
1410
+ const mediaType = isImage ? mediaTypeForMentionPath(mention.path) : void 0;
1411
+ let stat;
1412
+ try {
1413
+ stat = await readMention(input.box, absPath, { readBytes: false });
1414
+ } catch (err) {
1415
+ return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
1416
+ }
1417
+ if (!stat.succeeded) return { succeeded: false, error: stat.error };
1418
+ const projectedInlineCost = base64WireLen(stat.value.size) + byteLen(JSON.stringify({ type: "image", filename: mention.name, mediaType: mediaType ?? "", url: "" }));
1419
+ if (isImage && mediaType && !forcePath && runningInline + projectedInlineCost <= inlineBudget) {
1420
+ let read;
1421
+ try {
1422
+ read = await readMention(input.box, absPath, { readBytes: true });
1423
+ } catch (err) {
1424
+ return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
1425
+ }
1426
+ if (!read.succeeded) return { succeeded: false, error: read.error };
1427
+ if (!read.value.base64) return { succeeded: false, error: `mentioned image produced no bytes: ${absPath}` };
1428
+ const inlinePart = {
1429
+ type: "image",
1430
+ filename: mention.name,
1431
+ mediaType,
1432
+ url: `data:${mediaType};base64,${read.value.base64}`
1433
+ };
1434
+ const cost = byteLen(JSON.stringify(inlinePart));
1435
+ if (runningInline + cost <= inlineBudget) {
1436
+ parts.push(inlinePart);
1437
+ runningInline += cost;
1438
+ continue;
1439
+ }
1440
+ }
1441
+ parts.push(
1442
+ isImage && mediaType ? { type: "image", filename: mention.name, mediaType, path: absPath } : { type: "file", path: absPath }
1443
+ );
1444
+ }
1445
+ for (const part of parts) {
1446
+ if (violatesUrlPathXor(part)) {
1447
+ return { succeeded: false, error: "internal error: emitted media part violates the url/path exclusivity invariant" };
1448
+ }
1449
+ }
1450
+ const textPartSize = base64WireLen(byteLen(flattenedForSizing));
1451
+ const mediaPartsSize = parts.slice(1).reduce((total, part) => total + byteLen(JSON.stringify(part)), 0);
1452
+ const systemPromptSize = byteLen(JSON.stringify(input.systemPrompt));
1453
+ if (textPartSize + mediaPartsSize + systemPromptSize + input.profileWireBytes + structuralReserveBytes > requestMaxBytes) {
1454
+ return { succeeded: false, error: "dispatch parts exceed the sandbox proxy request cap even after path demotion" };
1455
+ }
1456
+ if (parts.length > maxParts) {
1457
+ return { succeeded: false, error: `dispatch parts exceed the sidecar per-request cap of ${maxParts}` };
1458
+ }
1459
+ return { succeeded: true, value: parts };
1460
+ }
1331
1461
  export {
1462
+ ALLOWED_ATTACHMENT_SNIFFED_MIMES,
1463
+ ATTACHMENT_ACCEPT,
1332
1464
  ATTACHMENT_MAX_COUNT,
1333
1465
  ChatTurnInputError,
1334
1466
  DEFAULT_STALE_TURN_LOCK_GRACE_MS,
@@ -1339,17 +1471,22 @@ export {
1339
1471
  DISPATCH_STRUCTURAL_RESERVE_BYTES,
1340
1472
  INLINE_PARTS_MAX_BYTES,
1341
1473
  MAX_ATTACHMENT_TOTAL_BYTES,
1474
+ MAX_BINARY_ATTACHMENT_BYTES,
1475
+ MAX_TEXT_ATTACHMENT_BYTES,
1342
1476
  MENTION_MAX_COUNT,
1343
1477
  PROMOTE_MAX_FILE_BYTES,
1344
1478
  UPLOAD_INLINE_MAX_BYTES,
1345
1479
  UPLOAD_MAX_FILE_BYTES,
1346
1480
  assertPromptPartsWithinCap,
1481
+ attachmentSizeErrorMessage,
1347
1482
  attachmentTotalSizeErrorMessage,
1348
1483
  base64WireLen,
1349
1484
  buildDispatchParts,
1350
1485
  buildMentionPromptBlock,
1351
1486
  bytesToBase64,
1352
1487
  chatTurnRequestInit,
1488
+ checkAttachmentType,
1489
+ createAttachmentUploadRoute,
1353
1490
  createChatTurnRoutes,
1354
1491
  createSandboxChatProducer,
1355
1492
  createSandboxFileIndexRoute,
@@ -1365,7 +1502,9 @@ export {
1365
1502
  promptPartsByteSize,
1366
1503
  reconcileStaleTurnLock,
1367
1504
  resolveChatAttachments,
1505
+ sanitizeAttachmentFileName,
1368
1506
  sanitizeUploadFilename,
1507
+ sniffBinary,
1369
1508
  sniffMimeFromName,
1370
1509
  validateSandboxMentionPath,
1371
1510
  withDurableChatProjection