Skip to content

Release/v0.4.2 - #283

Merged
lightningpixel merged 43 commits into
mainfrom
release/v0.4.2
Aug 28, 2026
Merged

Release/v0.4.2#283
lightningpixel merged 43 commits into
mainfrom
release/v0.4.2

Conversation

@lightningpixel

Copy link
Copy Markdown
Owner

No description provided.

kairoxxai and others added 30 commits June 21, 2026 00:20
- electron 33.3.0 → 42.0.1
- electron-builder 24.13.3 → 26.8.1
- electron-vite 2.3.0 → 5.0.0
- vite 5.4.0 → 8.0.12

Closes 22 npm audit findings (12 high, 6 moderate, 4 low) including
node-tar path traversal (multiple GHSAs), postcss XSS, and
esbuild/vite transitive vulns. Build verified clean across all three
electron-vite environments (main, preload, renderer). App launches
and exercises Python bridge + three.js renderer successfully.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(extensions): allow npm lifecycle scripts on install
BaseGenerator subclasses currently hardcode torch.cuda.is_available(),
so the four bundled models only run on Windows/Linux with an NVIDIA
GPU. Add select_device()/select_dtype() to base.py so extensions can
detect Apple Silicon (MPS) and fall back to CPU in one shared place
instead of duplicating the check per extension. unload() now also
releases MPS memory via torch.mps.empty_cache().

Windows/CUDA behavior is unchanged.

