Commit 4e78409
feat(function): mount referenced files into the code sandbox (#7251)
* feat(function): mount referenced files into the code sandbox
Reference a file's path in Function block code and it is mounted for you:
`<block.file.path>` resolves to its location on the sandbox filesystem, so
any language can open it. It is the counterpart to `.base64`, which inlines
the bytes and only works in JavaScript, leaving Python and Shell with no way
to read a referenced file at all.
Resolution happens long before a sandbox exists, and mount paths are only
assigned once the whole set is planned together, so the resolver leaves a
marker that the function runtime swaps for the real path — the same shape as
LargeValueRef.
Files written to /tmp/sim/outputs come back as platform file objects, so they
can be attached or uploaded with no intermediate step. Harvesting is automatic
on runs already in the remote sandbox; isolate runs pay nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(execution): cover the sim.* helpers in a real isolate
isolated-vm.test.ts mocks the spawn, so nothing proved the sim.* namespace
was reachable from user code — only that the process plumbing was called.
These run the real worker and assert values cross the boundary both ways.
Pins the fast runtime's global surface, which turned out narrower than
assumed: plain ECMAScript plus fetch, console and sim.*, with no Buffer,
require, process, crypto, TextDecoder, atob or setTimeout. That list is
exactly what decides whether a block needs an import and so moves to the
remote sandbox, so it is asserted rather than described.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate tool metadata and integration docs
Changing function_execute's params and adding file_write's fileInput made
both generated artifacts stale, which check:audits catches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(function): address review findings on sandbox file I/O
- Classify harvested output by content, not by file name. Gating the
provenance scan on a filename-derived MIME type let a resolved secret be
written as plaintext under a binary-looking extension and skip the only
guard before upload. Bytes that round-trip as UTF-8 are scannable
whatever they are called.
- Format the .path replacement through the shared context-aware helper.
Returning a bare identifier inserted __blockRef_N literally in Shell and
inside quoted strings instead of the mounted path.
- Enforce the mount ceiling on the combined set. Marker-derived mounts
bypassed the contract's max, which only bounded the explicit files param.
- Reuse one marker per file key, so referencing a path twice mounts once.
- Raise a coded error when the code deletes the output directory, so it
reports as a 400 with remediation rather than an opaque 500.
- Drop declared paths from the discovered set; counting a file in both
rejected a single output larger than half the byte ceiling.
- defineProperty when rebuilding context values, so an own __proto__ key
survives instead of hitting Object.prototype's setter.
- Give the directory sentinel a collision-resistant name, and cap
file_write's fileInput at the destination's own limit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(function): include files in the FAQ's output answer
Adding <function.files> to the outputs table left the FAQ still saying the
block returns two outputs, and the reference FAQ never mentioned that a
file is read by referencing its path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(function): scan every harvested output for resolved secrets
Gating the scan on whether the bytes looked textual was defeatable twice
over: name the file .png, or append a single invalid byte, and a plaintext
secret skipped the only guard before upload.
A lossy UTF-8 decode preserves ASCII runs, so a literal secret is findable
in any buffer. The scan is now unconditional. What remains out of reach is
a secret carried in transformed form, which no substring scan can see —
an inherent limit of scanning rather than a hole in the gate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(function): correct provenance, mount caps and runtime file plumbing
Review findings from the sandbox file I/O change, traced to root cause.
A file_write carrying fileInput copied its bytes into a new workspace
file without the source's secret lineage, so a file the platform had
locked as secret-derived became readable again under a new id. It now
derives provenance from the source through the same helper archiving
uses, which also marks a source with no workspace row unknown rather
than empty.
The tool half of that feature had shipped without the block half: the
File block's write mapper listed four params and dropped fileInput, and
no sub-block could supply one, so the documented binary path was
unreachable from the canvas and from an agent holding the File tool.
Adds the canonical pair, maps it, and drops content's unconditional
required now that the two are mutually exclusive sources.
The harvest counted secret names the matcher had already discarded as
too short to identify anything, so a workflow whose every secret was
under the substitutable-literal minimum built no matcher, classified
every output unknown, and refused each one while claiming it held a
secret. The counter now applies the matcher's own predicate.
The execution file index was lazily created on the per-call context
clone, so files a tool produced were recorded onto a throwaway and the
next call in the run never saw them. Materializing it on the source
before the spread makes both objects share one map.
A .path reference inside a quoted Python or JavaScript string was
JSON-encoded, putting literal quote characters inside the path the code
then opened. Mount paths are sanitized to a safe character set, so they
splice raw.
URL mounts now carry a ceiling that curl enforces on the bytes actually
served, rather than trusting a recorded size, and declared sandbox
outputs opt out of the harvest explicitly instead of relying on which
branch returns first. Also stops rebuilding non-plain context values
into stripped objects, and removes an agent tool-description branch that
no longer had a sub-block to read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(file): send one write source, and bound mounts by what they were charged
The write card sent `content` unconditionally, and the contract counts any
defined `content` as "text was provided" — so an untouched Content box,
which serializes as an empty string, collided with a selected file and
rejected every binary write. The mapper now emits only the source the card
carries, leaving the contract to catch filling both or neither.
A generated document that references other files needs a principal to
resolve them; without one the resolver can only serve an already-published
artifact and throws. The write path now passes it.
URL mounts are granted exactly the byte count they were charged against the
aggregate, rather than being charged a reported size while permitted the
global per-file maximum — twenty mounts each claiming a byte could
otherwise be allowed 500MB apiece. An honest size fetches normally and an
understated one is refused.
`--max-filesize` only refuses a transfer up front when the response
declares a Content-Length, so a chunked reply slipped past it. The
delivered file is now measured and removed if it overran, in the same
command rather than a second round trip.
Also documents that naming an explicit sandbox output path excludes the
harvest directory, in both the block docs and the tool description the
model reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(file): accept the picker shape, and bound a mount while it downloads
The write path required an already-complete UserFile, but the file picker
stores {name, path, key, size, type} with no id or url — so selecting a
file in the new basic field was rejected as not a file object before any
bytes moved. It now runs through the same normalizer every other operation
in that file applies to its own input, while a block reference or an
agent-resolved id still passes through as-is.
Writing a deliberately empty text file was also rejected, because an empty
string read as "no text". The selected file is what disambiguates now: with
one present an empty Content box means unused, and with none, content
always goes through.
--max-filesize only refuses a transfer that declares a Content-Length, so
the previous size check ran after the whole object had already been written
to sandbox disk. The fetch now streams through a byte cap, so at most one
byte over the limit can ever land, and curl's status travels through a file
so a 403 on an expired URL is still distinguishable from an empty download.
A non-finite mount size made every comparison false, so the aggregate check
passed while the mount was charged the per-file maximum anyway; the size is
resolved once now, before either test.
Mount resolution failures are the caller's files — unreadable, oversized,
or over the aggregate — and now answer 400 with the message naming the
file, rather than a 500. Files already uploaded when a later one in the
same harvest is refused are removed, since the export is all-or-nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(file): clamp a declared mount cap, and demand identity not full metadata
sandboxFiles reaches the sandbox layer from the request body, so a declared
maxBytes is a caller's number. It may now lower its own mount's ceiling but
never raise it past the one that layer guarantees.
The write path required full UserFile metadata, but size is never read
before the download and the download reports the real content type — so a
reference carrying id, key, url and name was rejected over two fields
nothing depends on. It now asks only for identity and fills the rest.
Uploads already made are also discarded when a later upload throws, not
only when a later file is refused for carrying a secret. Both exits leave
the harvest all-or-nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(file): answer null for a key a file input cannot be classified by
fileInputToUserFile normalizes caller-supplied file objects and returns
null for anything it cannot use — but it classified the storage key with
the throwing form, so a key without a recognized context prefix escaped as
a 500 from every operation that normalizes a file input, not just write.
Adds tryInferContextFromKey beside inferContextFromKey, sharing the one
prefix list so a new context cannot be added to half of them. The throwing
form stays right where an unclassifiable key means the platform built one
wrong; the nullable form is for keys that arrived in a request, where an
unrecognized prefix only means this is not a file we can use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent 62d0800 commit 4e78409
39 files changed
Lines changed: 3542 additions & 300 deletions
File tree
- apps
- docs/content/docs
- integrations
- workflows/blocks
- sim
- blocks/blocks
- executor
- variables
- lib
- api/contracts
- tools
- copilot/tools/handlers
- execution
- payloads
- remote-sandbox
- function-execution
- internal/file
- uploads/utils
- workflows
- providers
- tools
- file
- function
- generated
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
90 | | - | |
| 90 | + | |
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
97 | | - | |
98 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
99 | 100 | | |
100 | 101 | | |
101 | 102 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
105 | 148 | | |
106 | 149 | | |
107 | 150 | | |
| |||
401 | 444 | | |
402 | 445 | | |
403 | 446 | | |
404 | | - | |
405 | | - | |
| 447 | + | |
| 448 | + | |
406 | 449 | | |
407 | 450 | | |
408 | 451 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
83 | 86 | | |
84 | 87 | | |
85 | 88 | | |
| |||
921 | 924 | | |
922 | 925 | | |
923 | 926 | | |
924 | | - | |
| 927 | + | |
925 | 928 | | |
926 | 929 | | |
927 | 930 | | |
| |||
1031 | 1034 | | |
1032 | 1035 | | |
1033 | 1036 | | |
1034 | | - | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
1035 | 1056 | | |
1036 | 1057 | | |
1037 | 1058 | | |
| |||
1206 | 1227 | | |
1207 | 1228 | | |
1208 | 1229 | | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
1209 | 1242 | | |
1210 | 1243 | | |
1211 | | - | |
| 1244 | + | |
| 1245 | + | |
1212 | 1246 | | |
1213 | 1247 | | |
1214 | 1248 | | |
| |||
1432 | 1466 | | |
1433 | 1467 | | |
1434 | 1468 | | |
| 1469 | + | |
| 1470 | + | |
| 1471 | + | |
| 1472 | + | |
1435 | 1473 | | |
1436 | 1474 | | |
1437 | 1475 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
20 | 24 | | |
21 | 25 | | |
22 | 26 | | |
| |||
174 | 178 | | |
175 | 179 | | |
176 | 180 | | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
177 | 185 | | |
178 | 186 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
403 | 403 | | |
404 | 404 | | |
405 | 405 | | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
406 | 421 | | |
407 | 422 | | |
408 | 423 | | |
| |||
0 commit comments