Skip to content

SunVox Studio saves renders and projects to Crow storage, composes with an arrangement, and the artifact CSP allows WASM in pages only - #525

Merged
kh0pper merged 7 commits into
mainfrom
feat/sunvox-completion
Oct 20, 2026
Merged

kh0pper merged 7 commits into
mainfrom
feat/sunvox-completion

Conversation

@kh0pper

@kh0pper kh0pper commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Follow-up to #515. It completes the SunVox Studio MCP tools, fixes the minor points left open by its review, and narrows the artifact CSP.

What changes

sunvox_render and sunvox_compose save to Crow storage

  • When storage is available, each render and composed project is saved to Crow storage. The result includes the storage key, a URL valid for 1 hour, and the local path.
    • save_to_storage: if omitted, the file is saved whenever storage is available. true fails when storage is missing. false keeps only the local file.
    • Each file gets a storage_files row, so it appears in Files. The rules match crow_upload_file: the same MAX_UPLOAD_SIZE and STORAGE_QUOTA_MB limits, and the object is removed if its row cannot be written.
    • Keys look like sunvox-studio/<renders|projects>/<time>-<128 random bits>-<name>.
  • reference_type: "blog_post" attaches the file to a post. The file then becomes public at /blog/media/<key>, which is where a song page's SunVox player fetches it. No other reference type is accepted, because those carry project access checks that a bundle must not bypass.
  • The MCP child now reaches storage configured in the Nest (storage.shared.*, with sealed secrets). Before this change it could not, so reading by storage_key failed as well.
    • It reads the instance identity only if it already exists, using the new loadIdentityIfPresent(), which never creates one.
    • It opens the settings, zeroes the identity's key buffers straight away, and gives the s3-client only the opened config, using the new useStorageConfig(). The child never keeps the root seed.
    • The MINIO_* environment is the fallback.
  • Reading by storage_key is limited to SunVox Studio's own files (sunvox-studio/...) and blog media. Project files, attachments and private uploads are refused, so a render cannot re-publish a private file at /blog/media.
  • funkwhale_library (a UUID or a library name) uploads an mp3 or ogg render to Funkwhale, so fw_play can play it on the glasses.
    • The address and token come only from the Funkwhale add-on's own configuration, which the gateway's media relay also reads. No tool argument names a host.
    • Requests go to that one origin only and never follow redirects.
    • The whole push has one deadline: at most 35 s, and never past 55 s from the start of the tool call. If the render has used up too much of the call, the push is skipped and the result says to upload later with fw_upload_track.
    • Uploads are limited to 10 per hour, and a failed import is reported.
    • mp3 and ogg renders now carry title, artist and album tags.
  • notify: true raises a Crow notification when the render is done.
  • sample_rate can be 44100 or 48000. The library silently renders anything lower at 44100, so lower rates are refused.
  • duration_s is now the length of the audio actually written. A render can run up to 2 s past the end of the song because of effect tails. song_duration_s gives the song's own length.
  • The 20-minute render cap and every existing compose limit are unchanged.

sunvox_compose

  • arrangement places patterns on the timeline in order.
    • If x is missing, the pattern starts where the previous one ended. If y is missing, it is 0.
    • A pattern placed twice repeats, as a clone that shares its notes.
    • Every pattern must be placed.
    • Limits: 4096 placements, x up to 65536, |y| up to 1024.
  • The tempo effects go into whichever pattern plays first, not the first pattern listed in the spec.
  • The compose name becomes the song name.
  • Controller values outside a controller's min..max are refused instead of being clamped silently.
  • A composition with the same name is kept unless overwrite: true is passed. The write is atomic, so a concurrent compose cannot clobber it.
  • The work dir defaults to the instance's data dir, so a second instance gets its own. A ~/ setting expands to the home dir.

sunvox_info

  • Adds the format (sunvox, xm or mod), the project and engine versions, each module's x/y and colour, and each pattern's x/y.

Review minors from #515

  • spike/ is no longer in the bundle. Installed copies used to ship it, including scripts that wrote to the live data dir.

  • The test fixture is now a short song composed by sunvox_compose from a fixed spec (scripts/sunvox-studio/make-fixture.mjs). It reproduces byte for byte, and its expected values are derived from the spec. The upstream author's demo song has been removed from the repo.

  • CI now runs the engine tests instead of skipping them:

    • the suite job installs the bundle's dependencies with the installer's flags, plus ffmpeg and libasound;
    • CROW_REQUIRE_SUNVOX_ENGINE=1 turns a missing engine into a failure.

    The job keys suite, static-checks and audit are unchanged.

Artifact CSP

  • 'wasm-unsafe-eval' now appears on the page row only. The design kit allows its only WASM library (sunvox) in pages only.
  • A new guard test fails if a WASM library is ever allowed in a type whose CSP row does not permit WASM.
  • Artifacts 0.6.3.

Versions

  • sunvox-studio 0.1.2 → 0.2.0
  • artifacts 0.6.2 → 0.6.3
  • design-kit and songbook are unchanged.

