Skip to content

Harden composition audio-bed URL validation to match production asset policy #148

Description

@Brad-Edwards

Review finding

Composition-level audioBed.src is not validated against the production asset URL policy. Hardened deployments can pass assets: { baseUrl, allowedSchemes: ['https:'] } to validation and preloading, but composition beds are outside scene.assets and currently use the audio service's hardcoded default URL policy.

Evidence

  • docs/asset-url-policy.md:35 documents the hardened production profile with baseUrl and allowedSchemes: ['https:'].
  • src/runtime/validation.ts:379 checkCompositionAudioBed() calls only assertAudioBedDeclaration(), which validates shape but not URL scheme or base URL resolution.
  • src/runtime/audio.ts:990 validates audio URLs with resolveAssetUrl(url, undefined, DEFAULT_ALLOWED_SCHEMES), not the deployment's supplied asset policy.
  • src/runtime/scene-loader.ts:1315 passes the bed directly to createAudioService() after scene preloading; the preloader never sees composition-level bed URLs.

Impact

A production entrypoint that tightens scene assets to https: can still accept and attempt to play composition beds declared as http:, data:, or blob: URLs. That bypasses the documented transport and inline-payload controls for hardened deployments.

Recommended fix

Thread the same asset URL policy used by validateRuntime() and the preloader into composition audio-bed validation and runtime audio-bed source checks.

Acceptance checks

  • validateRuntime({ assets: { baseUrl, allowedSchemes: ['https:'] } }) reports findings for composition beds whose src resolves to http:, data:, blob:, file:, or another disallowed scheme.
  • createAudioService() or the loader receives the effective asset URL policy and rejects bed sources that violate it if validation was skipped.
  • Tests cover string and array audioBed.src values.
  • Current permissive authoring defaults continue to work when no explicit production policy is supplied.

