Skip to content

deck: decks with audio or video open - #511

Merged
ymote merged 5 commits into
mainfrom
feat/deck-media-opens
Oct 13, 2026
Merged

ymote merged 5 commits into
mainfrom
feat/deck-media-opens

Conversation

@ymote

@ymote ymote commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Why

The maintainer imported a real 42 MB, 36-slide .pptx with one ppt/media/media1.mp4, and Quick Deck couldn't open it: the import guard refused every audio or video part as held:. They approved opening such decks (11 Oct 2026, "yes OK"). Media rides along as opaque bytes and nothing plays it.

Audit: every call into the decoders, and what the service runs

Read at deckcraft d0e57d7e. The decoders are deckcraft-media: the symphonia audio decoder, plus the h264, hevc, av1, vp9, isobmff, matroska, ogg and opus crates. Per the crates' Cargo.tomls, only engine and ui-egui depend on it. pptx, format, render, anim, pdf, text, model and fonts don't, so none of them can call a decoder.

Call into deckcraft-media Reached by Verdict
engine/src/cmd/media.rs:100, deckcraft_media::probe media.info only held by the door (HELD_BACK, #448); no method builds it
engine/src/cmd/media.rs:123, deckcraft_media::poster_png media.posterFrame (media.rs:139) and insert.video (insert.rs:477) both held
engine/src/cmd/insert.rs:450, deckcraft_media::probe insert.audio and insert.video (insert.rs:511, 514) both held
engine/src/lib.rs:27, pub use deckcraft_media as media a re-export the service never names it
Service path What runs Reaches a decoder?
Import guard the service, plus the zip crate's directory read no
Open a package deckcraft_pptx::import (read/shapes.rs:259 stores the clip's bytes as a MediaClip, with the package's own poster picture), then deckcraft_format::repair no
Open a native deck deckcraft_format::load (each item's bytes from media/<id>) no
deck.cover, deck.view, deck.layouts, the images export render_slide, render_layout and cmd::file::render_png (render/src/lib.rs:559-571: the poster is drawn as a picture, otherwise a film or speaker glyph) no
deck.view {step}, deck.show deckcraft-anim (show_order, ShowState, Timeline::state), then the renderer no
deck.slide, deck.slides, deck.format_state, deck.hit slide.inspect (inspect.rs:99 reports a clip's id, video, autoplay and loop), document.inspect, format.state, Session::hit_test, model reads no
Native save save_bytes(.., "deckcraft"), so deckcraft_format::save: clip bytes are stored, video and audio not compressed again no
.pptx export save_bytes(.., "pptx"), so the pptx writer (write/shapes.rs:557: the media part and its relationships) no
PDF export cmd::file::pdf_bytes, so deckcraft-pdf, which uses the renderer no
Outline export save_bytes(.., "outline") / slides_to_outline no
Undo, redo edit.undo and edit.redo (edit.rs restore: a snapshot swap) no
Every method's engine commands the commands in #503's safety table. edit.rs:57 media_of_shapes only lists a shape's media ids for the clipboard no
The agent's deck.run the door, where all four media commands are held held

What

  • Guard (src/guard.rs): audio and video parts pass like any other part. The checks by extension, by declared content type ([Content_Types].xml), by container signature, and by a native manifest's contentType are gone. Every bound stays:
    • entries, name length, per-entry and total sizes, the 100:1 ratio past 100 KiB, and actual size against declared size;
    • nested zips outside ppt/embeddings/ (held:) and the read-alike parity rule;
    • the media ceiling, before the engine reads: the declared total of a zip's binary parts (everything but its XML and relationship parts, and a native deck's manifest and mimetype) past the session's 128 MiB of media is too_large:, in the session ceiling's words. The package reader copies each media part out, so the engine never takes in more than the ceiling;
    • and after it reads, on every path: read_guarded (open, cover) and read_deck (info, text, render, convert, run) hold the deck's own media to the same ceiling, so all seven paths answer alike. One set of words (media_over, too_large_deck) serves the guard, the paths and the session's Held::over.
      The four media commands stay held.
  • Error code: held: now means a zip beyond the guard, or a command the door holds back (src/codes.rs).
  • Docs: the deck paragraph in apps/AGENTS.md, the deck service paragraph in both apps READMEs, and ADR 0013's deck row in both languages.
  • Contract (apps/quickdeck/design/SERVICE.md, its own commit). The import guard's held: list drops audio and video. A new "Audio and video" paragraph says they open, carried as bytes and never decoded, with the four media commands held and too_large: past 128 MiB of media. In the error table, held: becomes "its zip is beyond the import guard (Engine robustness gaps from the command-door caps review: deck media, cad DXF splines, film export memory, photo.run caps #448)". A line in "As built" notes deck: decks with audio or video open #511, which lifts decision 4's media refusal.
  • Quick Deck's wording, with no layout change:

Tests

On the Linux build host (x86_64), at head dab0c0e0 on main 37eb1e23:

  • cargo test --locked -p octosense-deck-service -p octosense-engine-area: deck service lib 88 passed, 1 ignored (the timing test); first_call 1 and skill 1 passed; engine-area 28 passed.
  • cargo clippy --locked -p octosense-deck-service -p octosense-engine-area --all-targets --no-deps: no findings.
  • cargo test --locked -p octosense-shell quickdeck_model, from the root: 28 passed, including the new a_held_deck_is_explained_without_media. The whole shell lib suite from the root also passed: 1,050 passed.
  • New: a_deck_with_video_and_sound_opens_draws_saves_and_exports. A deck holds a video with a red poster and a sound.
    • It opens from both a .pptx and a native file, with the clips' bytes intact in the open document.
    • The poster is drawn red at the video's centre.
    • deck.view works with and without a step, and so does deck.cover.
    • It saves natively and reopens with the same bytes.
    • It exports a .pptx whose clips come back byte for byte, and which opens again, and a 3-page PDF.
    • Without a poster, the video draws as a dark frame with its glyph, and the sound draws a speaker glyph.
  • New: hostile_clip_bytes_are_carried_on_every_path_and_never_decoded. The clips are a hostile MP4 (boxes claiming 4 GiB and 16 EiB, four billion samples) and a WAV of 65,535 channels claiming 4 GiB.
    • Every path completes: info, text, render, convert to every format, run, open, slides, slide, view (plain, with a step and in edit mode), hit, show, layouts, cover, an edit then undo and redo, save, and the .pptx, PDF, images and outline exports.
    • The bytes come through unchanged.
    • media.info, media.posterFrame, insert.video and insert.audio stay held through deck.run.
    • The audit above is the proof that nothing decodes; this test shows every path completes with such bytes aboard.
  • New: a_deck_past_the_media_ceiling_is_too_large_before_the_engine_reads. A native deck and a package whose binary parts declare just past 128 MiB (1,311 parts of 100 KiB, each inside every other bound, in files of a few hundred KB) answer too_large: on all seven paths with no engine read. The read counter stays unchanged. One just under (1,310 parts) opens on all seven paths, with seven reads. The post-read check (media_ceiling) refuses and passes the same two decks in the same words.
  • Turned around: the guard's media tests now pass media, and still refuse a movie that lies about its size or compresses past the ratio. The ZIP64-extra redirection test now hides a nested zip instead of a movie. The late-content-types package's held variant is a nested zip. The hostile list drops its media case.

Also checked on the maintainer's Mac only: the maintainer's own deck. A throwaway check that wasn't committed opened it through the service and drew five slides. The file never left the Mac.

  • It opens, with 36 slides.
  • The video slide draws the poster the file carries. That poster is a plain dark frame, so the video shows as a dark rectangle.
  • Chinese text renders in real glyphs.

Not run: no shell or desktop run, and no Quick Deck UI. apps/quickdeck/tests/ui.py (a hidden desktop shell) wasn't re-run, so its stored s3-held evidence still shows the old card line. The design brief (design/BRIEF.md) keeps its original wording as a record.

🤖 Generated with Claude Code

ymote and others added 3 commits October 11, 2026 23:47
Approved by the maintainer (11 Oct 2026): a real 42 MB, 36-slide .pptx with
one video wouldn't open, because the import guard refused every audio or
video part (held:).

Audit (deckcraft d0e57d7e): the decoders, deckcraft-media, are a
dependency of the engine crate only (and the egui UI); pptx, format,
render, anim and pdf don't depend on them. Inside the engine they are
called from media.info and media.posterFrame (cmd/media.rs 100, 123) and
from insert.audio and insert.video (cmd/insert.rs 450, 477), the four
commands the door already holds back (#448). The package reader keeps a
clip's bytes and its poster (pptx read/shapes.rs 259), the renderer draws
the poster or a film or speaker glyph (render lib.rs 559-571), and the
writers copy the bytes. No path the service runs reaches a decoder.

- guard: audio and video parts pass like any other; the content-type,
  extension, signature and manifest checks go. Every bound stays
  (entries, names, sizes, totals, ratio, nesting outside
  ppt/embeddings/, parity), and a deck's media counts against the
  session's 128 MiB when it opens (too_large:)
- tests: a deck with a video (red poster) and a sound opens from a
  package and a native file, draws the poster (and the glyphs without
  one), saves natively and reopens, exports a package with the clips'
  bytes unchanged, and a PDF; hostile clip bytes ride along on every path
  while the four media commands stay held; a deck past 128 MiB of media
  is too_large:; the old media-held tests turned around
- docs: apps/AGENTS.md, both apps READMEs, ADR 0013's deck row in both
  languages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #511 a deck with audio or video opens; held: now means a zip inside
the zip, or a part past the import guard's bounds (#448). Wording only, no
layout change:

- main.splash: the open view's explanation names a zip inside the
  presentation or parts larger or more unusual than Quick Deck takes, and
  says to save a simpler copy; the library card says "Can't open this
  file"
- dev fixture: the held deck's refusal is the guard's nested-zip words
- READMEs (both languages): the held state's description
- model test: the held explanation names no media

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote marked this pull request as ready for review October 13, 2026 02:25
ymote and others added 2 commits October 13, 2026 10:44
Review of #511 (minor): the 128 MiB media ceiling ran only after the
engine read, on deck.open and deck.cover, and on deck.run only with
commands; info, text, render and convert never checked it. The package
reader copies each media part out, so the peak could be twice the media.

- guard: before the engine reads, the declared total of a zip's binary
  parts (everything but its XML and relationship parts, and a native
  deck's manifest and mimetype) past 128 MiB is too_large:, in the session
  ceiling's words
- read_guarded and read_deck: the deck's own media against the same
  ceiling right after the engine reads, so all seven paths answer alike
  (media_ceiling; media_over and too_large_deck give one set of words to
  the guard, the paths and the session's Held::over)
- test: a native deck and a package just past 128 MiB are refused on all
  seven paths with no engine read; one just under opens on all seven

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #511 up to main after #500 (the macOS updater) merged; no
conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote

ymote commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on da0fcc5 (tools/ci-local.sh --only all --linux-host --offload, 17m54s, Darwin arm64; the ubuntu jobs and the macOS jobs' portable steps (their macOS steps ran here) on the Linux build host, Linux x86_64).

GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.

Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 2s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 6m14s
desktop.yml / desktop Desktop tools and scripts PASS 41s
desktop.yml / desktop (linux) Prepare the pinned framework sources PASS 0s
desktop.yml / desktop (linux) One copy of every shell source PASS -
desktop.yml / desktop (linux) In-app updater and installer handoff PASS 1m50s
desktop.yml / desktop (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 2s
desktop.yml / desktop (linux) The native apps' generated places match native-apps.json PASS 0s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 1s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 2m06s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 5s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 1s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 5s
phone.yml / home (linux) Prepare pinned sources and the reviewed runtime patch PASS 0s
phone.yml / home (linux) Verify the single runtime graph PASS 2s
phone.yml / home (linux) The native apps' generated places match native-apps.json PASS 0s
phone.yml / home (linux) The pinned octos revision PASS 0s
phone.yml / home (linux) Build octos (unless cached) PASS -
phone.yml / home (linux) Build Home, App Hub admission and runtime policy tests PASS 3m35s
phone.yml / home (linux) Test Home, App Hub admission and runtime policy PASS 42s
phone.yml / home (linux) Two-lane scenario (real kernel, scripted model) PASS 27s
phone.yml / home (linux) Build opt-in AppCard tests PASS 2m45s
phone.yml / home (linux) Test opt-in AppCard PASS 1s
phone.yml / home (linux) Build craft-engine relay and system-chat tests PASS 4m04s
phone.yml / home (linux) Test craft-engine relay and system chat PASS 5s
phone.yml / home (linux) Build linked Terminal tests PASS 2m30s
phone.yml / home (linux) Test linked Terminal PASS 1s
phone.yml / home (linux) Build Wasm and runtime policy tests PASS 1m16s
phone.yml / home (linux) Test Wasm and runtime policy PASS 11s
phone.yml / home Compile Home and its bundled modules PASS 8m41s
apps.yml / services (linux) python3 tools/setup.py PASS 0s
apps.yml / services (linux) Shared system app interface PASS 0s
apps.yml / services (linux) Kernel service PASS 22s
apps.yml / services (linux) OctoSense release updater PASS 12s
apps.yml / services (linux) App peers PASS 30s
apps.yml / services (linux) AI providers config PASS 7s
apps.yml / services (linux) Connected provider protocols and Linux host adapter PASS 48s
apps.yml / services (linux) L0 in-card chat (sys.chat, host side) PASS 10s
apps.yml / services (linux) System toolbox (workflow templates) PASS 1m33s
apps.yml / kernel-security (linux) python3 tools/setup.py PASS 0s
apps.yml / kernel-security (linux) The pinned octos revision PASS -
apps.yml / kernel-security (linux) Build octos (unless cached) PASS -
apps.yml / kernel-security (linux) Real-kernel tests PASS 45s
apps.yml / apps (linux) python3 tools/setup.py PASS 0s
apps.yml / apps (linux) Host services (Mail, AI providers, News) PASS 2m11s
apps.yml / apps (linux) Connected accounts and Rinx editor PASS 1m32s
apps.yml / apps (linux) Shell AI services (crates/ai-host) PASS 2m57s
apps.yml / apps (linux) AppCard runtime setup safeguards PASS 0s
apps.yml / apps (linux) AppCard clippy (own crates; shared runtime compiles) PASS 27s
apps.yml / apps (linux) AppCard transport and store PASS 8s
apps.yml / apps (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 3s
rom.yml / product (linux) python3 -m unittest discover -s tests -v SKIPPED (!) -
rom.yml / product (linux) Check generated Agent Binder client SKIPPED (!) -
rom.yml / product (linux) bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer (linux) npm ci --ignore-scripts --no-audit --no-fund SKIPPED (!) -
rom.yml / web-installer (linux) npm test SKIPPED (!) -
rom.yml / web-installer (linux) npx playwright install --with-deps chromium SKIPPED (!) -
rom.yml / web-installer (linux) npm run test:browser SKIPPED (!) -
rom.yml / web-installer (linux) python3 -m unittest discover -s tests -p test_installer_manifest.py -v SKIPPED (!) -
rom.yml / web-installer (linux) bash -n scripts/release.sh SKIPPED (!) -
linux-host / sandbox (linux) python3 tools/setup.py PASS 0s
linux-host / sandbox (linux) Process sandbox on Linux (Landlock, seccomp) PASS 1m57s

Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):

  • rom.yml / product (linux): python3 -m unittest discover -s tests -v: javac -version does not work here: install a JDK (17 or newer) and put its bin/ on PATH or set JAVA_HOME
  • rom.yml / product (linux): Check generated Agent Binder client: needs build-tools/35.0.0/aidl, platforms/android-35/framework.aidl in the Android SDK (no Android SDK found): install the SDK packages 'build-tools;35.0.0' and 'platforms;android-35' (sdkmanager) and set ANDROID_HOME
  • rom.yml / web-installer (linux): npm ci --ignore-scripts --no-audit --no-fund: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm test: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npx playwright install --with-deps chromium: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm run test:browser: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): python3 -m unittest discover -s tests -p test_installer_manifest.py -v: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): bash -n scripts/release.sh: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • GitHub runs Python 3.12; the Linux host used Python 3.14.4

@ymote
ymote merged commit 3f700a2 into main Oct 13, 2026
2 of 6 checks passed
@ymote
ymote deleted the feat/deck-media-opens branch October 13, 2026 03:18
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