Testing

  • Unit and engine tests:
    • tests/sunvox-studio.test.js (33 cases);
    • tests/sunvox-studio-outputs.test.js (18 cases, run against fakes with no services);
    • tests/artifact-origin.test.js, tests/identity-load-if-present.test.js and tests/s3-client-use-config.test.js;
    • the updated sunvox pin in tests/typed-tool-schemas.test.js.
  • Full suite: 10121 tests, 10108 pass, 0 fail, 13 skipped.
  • Live end-to-end in a scratch home, run again after the review fixes. Storage was a scratch MinIO configured the way the Nest does it, with sealed storage.shared.* settings and no MinIO environment in the MCP child. The gateway was a scratch instance on loopback. 26 of 26 checks passed:
    • compose with an arrangement, mp3 render, blog_post and notify. Both files landed in storage with storage_files rows, a notification row was created, and the presigned URL served a decodable mp3.
    • a published song page with **SunVox:** storage:<key> was opened in headless Chrome and Play was pressed. The engine assets and the project loaded, the player showed "Ready" with the song name, and the transport advanced (line 0 → 9 in 1.5 s).
    • glasses path:
      • Funkwhale was a loopback stand-in;
      • funkwhale_library uploaded the render;
      • the real fw_play tool returned a stream envelope that the glasses' envelope reader accepts;
      • the gateway's pinned relay fetch streamed a decodable mp3 of the right length, with tags, and refused any other origin.
  • Still to check by hand:
    • a real Funkwhale import of a render (its tag and metadata handling);
    • audible playback on the glasses through fw_play.

Deploy

  • Restart each gateway that serves artifacts so the CSP change takes effect. Instances built from a git archive need a rebuild.
  • sunvox-studio: installed copies refresh on the next gateway boot because of the version bump. The bundle is not installed anywhere yet.
  • An installed copy that already has spike/ keeps it, because the refresh only adds files. It is harmless, and reinstalling removes it.
  • Known limit: a 20-minute render plus encoding and upload can exceed the gateway's 60 s tool-call timeout. Short songs are well within it; the end-to-end compose and render took about 2.3 s.

https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

…ct CSP

The design kit allows its WASM library (sunvox) in pages only, so slides,
data and map no longer carry the WASM carve-out. Artifacts 0.6.3.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…ment, song name and fuller info

- sunvox_render and sunvox_compose save to Crow storage (storage key, a
  storage_files row, a 1-hour URL); reference_type blog_post makes a file
  playable on a song page. Optional Funkwhale upload (the Funkwhale add-on's
  own configuration, one origin, no redirects, rate-limited), a done
  notification and a 48 kHz output rate.
- sunvox_compose: an arrangement (ordered, bounded timeline placements;
  repeats are clones), the song name, controller values checked against each
  controller's min/max, and a same-name composition is kept unless
  overwrite is true. The tempo goes into the pattern that plays first.
- sunvox_info: format, project and engine version, module x/y/colour,
  pattern x/y.
- spike/ leaves the bundle; the test fixture is now a short song composed
  by sunvox_compose from a fixed spec (scripts/sunvox-studio/make-fixture.mjs).
- CI installs the bundle's deps with the installer's flags and requires the
  engine tests to run. sunvox-studio 0.2.0.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…, tags for Funkwhale, budgets

- The MCP child opens Nest-configured storage (storage.shared.* with sealed
  secrets) the way the gateway does, with the instance identity loaded only
  if it exists (new loadIdentityIfPresent, which never creates one); the
  MINIO_* env stays the fallback. envKeys now name the variables the
  s3-client reads.
- mp3/ogg renders carry title/artist/album tags (a library import needs
  them). The Funkwhale push fits the 60 s tool-call budget (30 s upload, two
  short import checks), reports a failed import, and its import_reference is
  the content hash.
- Storage keys get a random part (blog_post files are public). The work dir
  defaults to this instance's data dir and a '~/' setting expands. A
  same-name compose that lands first is never clobbered (wx).
- The fixture test derives its expected values from the spec; CI installs
  ffmpeg and libasound and runs the mp3 cases; a guard ties the design kit's
  WASM libraries to the CSP rows that allow WASM.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
A render carries up to 2 s of effect tail after the song ends, so
duration_s now reports the rendered audio and song_duration_s the song.
Found by the live end-to-end check (a 10 s song rendered to 12.1 s).

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…rage reads limited, one Funkwhale deadline

- The MCP child opens the Nest storage settings, wipes the identity's key
  material at once and gives the s3-client only the opened config (new
  useStorageConfig); loadIdentityIfPresent reads the file without any create
  path.
- storage_key reads only SunVox Studio's own files and blog media (never
  project files, attachments or private uploads), so a render cannot
  re-publish a private file at /blog/media.
- The Funkwhale push has one 35 s deadline that caps every request, and
  refuses to upload unmetered when the database is missing.
- 0.1.x installs that carry the old '~/.crow/data/sunvox-studio' default get
  their own instance's work dir. Storage keys carry 128 random bits; local
  render names a random suffix.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…l call's start

A render that already used most of the 60 s tool call skips the push
(before any request or rate token) and says to upload later with
fw_upload_track; otherwise the push gets what is left, at most 35 s. The
Nest storage config is retried when its first load finds nothing, a
trailing slash on the legacy work-dir value is ignored, and the comment on
wiping the identity says what it does and does not protect.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
@kh0pper
kh0pper merged commit a661af1 into main Oct 20, 2026
4 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.

1 participant