Activity

  1. added
    bugSomething isn't working
    area:audioAudio service, mute, autoplay unlock, sprite playback
    area:assetsAsset declaration, preload, failure surfacing
    area:validationValidation pass, actionable errors, CI gate
    on May 23, 2026
  2. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    Picked up by /implement - driver Codex, branch 148-bed-url-policy, 2026-05-23T15:34:28Z.

  3. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    gc_codex_review — cycle 1 of 1 (pre-push) on issue #148 (branch 148-bed-url-policy)

    Core review

    Verdict: ship

    This change is shaped correctly: it promotes the existing asset URL policy into a shared contract at the asset-preloader seam, then threads that policy through validation, scene loading, and runtime audio construction while continuing to use the incumbent resolveAssetUrl helper for base URL and scheme handling. The cross-cutting concern is URL resolution policy, and the design boundary is cleanly expressed as injected AssetUrlPolicy rather than duplicated validation logic. It closes the prior composition-audio-bed gap without foreclosing the obvious next variation, such as per-deployment scheme hardening or alternate base URLs.

    No blocking findings.

    Security review

    Verdict: ship

    This change is shaped as a security-boundary tightening: it extracts the URL policy into a shared type, threads that policy through validation, scene loading, and audio construction, and resolves audio bed/source URLs before handing them to the engine. The seam is the asset URL policy, which is the right boundary for scheme and base URL enforcement; the change also preserves the existing explicit allowlist check for scene-declared audio sources. I do not see this foreclosing the next variation, such as per-deck policy or a stricter production default.

    No blocking findings.

  4. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    gc_codex_review pre-push cycle 1 of 1 complete for issue #148 on branch '148-bed-url-policy'. Posted by the MCP server to enforce the pre-push hard-cap-1 contract (issues #796, #804, #906). Do not edit or delete — used by the next gc_codex_review (uncommitted) invocation to count cycles.

  5. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    Review decision record — codex cycle 1 (issue #148)

    Reviewer: codex
    Cycle: 1

    Architectural read:

    Core reviewer: This change is shaped correctly: it promotes the existing asset URL policy into a shared contract at the asset-preloader seam, then threads that policy through validation, scene loading, and runtime audio construction while continuing to use the incumbent resolveAssetUrl helper for base URL and scheme handling. The cross-cutting concern is URL resolution policy, and the design boundary is cleanly expressed as injected AssetUrlPolicy rather than duplicated validation logic. It closes the prior composition-audio-bed gap without foreclosing the obvious next variation, such as per-deployment scheme hardening or alternate base URLs.

    Security reviewer: This change is shaped as a security-boundary tightening: it extracts the URL policy into a shared type, threads that policy through validation, scene loading, and audio construction, and resolves audio bed/source URLs before handing them to the engine. The seam is the asset URL policy, which is the right boundary for scheme and base URL enforcement; the change also preserves the existing explicit allowlist check for scene-declared audio sources. I do not see this foreclosing the next variation, such as per-deck policy or a stricter production default.

    Blocking findings: 0 (clean run)

  6. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    gc_test_quality_review cycle 1 of 1 — issue #148

    Reviewer: test-quality (claude-sonnet-4-6 via gc_test_quality_review)
    Branch: 148-bed-url-policy
    Cycle: 1 / 1
    Findings: 1

    Finding 1 — [warning] tests/runtime/audio.test.ts::createAudioService — lifecycle / cleanup (ADR-004)::mutating methods are inert after dispose; introspectors still work

    Problem: The test calls service.mute(true) after disposal to verify that mute is inert, but the only assertion that could detect a bypass is expect(fake.calls).toEqual([]). The fake engine's calls array tracks handle-level methods (play/stop/fade/loop/volume/rate/unload) only — engine.setMasterMute() is NOT recorded there. Removing the if (disposed) return; guard from mute() would cause the post-disposal call to reach engine.setMasterMute(true), but every assertion in the test would still pass: fake.calls stays empty, fake.created stays at length 1, and service.isDisposed() stays true.
    Why it matters: A stale async cleanup callback that calls ctx.audio.mute(false) on a disposed service would silently flip the engine's master-mute state, potentially unmuting audio for the NEXT navigation. The test purports to guard this scenario but cannot.
    Fix: After service.mute(true) on the already-disposed service, add expect(fake.masterMuted()).toBe(false). The FakeEngine interface already exposes masterMuted() — the assertion is a one-liner that gives the test an observable actually influenced by whether setMasterMute was called.

  7. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    gc_test_quality_review cycle 1 of 1 complete for issue #148 on branch '148-bed-url-policy'. Posted by the MCP server to enforce the gc_test_quality_review hard-cap-1 contract (issue #884 follow-up, default lowered in #906). Do not edit or delete — used by the next gc_test_quality_review invocation to count cycles.

  8. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    Review decision record — test-quality cycle 1 (issue #148)

    Reviewer: test-quality
    Cycle: 1

    Architectural read:

    Three-file test suite covering a well-separated audio stack. audio.test.ts unit-tests createAudioService against a handwritten deterministic fake engine that records every handle call with full argument lists — this is the right boundary, and the fake is rich enough to enable precise delegation assertions without a mock framework. scene-loader-audio.test.ts integration-tests the loader's wiring of the service: signal-binding, output-policy mapping, allowedSources plumbing, unlock-gate sequencing, and bed lifecycle. validation.test.ts unit-tests the static validation pass against raw declarative inputs. None of the files touch real I/O, DOM, audio APIs, or a real HTTP layer. The afterEach singleton-reset for noopAudioEngine addresses the prior test-quality review cycle's finding. The overall shape is correct and fits the codebase vocabulary.

    Blocking findings: 1

    Finding 1 — one-off

    • ID: F1
    • Title: (no title)
    • Decision: fix
    • Rationale: Addressed by next cycle
  9. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    Proceeding without an over-cap test-quality review cycle per user instruction after fixing the cycle-1 finding and passing local verification (

    @keplerops/pulsar@0.1.0 lint /home/atomik/src/pulsar
    biome check .

    Checked 191 files in 202ms. No fixes applied.

    @keplerops/pulsar@0.1.0 typecheck /home/atomik/src/pulsar
    tsc --noEmit

    @keplerops/pulsar@0.1.0 test /home/atomik/src/pulsar
    vitest run

    RUN v3.2.4 /home/atomik/src/pulsar

    ✓ tests/runtime/rng.test.ts (8 tests) 304ms
    ✓ tests/system/chrome-slots.test.ts (10 tests) 315ms
    ✓ tests/runtime/timeline.test.ts (98 tests) 338ms
    ✓ tests/system/helpers.test.ts (20 tests) 467ms
    ✓ tests/runtime/policy-q004-resource-cleanup.test.ts (145 tests) 462ms
    ✓ tests/system/chrome-extras.test.ts (5 tests) 321ms
    ✓ tests/runtime/policy-a006-slide-frameworks.test.ts (14 tests) 946ms
    ✓ PUL-A006 — live runtime independent of slide frameworks (source scan) > runtime tree (current code revision) > contains no A006 violations across the runtime-core file set 913ms
    ✓ tests/runtime/scrub-cue-gating.test.ts (1 test) 1075ms
    ✓ PUL-F017 — monotonic-forward audio cue gating > fires a cue on a forward crossing of its master time and suppresses it on a reverse crossing 1073ms
    ✓ tests/runtime/policy-a004-export-pipeline.test.ts (12 tests) 1151ms
    ✓ PUL-A004 — live runtime independent of export pipeline (source scan) > runtime tree (current code revision) > contains no A004 violations across the runtime-core file set 1119ms
    ✓ tests/system/presenter-bridge.test.ts (10 tests) 98ms
    ✓ tests/runtime/policy-q008-dom-css-accessibility.test.ts (85 tests) 1115ms
    ✓ PUL-Q008 — DOM/CSS accessibility (source scan) > runtime tree (current code revision) > contains no Q008 violations across src/runtime/**/*.ts 712ms
    ✓ tests/runtime/policy-a003-rendering-libraries.test.ts (15 tests) 1207ms
    ✓ PUL-A003 — optional rendering libraries are scene-local (source scan) > runtime tree (current code revision) > contains no A003 violations across the runtime-core file set 1161ms
    ✓ tests/runtime/policy-a010-export-metadata-share.test.ts (117 tests) 1629ms
    ✓ PUL-A010 — live and export share scene metadata (source scan) > runtime tree (current code revision) > rule 2: zero forbidden parallel scene/composition declarations across src/**/*.ts 1389ms
    ✓ tests/runtime/navigation.test.ts (111 tests) 89ms
    ✓ tests/runtime/policy-a002-audio-encapsulation.test.ts (47 tests) 148ms
    ✓ tests/runtime/policy-scene-trust-model-doc.test.ts (2 tests) 104ms
    ✓ tests/runtime/policy-a009-captions-single-source.test.ts (145 tests) 1963ms
    ✓ PUL-A009 — captions / prompter single source (source scan) > runtime tree (current code revision) > rule 2: zero forbidden parallel caption-schema declarations across src/**/*.ts 1593ms
    ✓ tests/runtime/scene.test.ts (125 tests) 35ms
    ✓ tests/runtime/asset-preloader.test.ts (34 tests) 72ms
    ✓ tests/runtime/policy-a008-mode-dispatch.test.ts (80 tests) 452ms
    ✓ tests/runtime/policy-q003-url-state-determinism.test.ts (130 tests) 2212ms
    ✓ PUL-Q003 — URL state determinism (source scan) > runtime tree (current code revision) > contains no Q003 violations across src/**/*.ts 2075ms
    ✓ tests/runtime/policy-a001-timeline-encapsulation.test.ts (27 tests) 175ms
    ✓ tests/runtime/policy-q007-remote-code-execution.test.ts (68 tests) 2511ms
    ✓ PUL-Q007 — no remote code execution (source scan) > runtime tree (current code revision) > contains no Q007 violations across src/**/*.ts 2426ms
    ✓ tests/runtime/composition-resolver.test.ts (42 tests) 75ms
    ✓ tests/runtime/screenshot-determinism-source.test.ts (80 tests) 2363ms
    ✓ PUL-Q001 — screenshot determinism source scan > runtime tree (current code revision) > contains no non-deterministic primitives across src/**/*.ts 2285ms
    ✓ tests/runtime/scene-loader-present.test.ts (47 tests) 64ms
    ✓ tests/runtime/composition.test.ts (116 tests) 67ms
    ✓ tests/runtime/validation.test.ts (59 tests) 37ms
    ✓ tests/runtime/scene-navigation.test.ts (63 tests) 33ms
    ✓ tests/runtime/scene-loader.test.ts (29 tests) 72ms
    ✓ tests/runtime/policy-a005-declarative-composition.test.ts (38 tests) 77ms
    ✓ tests/runtime/audio.test.ts (117 tests) 132ms
    ✓ tests/runtime/policy-q002-browser-support.test.ts (21 tests) 81ms
    ✓ tests/runtime/scene-loader-audio.test.ts (46 tests) 82ms
    ✓ tests/runtime/scene-loader-screenshot-prompter.test.ts (33 tests) 65ms
    ✓ tests/runtime/audio-engine.test.ts (15 tests) 40ms
    ✓ tests/system/audio-helpers.test.ts (13 tests) 37ms
    ✓ tests/runtime/scene-loader-paused-scrub.test.ts (34 tests) 42ms
    ✓ tests/runtime/registry.test.ts (35 tests) 23ms
    ✓ tests/runtime/scene-loader-beat-mode.test.ts (28 tests) 33ms
    ✓ tests/system/presenter-driven.test.ts (11 tests) 27ms
    ✓ tests/scenes/loop-fixture.test.ts (20 tests) 11ms
    ✓ tests/runtime/scene-loader-standalone-loop.test.ts (26 tests) 43ms
    ✓ tests/runtime/policy-biome-complexity-gate.test.ts (5 tests) 70ms
    ✓ tests/runtime/scene-loader-chrome.test.ts (19 tests) 88ms
    ✓ tests/runtime/scene-loader-screenshot-seed.test.ts (9 tests) 15ms
    ✓ tests/scenes/placeholder.test.ts (15 tests) 29ms
    ✓ tests/runtime/pul-q006-error-context.test.ts (16 tests) 32ms
    ✓ tests/system/transitions.test.ts (13 tests) 18ms
    ✓ tests/scenes/browser-support-fixture.test.ts (19 tests) 28ms
    ✓ tests/scenes/scrub-fixture.test.ts (18 tests) 23ms
    ✓ tests/system/scrub-controls.test.ts (13 tests) 16ms
    ✓ tests/runtime/presenter.test.ts (26 tests) 24ms
    ✓ tests/runtime/prompter.test.ts (12 tests) 28ms
    ✓ tests/runtime/workbench-chrome.test.ts (22 tests) 17ms
    ✓ tests/scenes/dom-css-accessibility-fixture.test.ts (22 tests) 13ms
    ✓ tests/runtime/composition-registry.test.ts (24 tests) 21ms
    ✓ tests/system/standalone-audio-bed.test.ts (3 tests) 14ms
    ✓ tests/scenes/paused-fixture.test.ts (19 tests) 17ms
    ✓ tests/system/counter.test.ts (12 tests) 10ms
    ✓ tests/system/presenter-renderers.test.ts (6 tests) 19ms
    ✓ tests/system/presenter-keyboard.test.ts (11 tests) 32ms
    ✓ tests/scenes/screenshot-rng-fixture.test.ts (8 tests) 9ms
    ✓ tests/runtime/error.test.ts (16 tests) 10ms
    ✓ tests/runtime/audio-unlock-dom.test.ts (7 tests) 21ms
    ✓ tests/system/register-tokens.test.ts (2 tests) 4ms
    ✓ tests/runtime/id-registry.test.ts (5 tests) 8ms
    ✓ tests/system/templates-options.test.ts (20 tests) 29ms
    ✓ tests/system/templates.test.ts (58 tests) 28ms
    ✓ tests/runtime/version.test.ts (2 tests) 6ms
    ✓ tests/system/prompter-window-url.test.ts (4 tests) 11ms
    ✓ tests/system/terminal-audio.test.ts (8 tests) 15ms
    ✓ tests/system/pulsar-intro-deck.test.ts (7 tests) 7ms
    ✓ tests/runtime/workbench-graph.test.ts (6 tests) 9ms

    Test Files 74 passed (74)
    Tests 2609 passed (2609)
    Start at 17:51:00
    Duration 6.40s (transform 7.25s, setup 931ms, collect 35.30s, tests 21.26s, environment 31ms, prepare 9.58s), plus focused audio/validation/loader tests).

  10. removed
    in-progressAn agent is actively working this issue via /implement
    on May 23, 2026
  11. Brad-Edwards commented on May 23, 2026

    @Brad-Edwards
    ContributorAuthor

    Final report — issue #148 complete

    PR: #155

    PR #155 is open and green for issue 148; issue closed after implementation handoff.

    Files changed

    Added:

    • changelog.d/148.fixed.md

    Modified:

    • src/runtime/asset-preloader.ts
    • src/runtime/audio.ts
    • src/runtime/scene-loader.ts
    • src/runtime/validation.ts
    • tests/runtime/audio.test.ts
    • tests/runtime/scene-loader-audio.test.ts
    • tests/runtime/validation.test.ts

    Reviews

    • codex: Cycle 1 clean; 0 findings.
    • test-quality: Cycle 1 reported one test weakness in the disposed-service mute assertion; fixed locally, verified, and user authorized proceeding without another review cycle after the cap.
    • sonarcloud: GitHub SonarCloud and SonarCloud Code Analysis checks passed. The Ground Control watcher could not authenticate because SONAR_TOKEN is unset on the MCP host.

    Traceability reconciliation

    • IMPLEMENTS / TESTS / DOCUMENTS added: 0
    • Links updated: 0
    • Stale links removed: 0

    No in-scope requirement UIDs were attached to issue 148. Existing traceability for the touched audio-bed validation/runtime files and tests remains present; no links changed.

    Status

    • CI: ✅ green
    • SonarCloud: ✅ passed
    • PR ready for user review and merge.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:assetsAsset declaration, preload, failure surfacingarea:audioAudio service, mute, autoplay unlock, sprite playbackarea:validationValidation pass, actionable errors, CI gatebugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions