The fallback name was never checked against the names already taken, so an
entry called exactly what the fallback produces took the same file: `media/1/a:b.png`,
`media/2/a_b.png` and `media/3/2-a_b.png` extracted to five files for six
entries, the third overwriting the second, and both items' footage pointing at
it. Deduplication now runs on the name actually written and counts up until it
is free.
The collision predates making the names safe (two entries named `a_b.png` and
`2-a_b.png` collide on their own), but sanitising gives more ways in, so it
belongs with it.
Found by CodeRabbit on the pull request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`file.createProxy` and `renderQueue.add` read a single string (`str_p`), so
`{path: ["…/a.mov"]}` leaves them on their default while the confinement saw a
path it could check and let the queue go ahead: on an unsaved project that was
`~/Movies/A_proxy.mov` again. Only a plain string now counts as an output the
agent named.
Found by CodeRabbit on the pull request, which reported it fixed by 9cc23209;
it was not, and the case is in the test now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dropping the points a later stroke of the other kind painted over could leave
a stroke with none, even though most of it was still painted: a small Alt tap
in the middle of a large foreground dot removed its only point, and the
selected model was skipped for the classical cut. Points are now kept by the
resolved ownership under them, and a stroke that loses every sampled point
but still owns pixels is prompted from the one deepest inside them. A stroke
painted over completely still prompts nothing.
Green, Alt (red) over part of the object, then green there again, as the
Layer panel sends them through roto.stroke: the render follows the last
stroke, and Undo / Redo step between the two results.
Segmentations and rendered frames are keyed by their strokes, which now
segment differently where strokes overlap; bumping both versions keeps old
entries from showing the old result. Frozen mattes are stored in the project
and are untouched.
A trained model is no longer prompted with both labels at one spot: points
along a stroke that a later stroke of the other kind painted over are
dropped, and the propagated matte's points, box and mask take this frame's
strokes over it.
A Refine Edge stroke over an earlier Alt (erase) stroke now restores the band
there; the band carried from the previous frame stays under this frame's
strokes.
A green stroke over an earlier Alt (red) stroke now restores the area: where
strokes overlap, the one painted last sets the hard constraint, for the cut,
the colour samples and the cleanup seeds alike.
`render.preRender` takes an Output Module template where a path goes, and the
confinement skips the path check for one. That made `given` true, so the new
lookup was skipped — and the template's own output is relative, so it resolved
exactly like an output the agent never gave: `render.preRender {output:
"Lossless"}` on an unsaved project queued `~/Movies/Lossless.mov` and
`~/Movies/Lossless_RenderLog.txt`.
The lookup now asks whether a *file* was named, so a template name takes the
same route as no output at all.
Found by CodeRabbit on the pull request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Prefs::set` makes a JSON pointer of the key (`key.replace('.', "/")`), so
`scripting/allowScriptsWriteFiles` resolves as well as the dotted spelling, and
the page on its own (`{key: "scripting", value: {…}}`) replaces every setting on
it — `coerce` takes an object for an object, and the page deserializes with
`serde(default)`. A prefix check on `scripting.` let both through. The page is
now read from the key's first segment, split on either separator.
Found by CodeRabbit on the pull request; reproduced before fixing, with
`prefs.set {key: "scripting/allowScriptsWriteFiles", value: true}` answering
`{"scripting/allowScriptsWriteFiles": true}` under confinement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EffectCraft writes its own templates as `media/<item id>/<file>` entries, and
extraction trusted both halves. `base_name` splits on `/` and `\`, but the item
id was taken with `split('/')` alone, and it is pasted in front of a base name
another item already used. So a second `media/..\..\..\Startup\x/clip.mp4`
entry became `<dir>/..\..\..\Startup\x-clip.mp4`, which on Windows is three
folders up, with the attacker choosing how the name ends.
The id is now used only when it is one, and the name goes through
`safe_file_name`: separators, drive colons and NUL become `_`, and `.` / `..`
become `footage`. Reached from `templates.create` with `path`, from File ▸ New
from Template, and from a template dropped in the user Templates folder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Layer is now a callable function with its own prototype, which costs
more to make than the plain object it was. __layer(c, l) makes each
layer once per evaluation (kept in __S, which __begin resets), so an
expression naming the same layer many times builds it once. That covers
thisComp.layer(...) in a loop, .parent, a Layer Control's value and
thisLayer.
The checks for a layer (the result, thisComp.layer(layer, offset), the
property proxy) read __isLayer only on functions. Numbers and other
primitives are no longer boxed to look the property up.
Timing (release, Windows, medians of 2000 evaluations, PR vs main):
- one evaluation building one layer: about 22 vs 16 us;
- thisLayer.transform.opacity: about 200 vs 170 us;
- 20000 thisComp.layer(1) lookups in one evaluation: 95 vs 138 ms.
In After Effects a layer is a property group: thisLayer("transform"),
thisComp.layer("Other")("effects")("Blur"), thisLayer(1). In
EffectCraft's expression runtime a Layer was a plain object, so those
failed with "TypeError: not a callable function", including the
references the pick whip writes with Compact English off
(thisComp.layer("Other")("effects")("ec.noise.fractal")(...)).
A Layer is now callable like a property group (built as __mkGroup
builds groups), looking up its own properties by match name, name or
index, and keeps its methods, name and index. The places that took a
layer for a plain object (the result reading as the layer's index,
thisComp.layer(layer, offset), a Layer Control's value handed through
the property proxy) accept it.
Test: layers_are_callable_property_groups (fails before).
`file.createProxy` builds a Render Queue item exactly as `renderQueue.add`
does, with `[compName]_proxy.[fileExtension]` as its output, so an unsaved
project sent it to the home folder too.
Its `path` / `output` had to join `WRITES` for this: the confinement only
knows an output was named through that table, and it was reading them as a
read before, so a proxy could be written into a read-only root (part of the
F-11 audit in #2131).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI installed no logger, so GPU warnings (a declined frame, out of
memory, fallback to the CPU) were dropped and an export silently rendered
on the CPU. Install the engine logger with a `warn` default (RUST_LOG
still overrides). Split out of #1415.
`script.run` and the Scripts menu are refused while file access is confined,
but a ScriptUI panel's script was not: `window.scriptPanel` runs it to build
the panel, `window.restoreScriptPanels` runs every remembered one,
`scriptui.click` / `set` / `mouse` / `close` dispatch events to its handlers,
and `script.runTasks` runs its `app.scheduleTask` tasks. A script reads and
writes files and reaches the network, so none of that may start while confined.
`prefs.set` is refused for the `scripting.*` settings for the same reason: the
engine already stops a script from granting itself the access the user withheld
(`Scripting::same_permissions`), and a confined agent writing it to the settings
file would hand it to every script run afterwards, long after the session.
`prefs.get` and the read-only `scriptui.list` / `scriptui.get` are untouched.
This is the "Running code" part of the F-11 audit; the other parts of #2131
are separate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`renderQueue.add` and `render.preRender` without an `output` take the default
from where the project sits, which for a project that has never been saved is
the home folder — outside every `--automation-read-root` / `--automation-write-root`.
The item was queued anyway, and only `renderQueue.render` refused it, so an agent
learned about it one command too late and with a queue it had to clean up.
Both commands now look the project up first (`Lookup::DefaultOutput`) and are
refused on an unsaved project, naming `output` as the way through. A project
saved inside the roots keeps its old behaviour: the default goes beside its file
and what it writes is checked when the queue is rendered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>