@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.
- package/README.md +17 -0
- package/dist/assistant/index.d.ts +1 -1
- package/dist/assistant/index.js +2 -2
- package/dist/attachment-validation-DX2KIzMC.d.ts +219 -0
- package/dist/chat-routes/index.d.ts +90 -20
- package/dist/chat-routes/index.js +308 -169
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chunk-3EKOSBYL.js +239 -0
- package/dist/chunk-3EKOSBYL.js.map +1 -0
- package/dist/{chunk-O6H2WD3I.js → chunk-EIG7ZQW2.js} +854 -369
- package/dist/chunk-EIG7ZQW2.js.map +1 -0
- package/dist/web-react/index.d.ts +175 -5
- package/dist/web-react/index.js +15 -2
- package/package.json +1 -1
- package/dist/chunk-LCNY3DCM.js +0 -84
- package/dist/chunk-LCNY3DCM.js.map +0 -1
- package/dist/chunk-O6H2WD3I.js.map +0 -1
- package/dist/file-index-b26ee-_R.d.ts +0 -114
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
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
|