Ref: #166
torch.backends.mps hard-crashes with NotImplementedError on ops that
have no Metal kernel yet (e.g. 3D pooling), instead of transparently
falling back to CPU like on other backends. The opt-in env var that
enables the fallback must be set before the process's first `import
torch` — setting it inside select_device() is too late for extensions
that import torch before calling it (confirmed by testing TripoSG end
to end). Set it once, for every extension subprocess, in
ExtensionProcess._build_env() on macOS.

Ref: #166
…tion

fix(extensions): validate model registration
Ensure argparse validation failures are reported as JSON instead of raw SystemExit/usage output, which preserves the Modly CLI machine-readable contract.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ExtensionNode's string-param browse button called
window.electron.fs.selectDirectory() unconditionally, so a manifest
asking for a file dialog with pickerIntent: "image" still got a folder
dialog. The Generate panel's copy of the same control had the identical
bug.

Route both through a shared resolver that maps a param's pickerIntent to
one of the dialogs the preload API already exposes (image, mesh, text),
falling back to the folder picker when the field is unset or holds a
value this build does not know -- so existing manifests are unaffected.
The button glyph and its accessible name now follow the resolved intent.

Refs #155
fix(extensions): honor pickerIntent on ExtensionNode string params
The new sync effects referenced identifiers that do not exist, so both
editors crashed as soon as an external save bumped updatedAt:

- WorkflowPanel: setEdges was never destructured from useEdgesState
- WorkflowsPage: setName has no counterpart in WorkflowCanvasInner
  (renaming is handled at page level via renameTarget/handleRename)

Also:

- WorkflowsPage kept the debounce cleanup despite the comment claiming
  otherwise, so switching tabs within 500 ms dropped the edit. The
  pending save is now flushed on unmount instead of cancelled.
- The saved-at claim is rolled back when the write fails, so a failed
  save no longer replaces the canvas with the last persisted version.
- handleGenerate / handleRun now claim their own save and drop the
  pending debounce, instead of triggering a resync + redundant write
  that could clobber input typed during the IPC round-trip.
- Restore the monotonic progress guard in generation.py: the loading
  phase walks the bar to 7 on a background thread and extensions then
  report their own 0-100 scale, so an unguarded assignment yanked the
  bar backwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keeps the raw line as emitted by the extension, as before #180.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix/UI state persistence across tab switches
…ror crashes

The embedded Python on Windows defaults stdout/stderr/stdin to the active
console codepage (cp1252, cp932, ...). Any Unicode print from Modly or an
extension generator (e.g. the arrow in _auto_download's progress line, tqdm
output) then crashes with UnicodeEncodeError, and reading UTF-8 worker
output under a legacy codepage can kill the stderr reader thread and
deadlock generation.

- ASCII-ify the unicode prints in BaseGenerator._auto_download and the
  registry reload message so the reported crash cannot happen regardless
  of encoding setup.
- Add services/stdio_utf8.ensure_utf8_stdio() and call it in the FastAPI
  process (api/main.py) and the extension worker (api/runner.py) so all
  Modly output is UTF-8 and matches the UTF-8 pipe readers on the
  Electron side.
- Spawn extension workers with PYTHONUTF8=1 and explicit
  encoding="utf-8", errors="replace" pipes so both ends agree on UTF-8
  even on non-UTF-8 Windows locales.
- Force PYTHONUTF8=1 for process extensions spawned by the Electron
  process runner.

Fixes #270; also addresses the deadlock root cause described in #214.
Add shared cuda→mps→cpu device-selection helper (#166)
fix(extensions): force UTF-8 stdio on Windows to stop UnicodeEncodeError crashes
The lockfile still described version 0.3.5 and was missing entries for
dependencies added since, esbuild among them. A clean `npm install`
therefore did not place esbuild in node_modules, and the ten test files
that import it failed with ERR_MODULE_NOT_FOUND -- enough to abort the
pre-push hook on any branch.

Regenerated by running npm install against the current package.json.
Windows refuses symlink creation unless the process is elevated or
Developer Mode is enabled, so this test raised EPERM on a stock Windows
checkout. The throw happened before the try block, which also leaked the
two temp directories it had just created.

An environment that cannot create a symlink cannot express the property
under test, and reporting that as a product failure blocks the pre-push
hook for every Windows contributor who has not turned Developer Mode on.
Probe the capability once and skip with a reason instead. Where symlinks
do work -- CI, macOS, Linux, elevated Windows -- the test runs exactly
as before.
Modly had no SECURITY.md and no private channel for vulnerability
reports, which left email as the only route for researchers. Private
vulnerability reporting is now enabled on the repository; this points
people at it and sets expectations around it.

The policy leads with a threat model and lets the scope follow from it,
so that an excluded report comes with the reason it was excluded. Two
assumptions are deliberate: workflow files are untrusted input because
sharing them is normal, and any web page the user has open is an
untrusted caller of the loopback API. The second is why the
network-exposure exclusion is narrowed to deliberate exposure only --
a page in the user's own browser needs none.

Every claim was checked against the code. The policy does not call the
installer signed (no platform signs it), says nothing about PyTorch
(we do not ship it), and does not excuse social engineering on the
strength of UI warnings that do not exist.
fix: make the test suite pass on a stock Windows checkout
docs: add a security policy with a private reporting route
…#238)

The canonical run endpoint hardcoded the "Default" collection, while the legacy
/generate/from-image accepts a `collection` field. A run driven over REST/MCP therefore
always landed in Default/, which the Workspace Library does not index, so everything
generated headlessly existed on disk but showed as "No workspace assets are indexed yet"
in the app (cause 1 of #238).

Accept the same `collection` field on /workflow-runs/from-image and route the run into it.
The name goes through a shared `sanitize_collection()`, extracted from the legacy endpoint's
inline check rather than copied, so the two surfaces cannot drift on which characters may
name a workspace subfolder: a path separator, a drive/wildcard character, or an empty name
still falls back to "Default", because that string becomes a directory under the workspace.

This is the API half of #238. It lets a headless caller target an indexed collection (e.g.
"Exports") instead of the workaround of mirroring files by hand; the indexer roots and job
meta records (causes 2 and 3) are separate and untouched here.

Tests (api/tests/test_workflow_runs_router.py): the sanitizer keeps a plain name, refuses
path and wildcard characters, and defaults on empty; the endpoint forwards the caller's
collection to the run and neutralizes a traversing one. 6 new tests, full api suite 59 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lightningpixel and others added 13 commits August 21, 2026 23:30
# Conflicts:
#	package-lock.json
#	package.json
Advisories published after PR #145 was authored: tar (critical),
postcss, axios, electron-updater, nanoid, js-yaml, flatted,
form-data and the @babel/@esbuild trees.

Lockfile-only: every bump stays inside the semver ranges already
declared in package.json, so no dependency was changed.

npm audit: 15 -> 0
electron 42 declares engines.node ">= 22.12.0", and vite 7 /
electron-vite 5 both want "^20.19.0 || >=22.12.0". The workflows
pinned Node 20 on all six jobs.

Without engine-strict this only warns at install time, so the
mismatch would have surfaced later, during electron-builder
packaging, rather than up front.
deps: bump electron stack and resolve all 22 npm audit findings
sanitize_collection()'s character blocklist let a bare ".." through --
it contains none of the listed characters -- so WORKSPACE_DIR / ".."
resolved to the workspace's parent and the generated mesh was written
outside the sandboxed root. This PR newly routes that helper into
/workflow-runs/from-image, an MCP-reachable endpoint that previously
always used a hardcoded "Default" and could never receive
attacker-influenced input here.

Legality and containment are now asked separately: the reserved
characters are still refused outright, and containment is checked
against the resolved path rather than the spelling.

Also closes a validation gap the two endpoints had drifted apart on:
create_run_from_image accepted any remesh value, while
generate_from_image already rejected anything outside
{"quad","triangle","none"} with a 400.
- create_run_from_image validated remesh after switch_model() had already
  unloaded the previously-active generator, so a rejected request still paid
  for (and forced a reload after) evicting it. Validation now runs first,
  matching generate_from_image's own ordering.
- The valid-remesh tuple was duplicated between the two endpoints -- exactly
  the drift #238 and the collection sanitizer were about avoiding. Both now
  share routers.generation.VALID_REMESH_MODES.
- sanitize_collection's containment check now uses the same relative_to()
  test generator_registry._path_belongs_to uses, instead of a hand-rolled
  parent-equality comparison with slightly different semantics.
- A name ending in a dot (e.g. "Exports...") cleared both checks but Windows
  silently drops the trailing dots from the directory it actually creates,
  so it collided with a plain "Exports" collection on disk. Such names are
  now refused rather than silently merging two collections' output.
fix(api): file /workflow-runs/from-image into the caller's collection (#238)
The merge commit that created this PR (e5eac44) had 0f2b7ff -- a complete,
working implementation -- as one parent and dev as the other, and the merge
resolution kept dev's version of both touched files almost verbatim while
discarding 0f2b7ff's changes. What survived on top of that was a later,
independent dev addition (per-slot text routing for multi-text inputs) plus a
partial, inconsistent generalization of the handle rendering -- leaving
dangling references to code that no longer existed:

- extraImagePaths was read in two places but never declared, so any
  process-extension node (single or multi-input) threw a ReferenceError.
- The multi-image slot bug this PR claims to fix was still live: inputPaths
  was declared and then never read back, so incoming edges still overwrote a
  single nodeInputPath/nodeInputMeshPath scalar with no per-slot routing.
- processParams was built (to carry extra_image_paths) and then discarded --
  runProcess was still called with the plain liveParams.
- ioSubheader still hardcoded exactly two input rows (ioRowRef/ioRow2Ref,
  themselves no longer declared) while handlesEl next to it had already been
  generalized to N handles -- a 3rd+ input rendered a handle with no label,
  and any multi-input node crashed on the removed refs before that.

Rebuilt per-slot routing (by target handle, not by incoming-edge arrival
order) and generalized ioSubheader to N rows the same way handlesEl already
was, reusing its own handleRefs/handleTops alignment. Kept the two things dev
had added independently since 0f2b7ff: per-slot text routing
(nodeInputTexts) and live-param freshness on re-run (processParams now
spreads liveParams, matching the model-node branch, instead of the stale
node.data.params 0f2b7ff used before liveParams existed).

Verified: `npm run lint` and `tsc --noEmit` clean, `npm run test:node` green
(40/40, no existing suite covers this path).
patch/keep image inputs in order on multi-image nodes
The PR's ding sound only works while you're within earshot. The repo owner
asked for something more visible, in this same PR's thread: a native Windows
notification (macOS/Linux get their platform's equivalent for free via
Electron's Notification API).

Wired through the same main-process IPC pattern every other renderer->main
call in this app uses (electron/main/ipc-handlers.ts -> preload bridge ->
window.electron.*), rather than the renderer's own Notification API, so it
behaves the same regardless of focus and carries the app's icon. Clicking the
toast restores and focuses the window.

Fires at the same two completion points the sound already does (a single
image-to-3D job in useGeneration.ts, a workflow run in workflowRunStore.ts's
finalize()), skipped when the window already has focus -- the user is
looking right at it, so a toast on top would just be noise; the sound still
plays either way.

Verified on this branch: `npm run lint` (0 errors -- pre-existing warnings
elsewhere untouched), `tsc --noEmit` clean, and the branch's JS test suite
(34 + 3 tests) green. Note: this branch predates dev's eslint.config.mjs and
test:node script split, both unrelated to this change.
play a short ding sound when generation or workflow run completes
Removes the Web Audio ding (which also leaked an AudioContext on every
completion, since it was never closed) and lets the platform toast play
its own default sound (Windows/macOS/Linux) since the notification isn't
marked silent.
@lightningpixel
lightningpixel merged commit 8d08249 into main Aug 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants