-
Notifications
You must be signed in to change notification settings - Fork 0
Web playground (WASM) for driving the complement engine from a phone #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| name: web-playground | ||
|
|
||
| # Builds the WASM playground (web/) and deploys it to GitHub Pages. | ||
| # Enable once under repo Settings → Pages → Source: "GitHub Actions". | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: ['web/**', 'core/**', '.github/workflows/web.yml'] | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pages: write | ||
| id-token: write | ||
|
|
||
| # One deploy at a time; let an in-progress run finish. | ||
| concurrency: | ||
| group: pages | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install wasm target | ||
| run: rustup target add wasm32-unknown-unknown | ||
| - name: Cache cargo | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| web/target | ||
| key: web-wasm-${{ hashFiles('web/Cargo.toml', 'core/Cargo.toml') }} | ||
| - name: Build playground | ||
| run: ./web/build.sh | ||
| - uses: actions/upload-pages-artifact@v3 | ||
| with: | ||
| path: web/dist | ||
|
|
||
| deploy: | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| environment: | ||
| name: github-pages | ||
| url: ${{ steps.deploy.outputs.page_url }} | ||
| steps: | ||
| - id: deploy | ||
| uses: actions/deploy-pages@v4 | ||
|
Comment on lines
+26
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n .github/workflows/web.yml | head -60Repository: PhysShell/griff Length of output: 1648 🌐 Web query:
💡 Result: GitHub’s official security guidance strongly recommends pinning GitHub Actions to a full-length commit SHA as the only way to ensure an action is used as an immutable release [1][2]. Key security considerations include: 1. Immutability: A commit SHA uniquely and immutably identifies a specific version of an action’s code [3][4]. In contrast, tags (e.g., Citations:
🌐 Web query:
💡 Result: Setting persist-credentials: false in the actions/checkout GitHub Action is a security best practice recommended to prevent unintended token exposure [1][2][3]. By default, actions/checkout sets persist-credentials to true, which stores the GITHUB_TOKEN (or a provided personal access token) in the local.git/config file on the runner [4][5][3]. This enables subsequent steps in the same job to execute authenticated Git commands (such as git fetch or push) [4][6][7]. However, this also makes the token accessible to any process, script, or third-party action running in that job [8][1][9]. The primary security risks associated with keeping the default (persist-credentials: true) include: 1. Artifact Exposure (ArtiPACKED): If a workflow uploads the repository workspace as an artifact (e.g., using actions/upload-artifact), the.git directory—containing the persisted credentials—may be included [10][11][12]. If this artifact is public or accessible to unauthorized users, the token can be exfiltrated and misused [2][10][12]. 2. Increased Attack Surface: Any compromised or malicious third-party action within the workflow can read the.git/config file to obtain the token, potentially escalating privileges beyond what was intended for that specific action [1][9]. Recommendation: - Set persist-credentials: false for any checkout step that does not explicitly require authenticated Git operations (e.g., jobs focused solely on testing, building, or scanning) [1][9]. - If your workflow must perform Git operations (like pushing commits), consider using persist-credentials: false and managing authentication through other methods, or ensure that artifacts are configured to strictly exclude the.git directory (e.g., by using paths to target only specific build output folders) [2][10][12]. Using security auditing tools like zizmor or custom security policies can help detect and enforce the use of persist-credentials: false across your workflows [12][13]. Citations:
Pin all GitHub Actions by commit SHA and disable checkout credential persistence. Lines 26, 30, 39, and 51 currently use tag refs ( Suggested hardening patch- - uses: actions/checkout@v4
+ - uses: actions/checkout@<full_commit_sha>
+ with:
+ persist-credentials: false
@@
- uses: actions/cache@v4
+ uses: actions/cache@<full_commit_sha>
@@
- - uses: actions/upload-pages-artifact@v3
+ - uses: actions/upload-pages-artifact@<full_commit_sha>
@@
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@<full_commit_sha>🧰 Tools🪛 zizmor (1.25.2)[warning] 26-26: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) [error] 30-30: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) [error] 39-39: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) [error] 51-51: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| # ADR 0024: Ship the egui frontend to the browser (WASM) for mobile testing | ||
|
|
||
| Date: 2026-06-16 | ||
| Status: Proposed | ||
|
|
||
| ## Context | ||
|
|
||
| griff today runs only as a desktop CLI and a `ratatui` terminal preview (S8) — | ||
| both tied to a computer terminal. The primary author/tester works almost | ||
| entirely from a phone, so iterating on generation (tweaking seed / mode / | ||
| variation and *hearing* the result) is impractical: every test means getting to | ||
| a desktop. | ||
|
|
||
| The pieces for a browser build are already in place: | ||
|
|
||
| - **`griff-core` is WASM-clean.** It is pure compute — no filesystem, threads, | ||
| wall-clock, or the `rand` crate (the seeded PRNG is a hand-rolled `SplitMix64` | ||
| finalizer); all file I/O lives in the CLI. It compiles to | ||
| `wasm32-unknown-unknown` today (verified 2026-06-16), `serde` and collections | ||
| included. | ||
| - **ADR-0016 already chose `egui`** as the GUI renderer over a shared UI core | ||
| (view-model → interaction core → scene → renderers), and **`eframe` builds the | ||
| *same* egui app to native desktop and web/WASM**. A browser build is therefore | ||
| not a new frontend — it is the planned egui renderer targeting the browser. | ||
| - **S8 already lists** `eframe/egui window` and `MIDI playback` as its remaining | ||
| items; the pure `PianoRollView` / `Analysis` projections exist. | ||
|
|
||
| What the browser changes versus the native plan is narrow: (1) audio — S8 planned | ||
| `midir`, which has no web backend; (2) input — no filesystem; (3) build/hosting. | ||
|
|
||
| ## Decision | ||
|
|
||
| 1. **The canonical web frontend is the `eframe`/`egui` app compiled to | ||
| `wasm32-unknown-unknown`** — the same renderer ADR-0016 specifies. Native and | ||
| web share one codebase; the browser is a *build target*, not a fork. That is | ||
| the M2 target. | ||
|
|
||
| 2. **The M1 MVP is a thin, throwaway front, not egui** — an *import-free* | ||
| `cdylib` (`web/`, no `wasm-bindgen`, no framework) that exports three C-ABI | ||
| functions, plus a static `index.html` + `app.js` that loads the `.wasm` with | ||
| `WebAssembly.instantiate(bytes, {})` and marshals a small JSON result through | ||
|
Comment on lines
+38
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ABI wording mismatch: this MVP exports two C-ABI functions, not three. Please update the ADR text to match the implemented/exported contract ( 🤖 Prompt for AI Agents |
||
| linear memory. This unlocks phone testing now without the egui/Trunk/ | ||
| wasm-bindgen toolchain. It is disposable, not a second canonical renderer, so | ||
| it carries no ADR-0016 divergence debt; egui replaces it at M2. | ||
|
|
||
| 3. **`griff-core` gains a default-on `gp` feature** so the wasm build can drop | ||
| the Guitar Pro importer (`guitarpro`/`zip` → `time`/`getrandom` → | ||
| `wasm-bindgen`/`js-sys`). With `default-features = false` the module is | ||
| genuinely import-free and ~90 KiB; the CLI and tests keep `gp` on and are | ||
| unchanged. | ||
|
|
||
| 4. **Audio on web is WebAudio**, not the Web MIDI API (absent on iOS Safari, | ||
| patchy on mobile) and not `midir` (no web backend). The MVP uses a placeholder | ||
| oscillator synth fed note events from core; a bundled SoundFont (guitar tone) | ||
| is a follow-up. The playback *driver* is the one per-target seam. | ||
|
|
||
| 5. **Input is a fixed in-code sample** for the MVP (a file picker / drag-drop | ||
| later); the CLI keeps path-based I/O. | ||
|
|
||
| 6. **Build and host: `cargo build --target wasm32` → copy the `.wasm` beside the | ||
| static files → GitHub Pages** (`web/build.sh`, `.github/workflows/web.yml`). | ||
| No Trunk or `wasm-bindgen` for the MVP. A URL, no install. | ||
|
|
||
| 7. **Determinism is unaffected** (SPEC §6): the same controls yield the same | ||
| output in the browser too; the engine's seeded PRNG never touches wall-clock | ||
| or OS randomness. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - The maintainer can run complement — and the `VariationControl` knob — on a | ||
| phone via a URL. That is the actual ask. | ||
| - The import-free `cdylib` needs no build tooling beyond the stock wasm target: | ||
| `cargo build --target wasm32-unknown-unknown` then static hosting. Tiny payload | ||
| (~90 KiB, ~35 KiB gzipped). | ||
| - The `gp` feature gate also benefits any future wasm/plugin target that only | ||
| needs MIDI; it is a clean, default-on split. | ||
| - At M2 the per-target surface becomes: one egui codebase for desktop + web, with | ||
| the playback driver (`midir` native / WebAudio web) and input (fs vs picker) | ||
| behind seams; a SoundFont (license-checked) lands for a real tone. | ||
| - Accepted: the MVP synth is a placeholder (sawtooth + envelope), the roll is a | ||
| throwaway canvas painter, and the sample part A is fixed — all replaced as M2/M3 | ||
| land. | ||
| - Accepted: mobile browsers require a user gesture before audio starts (a tap to | ||
| unlock the `AudioContext`); SoundFont licensing/bundling is a real chore. | ||
| - Accepted: the MVP roll is throwaway; the canonical piano-roll still needs the | ||
| ADR-0016 Scene/Viewport work (S8). | ||
| - Out of scope for the MVP: offline PWA install, and corpus curation / | ||
| persistence on web (the `preview/design/` curation dock — later). | ||
|
|
||
| ## Roadmap | ||
|
|
||
| Extends ADR-0016 and advances the S8 "egui window + playback" items toward a web | ||
| target. If it grows beyond a playground it earns its own appended stage | ||
| (append-only, per the stage-label audit). | ||
|
|
||
| ## See also | ||
|
|
||
| - [`0016-shared-ui-core-across-frontends.md`](0016-shared-ui-core-across-frontends.md) | ||
| - [`0007-clap-first-plugin-target.md`](0007-clap-first-plugin-target.md) | ||
| - [`../stages/S8-preview-app.md`](../stages/S8-preview-app.md) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| /target | ||
| /dist | ||
| Cargo.lock |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Excluded from the root workspace (see root Cargo.toml `exclude`): this crate | ||
| # only builds for `wasm32-unknown-unknown`, like `fuzz/` only builds on nightly. | ||
| [package] | ||
| name = "griff-web" | ||
| version = "0.0.0" | ||
| edition = "2021" | ||
| publish = false | ||
| description = "Browser playground (WASM) for the griff engine — see docs/adr/0024." | ||
|
|
||
| [lib] | ||
| # `rlib` alongside `cdylib` lets the host-side unit tests link the crate; the | ||
| # wasm build still emits the cdylib. | ||
| crate-type = ["cdylib", "rlib"] | ||
|
|
||
| # `default-features = false` drops the GP importer (guitarpro/zip/time/getrandom | ||
| # → wasm-bindgen), leaving a lean, import-free wasm module. | ||
| [dependencies] | ||
| griff-core = { path = "../core", default-features = false } | ||
|
|
||
| # A pure-compute WASM module: no unwinding (smaller, no import surface), squeezed | ||
| # for mobile download. | ||
| [profile.release] | ||
| panic = "abort" | ||
| opt-level = "z" | ||
| lto = true | ||
| codegen-units = 1 | ||
| strip = true | ||
|
|
||
| # Own workspace root (like `fuzz/`): isolates the wasm-only target and profile | ||
| # from the stable host workspace. | ||
| [workspace] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # griff web playground (WASM) | ||
|
|
||
| A browser front for the complement arranger — built so the engine can be driven | ||
| (and *heard*) from a phone, no install. See | ||
| [`docs/adr/0024-web-wasm-frontend-for-mobile.md`](../docs/adr/0024-web-wasm-frontend-for-mobile.md). | ||
|
|
||
| This is the **MVP** (ADR-0024 §2): a deliberately thin, throwaway front — no | ||
| `wasm-bindgen`, no framework. `griff-web` is an *import-free* `cdylib` that | ||
| exports three C-ABI functions; the page (`static/`) loads the `.wasm` with | ||
| `WebAssembly.instantiate(bytes, {})` and marshals a small JSON result through | ||
| linear memory. The canonical `egui` frontend (ADR-0016) replaces it at M2. | ||
|
|
||
| ## What it does | ||
|
|
||
| Builds a fixed sample lead (part A) and a generated complement (part B) entirely | ||
| in the browser, with live controls for **mode**, **seed**, **register offset**, | ||
| and **pitch spread** (the ADR-0023 `VariationControl`, audible on the grid-locked | ||
| modes). Deterministic: the same controls always produce the same result. | ||
|
|
||
| ## Build & run locally | ||
|
|
||
| ```sh | ||
| ./web/build.sh # → web/dist/ (wasm + static) | ||
| python3 -m http.server -d web/dist 8080 # open http://localhost:8080 | ||
| ``` | ||
|
|
||
| The crate is wasm32-only and excluded from the root workspace (like `fuzz/`), so | ||
| stable `--workspace` builds/clippy/tests never touch it. It depends on | ||
| `griff-core` with `default-features = false`, dropping the Guitar Pro importer | ||
| (`guitarpro`/`zip`/`time`/`getrandom` → `wasm-bindgen`) — that is what keeps the | ||
| module import-free and ~90 KiB. | ||
|
|
||
| ## ABI | ||
|
|
||
| | export | signature | meaning | | ||
| | --- | --- | --- | | ||
| | `arrange` | `(mode:u32, seed:u32, offset:i32, variation:f32) -> *const u8` | arrange; returns a pointer to JSON in linear memory | | ||
| | `arrange_len` | `() -> usize` | byte length of the last result | | ||
| | `memory` | — | the linear memory JS reads the JSON from | | ||
|
|
||
| `mode`: 0 `rhythm_lock`, 1 `register_contrast`, 2 `call_response`, | ||
| 3 `support_layer`, 4 `octave_double`, 5 `counter_melody`. | ||
|
|
||
| Result JSON: `{ppqn, tempo, realized_spread, error, tracks:[{name, role, notes:[{p,s,d,v}]}]}`. | ||
|
|
||
| ## Deploy | ||
|
|
||
| `.github/workflows/web.yml` builds `web/dist` and publishes it to GitHub Pages on | ||
| pushes to the default branch (enable Pages → "GitHub Actions" in repo settings). | ||
|
|
||
| ## Notes / next | ||
|
|
||
| - Audio is a placeholder WebAudio synth (sawtooth + envelope, A left / B right). | ||
| A real SoundFont (guitar tone) is a follow-up. | ||
| - Input is a fixed in-code sample; a file picker / drag-drop comes later. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| #!/usr/bin/env bash | ||
| # Build the griff web playground into web/dist/ (static, deployable anywhere). | ||
| # | ||
| # ./web/build.sh # release build → web/dist | ||
| # python3 -m http.server -d web/dist 8080 # then open http://localhost:8080 | ||
| # | ||
| # No wasm-bindgen / Trunk: griff-web is an import-free cdylib (ADR-0024), so the | ||
| # .wasm is copied next to the static files and loaded with WebAssembly.instantiate. | ||
| set -euo pipefail | ||
| here="$(cd "$(dirname "$0")" && pwd)" | ||
| out="$here/dist" | ||
|
|
||
| rustup target add wasm32-unknown-unknown >/dev/null 2>&1 || true | ||
| ( cd "$here" && cargo build --release --target wasm32-unknown-unknown ) | ||
|
|
||
| rm -rf "$out" | ||
| mkdir -p "$out" | ||
| cp "$here"/static/* "$out"/ | ||
| cp "$here"/target/wasm32-unknown-unknown/release/griff_web.wasm "$out"/ | ||
|
|
||
| size=$(wc -c < "$out/griff_web.wasm") | ||
| echo "built web/dist ($((size / 1024)) KiB wasm) — serve it with:" | ||
| echo " python3 -m http.server -d \"$out\" 8080" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scope elevated permissions to the deploy job only.
Line 14 (
pages: write) and Line 15 (id-token: write) are set workflow-wide; thebuildjob does not require them. Move these permissions tojobs.deploy.permissionsand keep top-level/job-build permissions read-only.Suggested permission scoping
🧰 Tools
🪛 zizmor (1.25.2)
[error] 14-14: overly broad permissions (excessive-permissions): pages: write is overly broad at the workflow level
(excessive-permissions)
[error] 15-15: overly broad permissions (excessive-permissions): id-token: write is overly broad at the workflow level
(excessive-permissions)
🤖 Prompt for AI Agents
Source: Linters/SAST tools