MIDI Out: optional TLS so Web MIDI works from non-localhost origins - #32
Merged
Merged
Conversation
Implements docs/MIDI_OUT_SECURE_CONTEXT_SPEC.md (added here). Web MIDI is
gated behind window.isSecureContext, and StemForge binds to 0.0.0.0 — so a
browser on a second machine reaches http://<server-ip>:8765, an insecure
origin where the MIDI permission reads as immovably "Block". In the
2026-08-30 hardware session that presentation cost ~20 minutes of chasing
Brave settings before the origin turned out to be the cause, and the old
banner text ("Open StemForge at http://localhost:8765") pointed at the one
remedy a remote user cannot follow.
run.py grows --ssl-certfile/--ssl-keyfile, mirroring uvicorn's parameter
names and passed straight through to uvicorn.run (None/None is uvicorn's
default, so plain-HTTP behavior is byte-identical). Supplying one flag
without the other is a startup error, exit 1 — a lone flag means the user
wants TLS, and silently starting over HTTP would reproduce exactly the
confusion this exists to remove. The check runs before the GPU lock is
acquired so a config error can't leave a lock file behind. The banner
takes a scheme parameter and prints https:// when TLS is on; the hostname
stays localhost since the banner describes reaching the server from its
own machine.
The midi.js secure-context banner no longer hardcodes port 8765 and no
longer names loopback as the only fix — it now points at localhost on the
server's actual port or HTTPS, with a pointer to the README. This is the
only frontend change; the isSecureContext guard itself is untouched (it is
the browser's contract), and frontend/ still contains no ws://, WebSocket,
or EventSource usage, so TLS cannot introduce mixed-content failures.
README documents the three deployment paths in order of preference —
Tailscale serve (publicly trusted cert, works from mobile), SSH tunnel
(the origin genuinely is loopback, no certs at all), and the new flags
with an mkcert-generated cert — noting that a bare self-signed cert is
not sufficient in Chrome/Brave (the interstitial click-through does not
create a secure context) and that HF Spaces already serves HTTPS.
StemForge deliberately consumes cert paths and never generates or manages
certificates.
tests/test_cli_args.py covers the flag surface: defaults, storage, and
the pair-check predicate in both directions (tested directly rather than
through main(), which acquires the GPU lock). Wired into the CI test job.
Verified locally: lone-flag start exits 1 with the error text and leaves
no gpu.lock; the banner prints http://localhost:8765 with no flags and
https:// with them. The full second-machine acceptance test (mkcert CA +
hardware synth) needs real hardware and remains manual per the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vb2foWspag8yfjz1hvxas2
tsondo
pushed a commit
that referenced
this pull request
Aug 30, 2026
Both branches appended test files to the same pytest invocation in ci.yml; the resolution keeps all three additions. CLAUDE.md and README auto-merged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vb2foWspag8yfjz1hvxas2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/MIDI_OUT_SECURE_CONTEXT_SPEC.md(added in this PR).Problem
Web MIDI is gated behind
window.isSecureContext. StemForge binds to0.0.0.0, so a browser on a second machine reacheshttp://<server-ip>:8765— an insecure origin, where the browser reports the MIDI permission as immovably "Block". In the 2026-08-30 hardware session that presentation cost ~20 minutes chasing Brave settings before the origin turned out to be the cause, and the old banner text ("Open StemForge at http://localhost:8765") named the one remedy a remote user cannot follow.Changes
run.py:--ssl-certfile/--ssl-keyfile, mirroring uvicorn's parameter names, passed straight through touvicorn.run(None/Noneis uvicorn's default — plain-HTTP behavior is byte-identical). Supplying one without the other is a startup error, exit 1, checked before the GPU lock so a config error can't leave a lock file behind. The banner printshttps://under TLS.frontend/components/midi.js(the only frontend change): the secure-context banner no longer hardcodes port 8765 and no longer names loopback as the only fix. TheisSecureContextguard itself is untouched.tests/test_cli_args.py: flag defaults, storage, and the pair-check predicate in both directions (tested directly rather than viamain(), which acquires the GPU lock). Wired into CI.Per the spec's DO-NOT list: no cert generation, no
--insecureescape hatch, no bind-host change, no new dependencies, no changes tobackend/main.py,/api/midi/events,webmidi.js, or scheduler logic.Validation
run.pycompiles;midi.jspassesnode --check;ci.ymlparsesgpu.lockleft behindhttp://localhost:8765with no flags,https://localhost:<port>with themgrep -rn "ws://\|WebSocket\|EventSource" frontend/returns nothing (DoD [A11Y] [High] Transport bar controls missing accessible names #8)🤖 Generated with Claude Code
https://claude.ai/code/session_01Vb2foWspag8yfjz1hvxas2
Generated by Claude Code