@assemblyline-agents/runtime 0.1.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.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/agent-harness.d.ts +108 -0
- package/dist/agent-harness.d.ts.map +1 -0
- package/dist/agent-harness.js +2 -0
- package/dist/agent-harness.js.map +1 -0
- package/dist/attachments-legacy.d.ts +17 -0
- package/dist/attachments-legacy.d.ts.map +1 -0
- package/dist/attachments-legacy.js +273 -0
- package/dist/attachments-legacy.js.map +1 -0
- package/dist/attachments.d.ts +49 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +832 -0
- package/dist/attachments.js.map +1 -0
- package/dist/builtin-tool-executors.d.ts +96 -0
- package/dist/builtin-tool-executors.d.ts.map +1 -0
- package/dist/builtin-tool-executors.js +501 -0
- package/dist/builtin-tool-executors.js.map +1 -0
- package/dist/builtin-tools.d.ts +30 -0
- package/dist/builtin-tools.d.ts.map +1 -0
- package/dist/builtin-tools.js +690 -0
- package/dist/builtin-tools.js.map +1 -0
- package/dist/capability-catalog.d.ts +27 -0
- package/dist/capability-catalog.d.ts.map +1 -0
- package/dist/capability-catalog.js +193 -0
- package/dist/capability-catalog.js.map +1 -0
- package/dist/channel-http-util.d.ts +34 -0
- package/dist/channel-http-util.d.ts.map +1 -0
- package/dist/channel-http-util.js +93 -0
- package/dist/channel-http-util.js.map +1 -0
- package/dist/channel-ingress.d.ts +56 -0
- package/dist/channel-ingress.d.ts.map +1 -0
- package/dist/channel-ingress.js +273 -0
- package/dist/channel-ingress.js.map +1 -0
- package/dist/channel.d.ts +146 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +2 -0
- package/dist/channel.js.map +1 -0
- package/dist/checkpoints.d.ts +34 -0
- package/dist/checkpoints.d.ts.map +1 -0
- package/dist/checkpoints.js +86 -0
- package/dist/checkpoints.js.map +1 -0
- package/dist/code-mode.d.ts +20 -0
- package/dist/code-mode.d.ts.map +1 -0
- package/dist/code-mode.js +658 -0
- package/dist/code-mode.js.map +1 -0
- package/dist/computer-relay-protocol.d.ts +36 -0
- package/dist/computer-relay-protocol.d.ts.map +1 -0
- package/dist/computer-relay-protocol.js +127 -0
- package/dist/computer-relay-protocol.js.map +1 -0
- package/dist/concurrency.d.ts +68 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +193 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/connection-auth-util.d.ts +56 -0
- package/dist/connection-auth-util.d.ts.map +1 -0
- package/dist/connection-auth-util.js +154 -0
- package/dist/connection-auth-util.js.map +1 -0
- package/dist/connection-clients.d.ts +50 -0
- package/dist/connection-clients.d.ts.map +1 -0
- package/dist/connection-clients.js +402 -0
- package/dist/connection-clients.js.map +1 -0
- package/dist/connection-stores.d.ts +108 -0
- package/dist/connection-stores.d.ts.map +1 -0
- package/dist/connection-stores.js +127 -0
- package/dist/connection-stores.js.map +1 -0
- package/dist/connection-util.d.ts +27 -0
- package/dist/connection-util.d.ts.map +1 -0
- package/dist/connection-util.js +118 -0
- package/dist/connection-util.js.map +1 -0
- package/dist/connections.d.ts +152 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +838 -0
- package/dist/connections.js.map +1 -0
- package/dist/content-capture.d.ts +32 -0
- package/dist/content-capture.d.ts.map +1 -0
- package/dist/content-capture.js +77 -0
- package/dist/content-capture.js.map +1 -0
- package/dist/context-policy-util.d.ts +45 -0
- package/dist/context-policy-util.d.ts.map +1 -0
- package/dist/context-policy-util.js +120 -0
- package/dist/context-policy-util.js.map +1 -0
- package/dist/conversation-context.d.ts +55 -0
- package/dist/conversation-context.d.ts.map +1 -0
- package/dist/conversation-context.js +179 -0
- package/dist/conversation-context.js.map +1 -0
- package/dist/conversation-transcript.d.ts +49 -0
- package/dist/conversation-transcript.d.ts.map +1 -0
- package/dist/conversation-transcript.js +77 -0
- package/dist/conversation-transcript.js.map +1 -0
- package/dist/conversation-turn-dispatch.d.ts +46 -0
- package/dist/conversation-turn-dispatch.d.ts.map +1 -0
- package/dist/conversation-turn-dispatch.js +201 -0
- package/dist/conversation-turn-dispatch.js.map +1 -0
- package/dist/delivery-queue.d.ts +89 -0
- package/dist/delivery-queue.d.ts.map +1 -0
- package/dist/delivery-queue.js +244 -0
- package/dist/delivery-queue.js.map +1 -0
- package/dist/delivery-send.d.ts +44 -0
- package/dist/delivery-send.d.ts.map +1 -0
- package/dist/delivery-send.js +494 -0
- package/dist/delivery-send.js.map +1 -0
- package/dist/env-config.d.ts +6 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +27 -0
- package/dist/env-config.js.map +1 -0
- package/dist/eval-run-options.d.ts +37 -0
- package/dist/eval-run-options.d.ts.map +1 -0
- package/dist/eval-run-options.js +106 -0
- package/dist/eval-run-options.js.map +1 -0
- package/dist/harness-continuation.d.ts +54 -0
- package/dist/harness-continuation.d.ts.map +1 -0
- package/dist/harness-continuation.js +58 -0
- package/dist/harness-continuation.js.map +1 -0
- package/dist/harness-loop.d.ts +110 -0
- package/dist/harness-loop.d.ts.map +1 -0
- package/dist/harness-loop.js +575 -0
- package/dist/harness-loop.js.map +1 -0
- package/dist/hook-dispatch.d.ts +19 -0
- package/dist/hook-dispatch.d.ts.map +1 -0
- package/dist/hook-dispatch.js +37 -0
- package/dist/hook-dispatch.js.map +1 -0
- package/dist/http.d.ts +26 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +114 -0
- package/dist/http.js.map +1 -0
- package/dist/in-memory-conversation-turn-store.d.ts +29 -0
- package/dist/in-memory-conversation-turn-store.d.ts.map +1 -0
- package/dist/in-memory-conversation-turn-store.js +166 -0
- package/dist/in-memory-conversation-turn-store.js.map +1 -0
- package/dist/in-memory-run-store.d.ts +39 -0
- package/dist/in-memory-run-store.d.ts.map +1 -0
- package/dist/in-memory-run-store.js +332 -0
- package/dist/in-memory-run-store.js.map +1 -0
- package/dist/in-memory-sandbox-session-store.d.ts +33 -0
- package/dist/in-memory-sandbox-session-store.d.ts.map +1 -0
- package/dist/in-memory-sandbox-session-store.js +186 -0
- package/dist/in-memory-sandbox-session-store.js.map +1 -0
- package/dist/in-memory-small-stores.d.ts +117 -0
- package/dist/in-memory-small-stores.d.ts.map +1 -0
- package/dist/in-memory-small-stores.js +340 -0
- package/dist/in-memory-small-stores.js.map +1 -0
- package/dist/in-memory-usage-store.d.ts +15 -0
- package/dist/in-memory-usage-store.d.ts.map +1 -0
- package/dist/in-memory-usage-store.js +49 -0
- package/dist/in-memory-usage-store.js.map +1 -0
- package/dist/index.d.ts +467 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2046 -0
- package/dist/index.js.map +1 -0
- package/dist/json-util.d.ts +19 -0
- package/dist/json-util.d.ts.map +1 -0
- package/dist/json-util.js +59 -0
- package/dist/json-util.js.map +1 -0
- package/dist/local-sandbox.d.ts +16 -0
- package/dist/local-sandbox.d.ts.map +1 -0
- package/dist/local-sandbox.js +327 -0
- package/dist/local-sandbox.js.map +1 -0
- package/dist/logging.d.ts +16 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +41 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp-client.d.ts +27 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +169 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-harnesses.d.ts +4 -0
- package/dist/model-harnesses.d.ts.map +1 -0
- package/dist/model-harnesses.js +6 -0
- package/dist/model-harnesses.js.map +1 -0
- package/dist/model-media.d.ts +36 -0
- package/dist/model-media.d.ts.map +1 -0
- package/dist/model-media.js +234 -0
- package/dist/model-media.js.map +1 -0
- package/dist/operator-controls.d.ts +31 -0
- package/dist/operator-controls.d.ts.map +1 -0
- package/dist/operator-controls.js +71 -0
- package/dist/operator-controls.js.map +1 -0
- package/dist/pi-agent-harness.d.ts +40 -0
- package/dist/pi-agent-harness.d.ts.map +1 -0
- package/dist/pi-agent-harness.js +222 -0
- package/dist/pi-agent-harness.js.map +1 -0
- package/dist/pi-harness.d.ts +42 -0
- package/dist/pi-harness.d.ts.map +1 -0
- package/dist/pi-harness.js +142 -0
- package/dist/pi-harness.js.map +1 -0
- package/dist/relay-mcp-client.d.ts +17 -0
- package/dist/relay-mcp-client.d.ts.map +1 -0
- package/dist/relay-mcp-client.js +119 -0
- package/dist/relay-mcp-client.js.map +1 -0
- package/dist/replay-resume.d.ts +107 -0
- package/dist/replay-resume.d.ts.map +1 -0
- package/dist/replay-resume.js +495 -0
- package/dist/replay-resume.js.map +1 -0
- package/dist/resource-projection-util.d.ts +58 -0
- package/dist/resource-projection-util.d.ts.map +1 -0
- package/dist/resource-projection-util.js +298 -0
- package/dist/resource-projection-util.js.map +1 -0
- package/dist/resource-projection.d.ts +45 -0
- package/dist/resource-projection.d.ts.map +1 -0
- package/dist/resource-projection.js +481 -0
- package/dist/resource-projection.js.map +1 -0
- package/dist/run-heartbeat.d.ts +34 -0
- package/dist/run-heartbeat.d.ts.map +1 -0
- package/dist/run-heartbeat.js +62 -0
- package/dist/run-heartbeat.js.map +1 -0
- package/dist/run-outcome.d.ts +47 -0
- package/dist/run-outcome.d.ts.map +1 -0
- package/dist/run-outcome.js +92 -0
- package/dist/run-outcome.js.map +1 -0
- package/dist/run-recovery.d.ts +117 -0
- package/dist/run-recovery.d.ts.map +1 -0
- package/dist/run-recovery.js +466 -0
- package/dist/run-recovery.js.map +1 -0
- package/dist/run-resolver.d.ts +35 -0
- package/dist/run-resolver.d.ts.map +1 -0
- package/dist/run-resolver.js +84 -0
- package/dist/run-resolver.js.map +1 -0
- package/dist/run-stream.d.ts +37 -0
- package/dist/run-stream.d.ts.map +1 -0
- package/dist/run-stream.js +78 -0
- package/dist/run-stream.js.map +1 -0
- package/dist/runs-query.d.ts +29 -0
- package/dist/runs-query.d.ts.map +1 -0
- package/dist/runs-query.js +142 -0
- package/dist/runs-query.js.map +1 -0
- package/dist/runtime-bundle.d.ts +3 -0
- package/dist/runtime-bundle.d.ts.map +1 -0
- package/dist/runtime-bundle.js +18 -0
- package/dist/runtime-bundle.js.map +1 -0
- package/dist/runtime-types.d.ts +397 -0
- package/dist/runtime-types.d.ts.map +1 -0
- package/dist/runtime-types.js +2 -0
- package/dist/runtime-types.js.map +1 -0
- package/dist/runtime-util.d.ts +45 -0
- package/dist/runtime-util.d.ts.map +1 -0
- package/dist/runtime-util.js +116 -0
- package/dist/runtime-util.js.map +1 -0
- package/dist/sandbox-cli-client.d.ts +20 -0
- package/dist/sandbox-cli-client.d.ts.map +1 -0
- package/dist/sandbox-cli-client.js +97 -0
- package/dist/sandbox-cli-client.js.map +1 -0
- package/dist/sandbox-fs.d.ts +56 -0
- package/dist/sandbox-fs.d.ts.map +1 -0
- package/dist/sandbox-fs.js +276 -0
- package/dist/sandbox-fs.js.map +1 -0
- package/dist/sandbox-session-key.d.ts +8 -0
- package/dist/sandbox-session-key.d.ts.map +1 -0
- package/dist/sandbox-session-key.js +32 -0
- package/dist/sandbox-session-key.js.map +1 -0
- package/dist/sandbox-snapshot-policy.d.ts +27 -0
- package/dist/sandbox-snapshot-policy.d.ts.map +1 -0
- package/dist/sandbox-snapshot-policy.js +94 -0
- package/dist/sandbox-snapshot-policy.js.map +1 -0
- package/dist/sandbox-sync-worker.d.ts +63 -0
- package/dist/sandbox-sync-worker.d.ts.map +1 -0
- package/dist/sandbox-sync-worker.js +448 -0
- package/dist/sandbox-sync-worker.js.map +1 -0
- package/dist/schedule-execution.d.ts +59 -0
- package/dist/schedule-execution.d.ts.map +1 -0
- package/dist/schedule-execution.js +951 -0
- package/dist/schedule-execution.js.map +1 -0
- package/dist/scheduler.d.ts +19 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +38 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/security.d.ts +20 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +145 -0
- package/dist/security.js.map +1 -0
- package/dist/self-improvement.d.ts +245 -0
- package/dist/self-improvement.d.ts.map +1 -0
- package/dist/self-improvement.js +554 -0
- package/dist/self-improvement.js.map +1 -0
- package/dist/source-loader.d.ts +19 -0
- package/dist/source-loader.d.ts.map +1 -0
- package/dist/source-loader.js +160 -0
- package/dist/source-loader.js.map +1 -0
- package/dist/state-store-helpers.d.ts +28 -0
- package/dist/state-store-helpers.d.ts.map +1 -0
- package/dist/state-store-helpers.js +164 -0
- package/dist/state-store-helpers.js.map +1 -0
- package/dist/state-stores.d.ts +25 -0
- package/dist/state-stores.d.ts.map +1 -0
- package/dist/state-stores.js +157 -0
- package/dist/state-stores.js.map +1 -0
- package/dist/state.d.ts +736 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +46 -0
- package/dist/state.js.map +1 -0
- package/dist/storage.d.ts +188 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +752 -0
- package/dist/storage.js.map +1 -0
- package/dist/subagent-execution.d.ts +80 -0
- package/dist/subagent-execution.d.ts.map +1 -0
- package/dist/subagent-execution.js +135 -0
- package/dist/subagent-execution.js.map +1 -0
- package/dist/telemetry.d.ts +49 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +60 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tool-apis.d.ts +22 -0
- package/dist/tool-apis.d.ts.map +1 -0
- package/dist/tool-apis.js +344 -0
- package/dist/tool-apis.js.map +1 -0
- package/dist/tool-execution.d.ts +143 -0
- package/dist/tool-execution.d.ts.map +1 -0
- package/dist/tool-execution.js +573 -0
- package/dist/tool-execution.js.map +1 -0
- package/dist/trusted-attachments.d.ts +20 -0
- package/dist/trusted-attachments.d.ts.map +1 -0
- package/dist/trusted-attachments.js +276 -0
- package/dist/trusted-attachments.js.map +1 -0
- package/dist/usage-accounting.d.ts +37 -0
- package/dist/usage-accounting.d.ts.map +1 -0
- package/dist/usage-accounting.js +181 -0
- package/dist/usage-accounting.js.map +1 -0
- package/dist/usage-budget.d.ts +29 -0
- package/dist/usage-budget.d.ts.map +1 -0
- package/dist/usage-budget.js +145 -0
- package/dist/usage-budget.js.map +1 -0
- package/dist/usage-reconciliation.d.ts +76 -0
- package/dist/usage-reconciliation.d.ts.map +1 -0
- package/dist/usage-reconciliation.js +463 -0
- package/dist/usage-reconciliation.js.map +1 -0
- package/dist/usage-records.d.ts +24 -0
- package/dist/usage-records.d.ts.map +1 -0
- package/dist/usage-records.js +445 -0
- package/dist/usage-records.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { inflateRawSync } from "node:zlib";
|
|
3
|
+
import { trustedProviderDownloadRequest } from "./trusted-attachments.js";
|
|
4
|
+
const DEFAULT_ATTACHMENT_MAX_COUNT = 20;
|
|
5
|
+
const DEFAULT_ATTACHMENT_MAX_BYTES = 50 * 1024 * 1024;
|
|
6
|
+
const DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS = 30_000;
|
|
7
|
+
const DEFAULT_ZIP_MAX_FILES = 500;
|
|
8
|
+
const DEFAULT_ZIP_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
|
|
9
|
+
export async function materializeTurnAttachments(input) {
|
|
10
|
+
const maxCount = boundedPositiveIntEnv(input.env.OPENEVE_ATTACHMENT_MAX_COUNT, DEFAULT_ATTACHMENT_MAX_COUNT, 1, 100);
|
|
11
|
+
const limited = input.attachments.slice(0, maxCount);
|
|
12
|
+
const output = [];
|
|
13
|
+
const usedPaths = new Set();
|
|
14
|
+
if (input.attachments.length > limited.length) {
|
|
15
|
+
await input.state.appendEvent(input.run.id, "attachment.intake_truncated", {
|
|
16
|
+
received: input.attachments.length,
|
|
17
|
+
stored: limited.length,
|
|
18
|
+
maxCount
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
for (const [index, attachment] of limited.entries()) {
|
|
22
|
+
const path = uniqueAttachmentPath(attachment, index, usedPaths);
|
|
23
|
+
const base = sanitizeAttachmentForContext({
|
|
24
|
+
...attachment,
|
|
25
|
+
path,
|
|
26
|
+
status: stringValue(attachment.status) || "pending"
|
|
27
|
+
});
|
|
28
|
+
const inline = inlineAttachmentContents(attachment);
|
|
29
|
+
if (inline !== undefined) {
|
|
30
|
+
try {
|
|
31
|
+
const maxBytes = boundedPositiveIntEnv(input.env.OPENEVE_ATTACHMENT_MAX_BYTES, DEFAULT_ATTACHMENT_MAX_BYTES, 1, 1024 * 1024 * 1024);
|
|
32
|
+
const contentType = attachmentContentType(attachment);
|
|
33
|
+
const bytes = typeof inline === "string" ? Buffer.from(inline) : Buffer.from(inline);
|
|
34
|
+
if (bytes.byteLength > maxBytes)
|
|
35
|
+
throw new Error(`Attachment exceeds max bytes (${maxBytes}).`);
|
|
36
|
+
const filename = attachmentFilename(attachment, index);
|
|
37
|
+
const blob = await input.blob.put(`attachments/${input.run.id}/${path.slice("/files/original/".length)}`, bytes, contentType);
|
|
38
|
+
const extraction = await maybeExtractZipAttachment({
|
|
39
|
+
...input,
|
|
40
|
+
attachment,
|
|
41
|
+
archivePath: path,
|
|
42
|
+
archiveFilename: filename,
|
|
43
|
+
bytes,
|
|
44
|
+
contentType
|
|
45
|
+
});
|
|
46
|
+
await input.state.recordFileIndex({
|
|
47
|
+
runId: input.run.id,
|
|
48
|
+
path,
|
|
49
|
+
kind: "file",
|
|
50
|
+
blobKey: blob.key,
|
|
51
|
+
sha256: blob.sha256,
|
|
52
|
+
sizeBytes: blob.size,
|
|
53
|
+
metadata: {
|
|
54
|
+
channel: input.channel,
|
|
55
|
+
source: "turn-attachment",
|
|
56
|
+
provider: stringValue(attachment.provider) || null,
|
|
57
|
+
filename,
|
|
58
|
+
contentType
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
await input.state.appendEvent(input.run.id, "attachment.stored", {
|
|
62
|
+
path,
|
|
63
|
+
channel: input.channel,
|
|
64
|
+
provider: stringValue(attachment.provider) || null,
|
|
65
|
+
sizeBytes: blob.size,
|
|
66
|
+
contentType,
|
|
67
|
+
inline: true
|
|
68
|
+
});
|
|
69
|
+
output.push({
|
|
70
|
+
...sanitizeAttachmentForContext({
|
|
71
|
+
...base,
|
|
72
|
+
id: stringValue(attachment.id) || `attachment-${index + 1}`,
|
|
73
|
+
channel: input.channel,
|
|
74
|
+
path,
|
|
75
|
+
filename,
|
|
76
|
+
name: filename,
|
|
77
|
+
status: extraction.extractedFiles.length > 0 ? "extracted" : "stored",
|
|
78
|
+
contentType,
|
|
79
|
+
mimeType: contentType,
|
|
80
|
+
size: blob.size,
|
|
81
|
+
sizeBytes: blob.size,
|
|
82
|
+
sha256: blob.sha256
|
|
83
|
+
}),
|
|
84
|
+
blobKey: blob.key,
|
|
85
|
+
...blobUriForContext(blob),
|
|
86
|
+
...(extraction.extractedPath ? { extractedPath: extraction.extractedPath } : {}),
|
|
87
|
+
...(extraction.extractedFiles.length > 0 ? { extractedFiles: extraction.extractedFiles } : {}),
|
|
88
|
+
...(extraction.errorMessage ? { extractionError: extraction.errorMessage } : {})
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
await input.state.appendEvent(input.run.id, "attachment.store_failed", {
|
|
94
|
+
path,
|
|
95
|
+
channel: input.channel,
|
|
96
|
+
provider: stringValue(attachment.provider) || null,
|
|
97
|
+
error: message
|
|
98
|
+
});
|
|
99
|
+
output.push(sanitizeAttachmentForContext({
|
|
100
|
+
...base,
|
|
101
|
+
status: "failed",
|
|
102
|
+
errorMessage: message
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const download = await attachmentDownloadRequest(attachment, input);
|
|
109
|
+
if (!download) {
|
|
110
|
+
output.push(sanitizeAttachmentForContext({
|
|
111
|
+
...base,
|
|
112
|
+
status: "metadata_only"
|
|
113
|
+
}));
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const downloaded = await fetchAttachmentBytes(input.fetch, download, input.env);
|
|
117
|
+
const bytes = downloaded.bytes;
|
|
118
|
+
const hintedContentType = attachmentContentType(attachment);
|
|
119
|
+
const contentType = responseContentType(downloaded.contentType, hintedContentType);
|
|
120
|
+
const filename = attachmentFilename(attachment, index);
|
|
121
|
+
const blob = await input.blob.put(`attachments/${input.run.id}/${path.slice("/files/original/".length)}`, bytes, contentType);
|
|
122
|
+
const extraction = await maybeExtractZipAttachment({
|
|
123
|
+
...input,
|
|
124
|
+
attachment,
|
|
125
|
+
archivePath: path,
|
|
126
|
+
archiveFilename: filename,
|
|
127
|
+
bytes,
|
|
128
|
+
contentType
|
|
129
|
+
});
|
|
130
|
+
const stored = {
|
|
131
|
+
...sanitizeAttachmentForContext({
|
|
132
|
+
...base,
|
|
133
|
+
id: stringValue(attachment.id) || `attachment-${index + 1}`,
|
|
134
|
+
channel: input.channel,
|
|
135
|
+
status: extraction.extractedFiles.length > 0 ? "extracted" : "stored",
|
|
136
|
+
path,
|
|
137
|
+
filename,
|
|
138
|
+
name: filename,
|
|
139
|
+
contentType,
|
|
140
|
+
mimeType: contentType,
|
|
141
|
+
size: blob.size,
|
|
142
|
+
sizeBytes: blob.size,
|
|
143
|
+
sha256: blob.sha256
|
|
144
|
+
}),
|
|
145
|
+
blobKey: blob.key,
|
|
146
|
+
...blobUriForContext(blob),
|
|
147
|
+
...(extraction.extractedPath ? { extractedPath: extraction.extractedPath } : {}),
|
|
148
|
+
...(extraction.extractedFiles.length > 0 ? { extractedFiles: extraction.extractedFiles } : {}),
|
|
149
|
+
...(extraction.errorMessage ? { extractionError: extraction.errorMessage } : {})
|
|
150
|
+
};
|
|
151
|
+
await input.state.recordFileIndex({
|
|
152
|
+
runId: input.run.id,
|
|
153
|
+
path,
|
|
154
|
+
kind: "file",
|
|
155
|
+
blobKey: blob.key,
|
|
156
|
+
sha256: blob.sha256,
|
|
157
|
+
sizeBytes: blob.size,
|
|
158
|
+
metadata: {
|
|
159
|
+
channel: input.channel,
|
|
160
|
+
source: "turn-attachment",
|
|
161
|
+
provider: stringValue(attachment.provider) || null,
|
|
162
|
+
filename,
|
|
163
|
+
contentType
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
await input.state.appendEvent(input.run.id, "attachment.stored", {
|
|
167
|
+
path,
|
|
168
|
+
channel: input.channel,
|
|
169
|
+
provider: stringValue(attachment.provider) || null,
|
|
170
|
+
sizeBytes: blob.size,
|
|
171
|
+
contentType
|
|
172
|
+
});
|
|
173
|
+
output.push(stored);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
await input.state.appendEvent(input.run.id, "attachment.store_failed", {
|
|
178
|
+
path,
|
|
179
|
+
channel: input.channel,
|
|
180
|
+
provider: stringValue(attachment.provider) || null,
|
|
181
|
+
error: message
|
|
182
|
+
});
|
|
183
|
+
output.push(sanitizeAttachmentForContext({
|
|
184
|
+
...base,
|
|
185
|
+
status: "failed",
|
|
186
|
+
errorMessage: message
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return output;
|
|
191
|
+
}
|
|
192
|
+
export function fileManifestEntries(attachments) {
|
|
193
|
+
return attachments.flatMap((attachment, index) => {
|
|
194
|
+
const path = attachmentResourcePath(attachment);
|
|
195
|
+
const displayName = stringValue(attachment.displayName) ||
|
|
196
|
+
stringValue(attachment.filename) ||
|
|
197
|
+
stringValue(attachment.name) ||
|
|
198
|
+
path.split("/").at(-1) ||
|
|
199
|
+
`attachment-${index + 1}`;
|
|
200
|
+
const contentType = attachmentContentType(attachment);
|
|
201
|
+
const size = numberValue(attachment.sizeBytes) ?? numberValue(attachment.size) ?? inlineAttachmentSize(attachment);
|
|
202
|
+
const entry = {
|
|
203
|
+
fileId: stringValue(attachment.id) || `attachment-${index + 1}`,
|
|
204
|
+
displayName,
|
|
205
|
+
mimeType: contentType,
|
|
206
|
+
contentType,
|
|
207
|
+
source: stringValue(attachment.provider) || stringValue(attachment.source) || "turn-attachment",
|
|
208
|
+
status: stringValue(attachment.status) || (hasAttachmentContents(attachment) ? "stored" : "metadata_only"),
|
|
209
|
+
originalPath: path,
|
|
210
|
+
path
|
|
211
|
+
};
|
|
212
|
+
if (size !== undefined) {
|
|
213
|
+
entry.sizeBytes = size;
|
|
214
|
+
entry.size = size;
|
|
215
|
+
}
|
|
216
|
+
const revision = stringValue(attachment.sha256);
|
|
217
|
+
if (revision)
|
|
218
|
+
entry.sha256 = revision;
|
|
219
|
+
const extractedPath = stringValue(attachment.extractedPath);
|
|
220
|
+
if (extractedPath)
|
|
221
|
+
entry.extractedPath = extractedPath;
|
|
222
|
+
const errorMessage = stringValue(attachment.errorMessage);
|
|
223
|
+
if (errorMessage)
|
|
224
|
+
entry.errorMessage = errorMessage;
|
|
225
|
+
const extracted = Array.isArray(attachment.extractedFiles)
|
|
226
|
+
? attachment.extractedFiles.flatMap((value) => {
|
|
227
|
+
const record = recordValue(value);
|
|
228
|
+
return record ? [record] : [];
|
|
229
|
+
})
|
|
230
|
+
: [];
|
|
231
|
+
return [entry, ...extracted];
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
export async function attachmentContentsFromRecord(attachment, blob) {
|
|
235
|
+
const inline = inlineAttachmentContents(attachment);
|
|
236
|
+
if (inline !== undefined)
|
|
237
|
+
return inline;
|
|
238
|
+
const blobKey = stringValue(attachment.blobKey);
|
|
239
|
+
if (!blobKey || !blob)
|
|
240
|
+
return undefined;
|
|
241
|
+
const bytes = await blob.get(blobKey);
|
|
242
|
+
if (!bytes)
|
|
243
|
+
return undefined;
|
|
244
|
+
return isTextualContentType(attachmentContentType(attachment)) ? Buffer.from(bytes).toString("utf8") : bytes;
|
|
245
|
+
}
|
|
246
|
+
export function attachmentResourcePath(attachment) {
|
|
247
|
+
const explicitPath = stringValue(attachment.path);
|
|
248
|
+
if (explicitPath) {
|
|
249
|
+
const normalized = normalizeResourcePath(explicitPath);
|
|
250
|
+
return normalized.startsWith("/files") ? normalized : `/files/original/${sanitizeResourcePath(normalized)}`;
|
|
251
|
+
}
|
|
252
|
+
const name = stringValue(attachment.filename) || stringValue(attachment.name) || stringValue(attachment.id);
|
|
253
|
+
if (name)
|
|
254
|
+
return `/files/original/${sanitizeResourcePath(name)}`;
|
|
255
|
+
return `/files/original/attachment-${hashJson(attachment).slice(0, 12)}.txt`;
|
|
256
|
+
}
|
|
257
|
+
export function attachmentContentType(attachment) {
|
|
258
|
+
return stringValue(attachment.contentType) ||
|
|
259
|
+
stringValue(attachment.mimeType) ||
|
|
260
|
+
stringValue(attachment.mimetype) ||
|
|
261
|
+
"application/octet-stream";
|
|
262
|
+
}
|
|
263
|
+
function uniqueAttachmentPath(attachment, index, usedPaths) {
|
|
264
|
+
const explicit = stringValue(attachment.path);
|
|
265
|
+
const basePath = explicit
|
|
266
|
+
? attachmentResourcePath({ path: explicit })
|
|
267
|
+
: `/files/original/${sanitizeResourcePath(attachmentFilename(attachment, index))}`;
|
|
268
|
+
const normalized = normalizeResourcePath(basePath);
|
|
269
|
+
if (!usedPaths.has(normalized)) {
|
|
270
|
+
usedPaths.add(normalized);
|
|
271
|
+
return normalized;
|
|
272
|
+
}
|
|
273
|
+
const dot = normalized.lastIndexOf(".");
|
|
274
|
+
const suffix = `-${index + 1}`;
|
|
275
|
+
const candidate = dot > "/files/original/".length
|
|
276
|
+
? `${normalized.slice(0, dot)}${suffix}${normalized.slice(dot)}`
|
|
277
|
+
: `${normalized}${suffix}`;
|
|
278
|
+
usedPaths.add(candidate);
|
|
279
|
+
return candidate;
|
|
280
|
+
}
|
|
281
|
+
function attachmentFilename(attachment, index) {
|
|
282
|
+
const name = stringValue(attachment.filename) ||
|
|
283
|
+
stringValue(attachment.name) ||
|
|
284
|
+
stringValue(attachment.displayName) ||
|
|
285
|
+
stringValue(attachment.title) ||
|
|
286
|
+
stringValue(attachment.id);
|
|
287
|
+
if (name)
|
|
288
|
+
return name;
|
|
289
|
+
const kind = stringValue(attachment.kind) || "file";
|
|
290
|
+
return `attachment-${index + 1}${extensionForContentType(attachmentContentType(attachment), kind)}`;
|
|
291
|
+
}
|
|
292
|
+
function extensionForContentType(contentType, kind) {
|
|
293
|
+
if (contentType === "text/markdown")
|
|
294
|
+
return ".md";
|
|
295
|
+
if (contentType === "text/plain")
|
|
296
|
+
return ".txt";
|
|
297
|
+
if (contentType === "application/json")
|
|
298
|
+
return ".json";
|
|
299
|
+
if (contentType === "application/pdf")
|
|
300
|
+
return ".pdf";
|
|
301
|
+
if (contentType === "application/zip")
|
|
302
|
+
return ".zip";
|
|
303
|
+
if (contentType === "image/png")
|
|
304
|
+
return ".png";
|
|
305
|
+
if (contentType === "image/jpeg")
|
|
306
|
+
return ".jpg";
|
|
307
|
+
if (contentType === "image/heic" || contentType === "image/heif")
|
|
308
|
+
return ".heic";
|
|
309
|
+
if (kind === "image")
|
|
310
|
+
return ".img";
|
|
311
|
+
if (kind === "audio")
|
|
312
|
+
return ".audio";
|
|
313
|
+
if (kind === "video")
|
|
314
|
+
return ".video";
|
|
315
|
+
return "";
|
|
316
|
+
}
|
|
317
|
+
function responseContentType(responseType, hintedType) {
|
|
318
|
+
const contentType = responseType?.split(";")[0]?.trim();
|
|
319
|
+
return contentType || hintedType || "application/octet-stream";
|
|
320
|
+
}
|
|
321
|
+
async function maybeExtractZipAttachment(input) {
|
|
322
|
+
if (!isZipAttachment(input.contentType, input.archiveFilename, input.bytes))
|
|
323
|
+
return { extractedFiles: [] };
|
|
324
|
+
const zipRoot = `/files/extracted/${zipRootName(input.archiveFilename)}`;
|
|
325
|
+
try {
|
|
326
|
+
const entries = extractZipEntries(input.bytes, input.env);
|
|
327
|
+
const extractedFiles = [];
|
|
328
|
+
let skipped = 0;
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
if (entry.skipped) {
|
|
331
|
+
skipped += 1;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const contentType = contentTypeFromFilename(entry.path);
|
|
335
|
+
const path = `${zipRoot}/${entry.path}`;
|
|
336
|
+
const blob = await input.blob.put(`attachments/${input.run.id}/extracted/${zipRoot.slice("/files/extracted/".length)}/${entry.path}`, entry.bytes, contentType);
|
|
337
|
+
await input.state.recordFileIndex({
|
|
338
|
+
runId: input.run.id,
|
|
339
|
+
path,
|
|
340
|
+
kind: "file",
|
|
341
|
+
blobKey: blob.key,
|
|
342
|
+
sha256: blob.sha256,
|
|
343
|
+
sizeBytes: blob.size,
|
|
344
|
+
metadata: {
|
|
345
|
+
channel: input.channel,
|
|
346
|
+
source: "zip-extraction",
|
|
347
|
+
provider: stringValue(input.attachment.provider) || null,
|
|
348
|
+
archivePath: input.archivePath,
|
|
349
|
+
archiveFilename: input.archiveFilename,
|
|
350
|
+
contentType
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
extractedFiles.push({
|
|
354
|
+
path,
|
|
355
|
+
displayName: entry.path,
|
|
356
|
+
contentType,
|
|
357
|
+
sizeBytes: blob.size,
|
|
358
|
+
sha256: blob.sha256,
|
|
359
|
+
status: "extracted"
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
await input.state.appendEvent(input.run.id, "attachment.zip_extracted", {
|
|
363
|
+
archivePath: input.archivePath,
|
|
364
|
+
extractedPath: zipRoot,
|
|
365
|
+
extractedFiles: extractedFiles.length,
|
|
366
|
+
skippedEntries: skipped,
|
|
367
|
+
sizeBytes: input.bytes.byteLength
|
|
368
|
+
});
|
|
369
|
+
return { extractedPath: zipRoot, extractedFiles };
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
373
|
+
await input.state.appendEvent(input.run.id, "attachment.zip_extract_failed", {
|
|
374
|
+
archivePath: input.archivePath,
|
|
375
|
+
error: message
|
|
376
|
+
});
|
|
377
|
+
return { extractedPath: zipRoot, extractedFiles: [], errorMessage: message };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function isZipAttachment(contentType, filename, bytes) {
|
|
381
|
+
const normalized = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
382
|
+
return normalized === "application/zip" ||
|
|
383
|
+
normalized === "application/x-zip-compressed" ||
|
|
384
|
+
filename.toLowerCase().endsWith(".zip") ||
|
|
385
|
+
(bytes.byteLength >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04);
|
|
386
|
+
}
|
|
387
|
+
function extractZipEntries(bytes, env) {
|
|
388
|
+
const buffer = Buffer.from(bytes);
|
|
389
|
+
const eocdOffset = findEndOfCentralDirectory(buffer);
|
|
390
|
+
if (eocdOffset < 0)
|
|
391
|
+
throw new Error("ZIP central directory was not found.");
|
|
392
|
+
const entryCount = buffer.readUInt16LE(eocdOffset + 10);
|
|
393
|
+
const centralDirectorySize = buffer.readUInt32LE(eocdOffset + 12);
|
|
394
|
+
const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16);
|
|
395
|
+
if (entryCount === 0xffff || centralDirectorySize === 0xffffffff || centralDirectoryOffset === 0xffffffff) {
|
|
396
|
+
throw new Error("ZIP64 archives are not supported by built-in intake extraction.");
|
|
397
|
+
}
|
|
398
|
+
const maxFiles = boundedPositiveIntEnv(env.OPENEVE_ZIP_MAX_FILES, DEFAULT_ZIP_MAX_FILES, 1, 10_000);
|
|
399
|
+
const maxTotalBytes = boundedPositiveIntEnv(env.OPENEVE_ZIP_MAX_TOTAL_BYTES, DEFAULT_ZIP_MAX_TOTAL_BYTES, 1, 2 * 1024 * 1024 * 1024);
|
|
400
|
+
const entries = [];
|
|
401
|
+
let offset = centralDirectoryOffset;
|
|
402
|
+
let extractedFiles = 0;
|
|
403
|
+
let totalBytes = 0;
|
|
404
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
405
|
+
if (offset + 46 > buffer.byteLength || buffer.readUInt32LE(offset) !== 0x02014b50) {
|
|
406
|
+
throw new Error("ZIP central directory is malformed.");
|
|
407
|
+
}
|
|
408
|
+
const flags = buffer.readUInt16LE(offset + 8);
|
|
409
|
+
const method = buffer.readUInt16LE(offset + 10);
|
|
410
|
+
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
411
|
+
const uncompressedSize = buffer.readUInt32LE(offset + 24);
|
|
412
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
413
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
414
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
415
|
+
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
416
|
+
const rawName = buffer.subarray(offset + 46, offset + 46 + nameLength).toString((flags & 0x0800) !== 0 ? "utf8" : "utf8");
|
|
417
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
418
|
+
const safePath = safeZipEntryPath(rawName);
|
|
419
|
+
if (!safePath || rawName.endsWith("/") || (flags & 0x0001) !== 0 || (method !== 0 && method !== 8)) {
|
|
420
|
+
entries.push({ skipped: true });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff || localHeaderOffset === 0xffffffff) {
|
|
424
|
+
entries.push({ skipped: true });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
extractedFiles += 1;
|
|
428
|
+
if (extractedFiles > maxFiles)
|
|
429
|
+
throw new Error(`ZIP extraction exceeds max files (${maxFiles}).`);
|
|
430
|
+
totalBytes += uncompressedSize;
|
|
431
|
+
if (totalBytes > maxTotalBytes)
|
|
432
|
+
throw new Error(`ZIP extraction exceeds max bytes (${maxTotalBytes}).`);
|
|
433
|
+
const entryBytes = readZipEntry(buffer, localHeaderOffset, compressedSize, uncompressedSize, method);
|
|
434
|
+
entries.push({ path: safePath, bytes: entryBytes });
|
|
435
|
+
}
|
|
436
|
+
return entries;
|
|
437
|
+
}
|
|
438
|
+
function findEndOfCentralDirectory(buffer) {
|
|
439
|
+
const min = Math.max(0, buffer.byteLength - 65_557);
|
|
440
|
+
for (let offset = buffer.byteLength - 22; offset >= min; offset -= 1) {
|
|
441
|
+
if (buffer.readUInt32LE(offset) === 0x06054b50)
|
|
442
|
+
return offset;
|
|
443
|
+
}
|
|
444
|
+
return -1;
|
|
445
|
+
}
|
|
446
|
+
function readZipEntry(buffer, localHeaderOffset, compressedSize, uncompressedSize, method) {
|
|
447
|
+
if (localHeaderOffset + 30 > buffer.byteLength || buffer.readUInt32LE(localHeaderOffset) !== 0x04034b50) {
|
|
448
|
+
throw new Error("ZIP local file header is malformed.");
|
|
449
|
+
}
|
|
450
|
+
const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
|
|
451
|
+
const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
|
|
452
|
+
const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength;
|
|
453
|
+
const dataEnd = dataStart + compressedSize;
|
|
454
|
+
if (dataEnd > buffer.byteLength)
|
|
455
|
+
throw new Error("ZIP entry data exceeds archive size.");
|
|
456
|
+
const compressed = buffer.subarray(dataStart, dataEnd);
|
|
457
|
+
const output = method === 0 ? Buffer.from(compressed) : inflateRawSync(compressed);
|
|
458
|
+
if (output.byteLength !== uncompressedSize) {
|
|
459
|
+
throw new Error("ZIP entry expanded to an unexpected size.");
|
|
460
|
+
}
|
|
461
|
+
return output;
|
|
462
|
+
}
|
|
463
|
+
function safeZipEntryPath(name) {
|
|
464
|
+
const normalized = name.replace(/\\/g, "/").replace(/\0/g, "");
|
|
465
|
+
if (!normalized || /^[A-Za-z]:/u.test(normalized))
|
|
466
|
+
return undefined;
|
|
467
|
+
const parts = normalized
|
|
468
|
+
.split("/")
|
|
469
|
+
.filter((part) => part && part !== ".")
|
|
470
|
+
.map((part) => part.trim());
|
|
471
|
+
if (parts.length === 0 || parts.some((part) => part === ".."))
|
|
472
|
+
return undefined;
|
|
473
|
+
return parts.map((part) => part.replace(/[^A-Za-z0-9._ -]/gu, "_")).join("/");
|
|
474
|
+
}
|
|
475
|
+
function zipRootName(filename) {
|
|
476
|
+
const clean = filename.replace(/\.zip$/iu, "") || "archive";
|
|
477
|
+
return sanitizeResourcePath(clean);
|
|
478
|
+
}
|
|
479
|
+
function contentTypeFromFilename(filename) {
|
|
480
|
+
const lower = filename.toLowerCase();
|
|
481
|
+
if (lower.endsWith(".md") || lower.endsWith(".markdown"))
|
|
482
|
+
return "text/markdown";
|
|
483
|
+
if (lower.endsWith(".txt") || lower.endsWith(".log"))
|
|
484
|
+
return "text/plain";
|
|
485
|
+
if (lower.endsWith(".json"))
|
|
486
|
+
return "application/json";
|
|
487
|
+
if (lower.endsWith(".csv"))
|
|
488
|
+
return "text/csv";
|
|
489
|
+
if (lower.endsWith(".tsv"))
|
|
490
|
+
return "text/tab-separated-values";
|
|
491
|
+
if (lower.endsWith(".xml"))
|
|
492
|
+
return "application/xml";
|
|
493
|
+
if (lower.endsWith(".html") || lower.endsWith(".htm"))
|
|
494
|
+
return "text/html";
|
|
495
|
+
if (lower.endsWith(".css"))
|
|
496
|
+
return "text/css";
|
|
497
|
+
if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs"))
|
|
498
|
+
return "application/javascript";
|
|
499
|
+
if (lower.endsWith(".ts") || lower.endsWith(".tsx"))
|
|
500
|
+
return "application/typescript";
|
|
501
|
+
if (lower.endsWith(".zip"))
|
|
502
|
+
return "application/zip";
|
|
503
|
+
if (lower.endsWith(".png"))
|
|
504
|
+
return "image/png";
|
|
505
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
|
|
506
|
+
return "image/jpeg";
|
|
507
|
+
if (lower.endsWith(".gif"))
|
|
508
|
+
return "image/gif";
|
|
509
|
+
if (lower.endsWith(".webp"))
|
|
510
|
+
return "image/webp";
|
|
511
|
+
if (lower.endsWith(".pdf"))
|
|
512
|
+
return "application/pdf";
|
|
513
|
+
return "application/octet-stream";
|
|
514
|
+
}
|
|
515
|
+
function inlineAttachmentContents(attachment) {
|
|
516
|
+
const encoding = stringValue(attachment.encoding).toLowerCase();
|
|
517
|
+
for (const key of ["content", "text", "body", "data"]) {
|
|
518
|
+
const value = attachment[key];
|
|
519
|
+
if (typeof value !== "string")
|
|
520
|
+
continue;
|
|
521
|
+
if (encoding === "base64")
|
|
522
|
+
return Buffer.from(value, "base64");
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
const dataBase64 = stringValue(attachment.dataBase64) || stringValue(attachment.base64);
|
|
526
|
+
if (dataBase64)
|
|
527
|
+
return Buffer.from(dataBase64, "base64");
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
function inlineAttachmentSize(attachment) {
|
|
531
|
+
const contents = inlineAttachmentContents(attachment);
|
|
532
|
+
if (contents === undefined)
|
|
533
|
+
return undefined;
|
|
534
|
+
return typeof contents === "string" ? Buffer.byteLength(contents) : contents.byteLength;
|
|
535
|
+
}
|
|
536
|
+
function hasAttachmentContents(attachment) {
|
|
537
|
+
return inlineAttachmentContents(attachment) !== undefined || typeof attachment.blobKey === "string";
|
|
538
|
+
}
|
|
539
|
+
export function isTextualContentType(contentType) {
|
|
540
|
+
const normalized = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
541
|
+
return normalized.startsWith("text/") ||
|
|
542
|
+
normalized === "application/json" ||
|
|
543
|
+
normalized === "application/xml" ||
|
|
544
|
+
normalized === "application/javascript" ||
|
|
545
|
+
normalized === "application/typescript" ||
|
|
546
|
+
normalized.endsWith("+json") ||
|
|
547
|
+
normalized.endsWith("+xml");
|
|
548
|
+
}
|
|
549
|
+
async function attachmentDownloadRequest(attachment, input) {
|
|
550
|
+
if (input.resolveAttachment) {
|
|
551
|
+
// The turn's own channel module resolves provider-specific downloads
|
|
552
|
+
// (auth lookups, host allowlists, credential headers).
|
|
553
|
+
const resolved = await input.resolveAttachment(attachment);
|
|
554
|
+
if (resolved) {
|
|
555
|
+
const request = { url: resolved.url, headers: resolved.headers };
|
|
556
|
+
if (input.trustedProvider)
|
|
557
|
+
request.provider = input.trustedProvider;
|
|
558
|
+
const filename = resolved.filename || stringValue(attachment.filename) || stringValue(attachment.name);
|
|
559
|
+
if (filename)
|
|
560
|
+
request.filename = filename;
|
|
561
|
+
return request;
|
|
562
|
+
}
|
|
563
|
+
// The channel declined the attachment: fall back to a generic,
|
|
564
|
+
// unauthenticated URL download so plain public links keep working.
|
|
565
|
+
const url = attachmentRemoteUrl(attachment);
|
|
566
|
+
if (!url)
|
|
567
|
+
return undefined;
|
|
568
|
+
const request = { url, headers: {} };
|
|
569
|
+
const provider = stringValue(attachment.provider);
|
|
570
|
+
if (provider)
|
|
571
|
+
request.provider = provider;
|
|
572
|
+
const filename = stringValue(attachment.filename) || stringValue(attachment.name);
|
|
573
|
+
if (filename)
|
|
574
|
+
request.filename = filename;
|
|
575
|
+
return request;
|
|
576
|
+
}
|
|
577
|
+
// Direct `run({ trustedAttachmentProvider })` callers get the in-runtime
|
|
578
|
+
// provider resolvers (files.info lookups, host allowlists, credential
|
|
579
|
+
// headers). Anything else falls back to a generic, unauthenticated
|
|
580
|
+
// public-URL download — provider credentials always require either a
|
|
581
|
+
// channel resolver or an explicit trusted provider.
|
|
582
|
+
if (input.trustedProvider) {
|
|
583
|
+
return trustedProviderDownloadRequest(attachment, input);
|
|
584
|
+
}
|
|
585
|
+
const url = attachmentRemoteUrl(attachment);
|
|
586
|
+
if (!url)
|
|
587
|
+
return undefined;
|
|
588
|
+
const request = { url, headers: {} };
|
|
589
|
+
const provider = stringValue(attachment.provider);
|
|
590
|
+
if (provider)
|
|
591
|
+
request.provider = provider;
|
|
592
|
+
const filename = stringValue(attachment.filename) || stringValue(attachment.name);
|
|
593
|
+
if (filename)
|
|
594
|
+
request.filename = filename;
|
|
595
|
+
return request;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Extract the first downloadable http(s) URL a channel attachment advertises.
|
|
599
|
+
* Exported for channel modules implementing `resolveAttachment`.
|
|
600
|
+
*/
|
|
601
|
+
export function attachmentRemoteUrl(attachment) {
|
|
602
|
+
return attachmentDownloadUrl(attachment);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* True when the attachment's own trust markers (`remote.trusted` plus a
|
|
606
|
+
* matching provider/auth hint) claim the given provider. Channel modules use
|
|
607
|
+
* this in `resolveAttachment` before attaching credentials, so forged
|
|
608
|
+
* attachments never receive another provider's tokens.
|
|
609
|
+
*/
|
|
610
|
+
export function attachmentTrustedForProvider(attachment, provider) {
|
|
611
|
+
return trustedProviderForAttachment(attachment, provider) !== undefined;
|
|
612
|
+
}
|
|
613
|
+
function attachmentDownloadUrl(attachment) {
|
|
614
|
+
const remote = recordValue(attachment.remote) ?? recordValue(attachment.download);
|
|
615
|
+
const candidates = [
|
|
616
|
+
remote?.downloadUrl,
|
|
617
|
+
remote?.contentUrl,
|
|
618
|
+
attachment.downloadUrl,
|
|
619
|
+
attachment.url_private_download,
|
|
620
|
+
remote?.url_private_download,
|
|
621
|
+
attachment.url_private,
|
|
622
|
+
remote?.url_private,
|
|
623
|
+
attachment.contentUrl,
|
|
624
|
+
attachment.mediaUrl,
|
|
625
|
+
attachment.fileUrl,
|
|
626
|
+
attachment.url,
|
|
627
|
+
remote?.url,
|
|
628
|
+
attachment.href
|
|
629
|
+
];
|
|
630
|
+
return firstDownloadableUrl(candidates);
|
|
631
|
+
}
|
|
632
|
+
/** Exported for channel modules and the transitional legacy resolver module. */
|
|
633
|
+
export function firstDownloadableUrl(candidates) {
|
|
634
|
+
for (const candidate of candidates) {
|
|
635
|
+
const url = stringValue(candidate);
|
|
636
|
+
if (isDownloadableHttpUrl(url))
|
|
637
|
+
return url;
|
|
638
|
+
}
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
function isDownloadableHttpUrl(value) {
|
|
642
|
+
if (!value)
|
|
643
|
+
return false;
|
|
644
|
+
try {
|
|
645
|
+
const url = new URL(value);
|
|
646
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
export function trustedProviderForAttachment(attachment, trustedProvider) {
|
|
653
|
+
const normalizedTrustedProvider = normalizeAttachmentProvider(trustedProvider);
|
|
654
|
+
if (!normalizedTrustedProvider)
|
|
655
|
+
return undefined;
|
|
656
|
+
const remote = recordValue(attachment.remote) ?? recordValue(attachment.download);
|
|
657
|
+
if (!remoteTrusted(remote))
|
|
658
|
+
return undefined;
|
|
659
|
+
const providers = [
|
|
660
|
+
attachment.provider,
|
|
661
|
+
attachment.remoteProvider,
|
|
662
|
+
remote?.provider,
|
|
663
|
+
remote?.auth,
|
|
664
|
+
remote?.authProvider,
|
|
665
|
+
attachment.authProvider
|
|
666
|
+
].map((value) => normalizeAttachmentProvider(stringValue(value)));
|
|
667
|
+
return providers.includes(normalizedTrustedProvider) ? normalizedTrustedProvider : undefined;
|
|
668
|
+
}
|
|
669
|
+
function remoteTrusted(remote) {
|
|
670
|
+
return remote?.trusted === true || stringValue(remote?.trusted) === "true";
|
|
671
|
+
}
|
|
672
|
+
export function normalizeAttachmentProvider(value) {
|
|
673
|
+
if (!value)
|
|
674
|
+
return undefined;
|
|
675
|
+
const provider = value.toLowerCase();
|
|
676
|
+
if (provider === "photon-spectrum")
|
|
677
|
+
return "photon";
|
|
678
|
+
if (provider === "microsoft-teams")
|
|
679
|
+
return "teams";
|
|
680
|
+
if (provider === "slack" || provider === "telegram" || provider === "photon" || provider === "teams")
|
|
681
|
+
return provider;
|
|
682
|
+
return provider;
|
|
683
|
+
}
|
|
684
|
+
function blobUriForContext(blob) {
|
|
685
|
+
return blob.visibility === "public" ? { blobUri: blob.uri } : {};
|
|
686
|
+
}
|
|
687
|
+
async function fetchAttachmentBytes(fetchImpl, request, env) {
|
|
688
|
+
const maxBytes = boundedPositiveIntEnv(env.OPENEVE_ATTACHMENT_MAX_BYTES, DEFAULT_ATTACHMENT_MAX_BYTES, 1, 1024 * 1024 * 1024);
|
|
689
|
+
const init = { signal: timeoutSignal(attachmentFetchTimeoutMs(env)) };
|
|
690
|
+
if (request.headers)
|
|
691
|
+
init.headers = request.headers;
|
|
692
|
+
const response = await fetchImpl(request.url, init);
|
|
693
|
+
if (!response.ok)
|
|
694
|
+
throw new Error(`Attachment download failed: ${response.status}`);
|
|
695
|
+
const bytes = await readResponseBytesLimited(response, maxBytes);
|
|
696
|
+
const contentType = response.headers.get("content-type") ?? undefined;
|
|
697
|
+
if (isSlackHtmlDownloadPage(request, contentType, bytes)) {
|
|
698
|
+
throw new Error("Slack returned an HTML file page instead of the private file bytes. Pass Slack url_private_download or a Slack file id with SLACK_BOT_TOKEN/SLACK_USER_TOKEN.");
|
|
699
|
+
}
|
|
700
|
+
const out = { bytes };
|
|
701
|
+
if (contentType)
|
|
702
|
+
out.contentType = contentType;
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
function isSlackHtmlDownloadPage(request, contentType, bytes) {
|
|
706
|
+
if (request.provider !== "slack")
|
|
707
|
+
return false;
|
|
708
|
+
const normalized = contentType?.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
709
|
+
if (normalized !== "text/html")
|
|
710
|
+
return false;
|
|
711
|
+
const filename = request.filename?.toLowerCase() ?? "";
|
|
712
|
+
if (!filename.endsWith(".zip"))
|
|
713
|
+
return false;
|
|
714
|
+
return Buffer.from(bytes.subarray(0, 512)).toString("utf8").toLowerCase().includes("<html");
|
|
715
|
+
}
|
|
716
|
+
async function readResponseBytesLimited(response, maxBytes) {
|
|
717
|
+
const reader = response.body?.getReader();
|
|
718
|
+
if (!reader) {
|
|
719
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
720
|
+
if (buffer.byteLength > maxBytes)
|
|
721
|
+
throw new Error(`Attachment exceeds max bytes (${maxBytes}).`);
|
|
722
|
+
return buffer;
|
|
723
|
+
}
|
|
724
|
+
const chunks = [];
|
|
725
|
+
let total = 0;
|
|
726
|
+
while (true) {
|
|
727
|
+
const { done, value } = await reader.read();
|
|
728
|
+
if (done)
|
|
729
|
+
break;
|
|
730
|
+
if (!value)
|
|
731
|
+
continue;
|
|
732
|
+
total += value.byteLength;
|
|
733
|
+
if (total > maxBytes) {
|
|
734
|
+
await reader.cancel().catch(() => undefined);
|
|
735
|
+
throw new Error(`Attachment exceeds max bytes (${maxBytes}).`);
|
|
736
|
+
}
|
|
737
|
+
chunks.push(value);
|
|
738
|
+
}
|
|
739
|
+
const bytes = new Uint8Array(total);
|
|
740
|
+
let offset = 0;
|
|
741
|
+
for (const chunk of chunks) {
|
|
742
|
+
bytes.set(chunk, offset);
|
|
743
|
+
offset += chunk.byteLength;
|
|
744
|
+
}
|
|
745
|
+
return bytes;
|
|
746
|
+
}
|
|
747
|
+
function sanitizeAttachmentForContext(attachment) {
|
|
748
|
+
const blocked = new Set([
|
|
749
|
+
"authorization",
|
|
750
|
+
"headers",
|
|
751
|
+
"token",
|
|
752
|
+
"remote",
|
|
753
|
+
"download",
|
|
754
|
+
"downloadUrl",
|
|
755
|
+
"contentUrl",
|
|
756
|
+
"mediaUrl",
|
|
757
|
+
"fileUrl",
|
|
758
|
+
"dataBase64",
|
|
759
|
+
"base64",
|
|
760
|
+
"url",
|
|
761
|
+
"url_private_download",
|
|
762
|
+
"url_private",
|
|
763
|
+
"href",
|
|
764
|
+
"authProvider",
|
|
765
|
+
"remoteProvider",
|
|
766
|
+
// Blob pointers must always be runtime-issued: a forged inbound blobKey
|
|
767
|
+
// (or a nested extractedFiles entry carrying one) would otherwise let a
|
|
768
|
+
// crafted payload read arbitrary blobs in the shared storage root.
|
|
769
|
+
// Callers re-attach the runtime's own pointers after sanitization.
|
|
770
|
+
"blobKey",
|
|
771
|
+
"blobUri",
|
|
772
|
+
"extractedFiles"
|
|
773
|
+
]);
|
|
774
|
+
const out = {};
|
|
775
|
+
for (const [key, value] of Object.entries(attachment)) {
|
|
776
|
+
if (blocked.has(key))
|
|
777
|
+
continue;
|
|
778
|
+
out[key] = safeJson(value);
|
|
779
|
+
}
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
782
|
+
function timeoutSignal(timeoutMs) {
|
|
783
|
+
return AbortSignal.timeout(timeoutMs);
|
|
784
|
+
}
|
|
785
|
+
/** Attachment download timeout (`OPENEVE_ATTACHMENT_FETCH_TIMEOUT_MS`). Exported for channel `resolveAttachment` lookups. */
|
|
786
|
+
export function attachmentFetchTimeoutMs(env) {
|
|
787
|
+
return boundedPositiveIntEnv(env.OPENEVE_ATTACHMENT_FETCH_TIMEOUT_MS, DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS, 1_000, 120_000);
|
|
788
|
+
}
|
|
789
|
+
function boundedPositiveIntEnv(value, fallback, min, max) {
|
|
790
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
791
|
+
if (!Number.isFinite(parsed))
|
|
792
|
+
return fallback;
|
|
793
|
+
return Math.min(max, Math.max(min, parsed));
|
|
794
|
+
}
|
|
795
|
+
function normalizeResourcePath(path) {
|
|
796
|
+
const normalized = path.replace(/\\/g, "/").replace(/\/+/g, "/").trim();
|
|
797
|
+
const absolute = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
798
|
+
if (absolute.split("/").some((part) => part === "..")) {
|
|
799
|
+
throw new Error(`Invalid resource path: ${path}`);
|
|
800
|
+
}
|
|
801
|
+
return absolute === "/" ? "/" : absolute.replace(/\/$/u, "");
|
|
802
|
+
}
|
|
803
|
+
function sanitizeResourcePath(path) {
|
|
804
|
+
return path
|
|
805
|
+
.replace(/\\/g, "/")
|
|
806
|
+
.replace(/^\/+/u, "")
|
|
807
|
+
.split("/")
|
|
808
|
+
.filter((part) => part && part !== "." && part !== "..")
|
|
809
|
+
.map((part) => part.replace(/[^A-Za-z0-9._-]/gu, "_"))
|
|
810
|
+
.join("/") || "resource";
|
|
811
|
+
}
|
|
812
|
+
function stringValue(value) {
|
|
813
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
814
|
+
return String(value);
|
|
815
|
+
return typeof value === "string" ? value.trim() : "";
|
|
816
|
+
}
|
|
817
|
+
function numberValue(value) {
|
|
818
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
819
|
+
}
|
|
820
|
+
function recordValue(value) {
|
|
821
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value)) ? value : undefined;
|
|
822
|
+
}
|
|
823
|
+
function safeJson(value) {
|
|
824
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
825
|
+
}
|
|
826
|
+
function hashJson(value) {
|
|
827
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
828
|
+
}
|
|
829
|
+
function sha256(contents) {
|
|
830
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
831
|
+
}
|
|
832
|
+
//# sourceMappingURL=attachments.js.map
|