Repository navigation
deck service v2: open documents for Quick Deck v2 - #503
Merged
Merged
Conversation
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
Coordinator decision 7 on #503: a part under ppt/embeddings/ (a chart's workbook, an embedded document) may be a zip, because neither the service nor deckcraft ever unzips it. It still counts against every other bound (entries, name, declared and actual size, ratio, totals) and the media checks. A zip anywhere else in the package, or in a native deck, stays held. - guard.rs: EMBEDDINGS and embedded_object (no empty, "." or ".." segment, no backslash), and the doc comment - tests: a .pptx with an embedded workbook opens on every path; nested zips elsewhere and look-alike spellings stay held; an embedded workbook that is a zip bomb is refused by its ratio, the same with its stream broken (never inflated); the bounds and media checks still apply to embedded parts - apps/AGENTS.md and ADR 0013's deck row in both languages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
A presentation editor rebuilt from the ten approved designs through App Flow, on the deck service's open documents (design/SERVICE.md, as built in #503): a library with the engine's covers; tabs; a toolbar with insert strips; a navigator with sections; a canvas with the engine's render, selection, handles and text edited in place; an inspector with Format, Layout, Theme and Notes; a sorter with sections; New from an outline, typed or drafted with the person's own provider (model.complete); Present on a dark stage inside the window; Export to .pptx, PDF and pictures; and every state the brief lists. Light and dark through shared tokens; state on mod.quickdeck_state; no fixture or replay path in the bundle. The manifest declares storage, files, deck and model, asks for the designs' 1536 x 1024 window (schema_minor 1) and moves to 0.2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
dev-fixture/engine.splash is a stand-in for the deck service that answers SERVICE.md's methods as #503 does (layouts by id, a notes preview in deck.slides, transitions in milliseconds, colour schemes by name); make_fixture.py fills the app's storage with the designs' sample decks and the engine's real catalogs. v1's fixture goes. tests/ui.py runs every designed screen and state in card-host, light and dark, beside its design, and the real engine's journey in a hidden desktop shell (on Linux on a private headless weston, as PDF Tools' ui.py does), then a restart. quickdeck_model_tests.rs runs the bundle's pure functions in a script VM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
Review of #503 (C1, critical): the guard counted a zip's entries from the end record's total while the engine's zip reader (the zip crate, 3.0.0) counts the entries on this disk, so a zip saying 1 and 2 hid a second entry, a movie or a zip bomb, from every check on all seven paths. Fixed as a class: the guard takes a zip only in a shape no two readers take differently, then has the engine's own reader read it. - the end record: the last one, its comment ending the file, and no second end record after it (in its comment or overlapping it) - its counts agree (on this disk and in all), in the ZIP64 record too; the ZIP64 record ends at its locator and needs no newer version than wrote it; every disk number is 0 - the directory fills exactly the space it declares, up to the end record (or the ZIP64 record), every header inside it - every entry starts before the directory, the first at offset 0: nothing prepended for the reader to relocate - every extra field read as that reader reads it (ZIP64 values applied the same way, a 24-byte field giving all three); what it would refuse (NTFS or extended times of another shape, AES, encryption, other methods) and Info-ZIP Unicode names, which rename an entry, refused first, so the reader never falls back to another end record - then read_alike: zip::ZipArchive::new on the same bytes must find no offset, the same directory and the same entries (names, sizes, offsets, data start, method), else damaged - the format comes from the engine's own sniffers, and the media checks read [Content_Types].xml and presentation.json whenever present - a test keeps the service on deckcraft's zip crate in Cargo.lock Also from the review: engine-area builds its quota refusals from one constant and exports no_room (M1), which codes::area uses (the pdf service is unchanged); a picture whose header gives no size fails closed (N1). And deck.hit takes an empty selected list as no selection. Hostile fixtures, each refused before the engine reads a byte: the review's proof of concept (movie and bomb), short and long directories, a header past the count, two end records, one in the comment, data before the zip (relative and absolute offsets), twin names, a ZIP64 field that moves an entry onto a hidden movie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deck host service now keeps open documents for Quick Deck v2, as apps/quickdeck/design/SERVICE.md specifies: deck.open and deck.create return a handle, and the methods on it run reviewed engine commands through the same door as deck.run, each call one undo step. The service's own import guard checks every file before deckcraft reads a byte of it (#448). - docs.rs: the open-document table (a thread_local; at most 8 open a caller and 32 in all; released on close, when the isolate closes, when the storage scope changes, and after 15 idle minutes) - gate.rs: one gate for every method (Door::admit_method with the method's own ceiling, the run ceilings after every command), all or nothing, one labelled undo step a call (at most 100), rev and changed from the engine's copy-on-write slide pointers - guard.rs: the import guard (bounded central directory, every entry inflated against its declared size, nested zips and audio or video held, an encrypted package protected, a legacy .ppt unsupported) - reading, objects, slides, writing, covers, cache, pictures, args, codes: the methods, Present's order and build steps (deckcraft-anim), the render cache (16 MiB, 64 files, 32 covers) and stable error codes - engine-area door: admit_method and Unread, generic; deck.run's gate and messages are unchanged - warm.rs: a call waits for a running warm-up, so the engine's fonts load on one thread at a time - deckcraft-anim and deckcraft-fonts at deckcraft's pinned revision - 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>
Coordinator decision 7 on #503: a part under ppt/embeddings/ (a chart's workbook, an embedded document) may be a zip, because neither the service nor deckcraft ever unzips it. It still counts against every other bound (entries, name, declared and actual size, ratio, totals) and the media checks. A zip anywhere else in the package, or in a native deck, stays held. - guard.rs: EMBEDDINGS and embedded_object (no empty, "." or ".." segment, no backslash), and the doc comment - tests: a .pptx with an embedded workbook opens on every path; nested zips elsewhere and look-alike spellings stay held; an embedded workbook that is a zip bomb is refused by its ratio, the same with its stream broken (never inflated); the bounds and media checks still apply to embedded parts - apps/AGENTS.md and ADR 0013's deck row in both languages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #503 (C1, critical): the guard counted a zip's entries from the end record's total while the engine's zip reader (the zip crate, 3.0.0) counts the entries on this disk, so a zip saying 1 and 2 hid a second entry, a movie or a zip bomb, from every check on all seven paths. Fixed as a class: the guard takes a zip only in a shape no two readers take differently, then has the engine's own reader read it. - the end record: the last one, its comment ending the file, and no second end record after it (in its comment or overlapping it) - its counts agree (on this disk and in all), in the ZIP64 record too; the ZIP64 record ends at its locator and needs no newer version than wrote it; every disk number is 0 - the directory fills exactly the space it declares, up to the end record (or the ZIP64 record), every header inside it - every entry starts before the directory, the first at offset 0: nothing prepended for the reader to relocate - every extra field read as that reader reads it (ZIP64 values applied the same way, a 24-byte field giving all three); what it would refuse (NTFS or extended times of another shape, AES, encryption, other methods) and Info-ZIP Unicode names, which rename an entry, refused first, so the reader never falls back to another end record - then read_alike: zip::ZipArchive::new on the same bytes must find no offset, the same directory and the same entries (names, sizes, offsets, data start, method), else damaged - the format comes from the engine's own sniffers, and the media checks read [Content_Types].xml and presentation.json whenever present - a test keeps the service on deckcraft's zip crate in Cargo.lock Also from the review: engine-area builds its quota refusals from one constant and exports no_room (M1), which codes::area uses (the pdf service is unchanged); a picture whose header gives no size fails closed (N1). And deck.hit takes an empty selected list as no selection. Hostile fixtures, each refused before the engine reads a byte: the review's proof of concept (movie and bomb), short and long directories, a header past the count, two end records, one in the comment, data before the zip (relative and absolute offsets), twin names, a ZIP64 field that moves an entry onto a hidden movie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review of #503 (major, older than the C1 fix): every .pptx LibreOffice writes was refused as unsupported. deckcraft_pptx::sniff looks for [Content_Types].xml in a package's first 4 KiB only, and LibreOffice writes that part late, so the guard's classification and the engine's open_presentation both missed it, though deckcraft_pptx::import reads it. - guard: a native deck by deckcraft's own marker (deckcraft_format::sniff, which the engine tries first), else an Office package by its [Content_Types].xml entry in the directory the guard verified (and the engine's zip reader read alike) - lib.rs engine_read: the guard's verdict picks the reader, each branch a step-for-step copy of open_presentation after its sniff (crates/engine/src/cmd/file.rs 58-78 at d0e57d7e): load for a native deck (59-61), import then repair for a package (62-66), and open_presentation itself for outline text (67-77) - a test compares engine_read with open_presentation on every format, so a step the pinned function grows fails it; a package rewritten with its content types last, past 4 KiB, opens on all seven paths, and the same layout with a movie, a zip bomb or disagreeing counts is refused before the engine reads a byte Nit: a ZIP64 end record carries at most 1 KiB of extensible data (the zip crate allocates it whole), else held:, with fixtures at the cap and past it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
force-pushed
the
feat/deck-service-v2
branch
from
October 11, 2026 20:28
099750f to
a1a0b49
Compare
ymote
marked this pull request as ready for review
October 11, 2026 20:29
Contributor
Author
|
Local CI passed on a1a0b49 ( 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.
Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):
Notes:
Native GitHub acceptance passed on the same head: embedded-browser.yml. |
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
A presentation editor rebuilt from the ten approved designs through App Flow, on the deck service's open documents (design/SERVICE.md, as built in #503): a library with the engine's covers; tabs; a toolbar with insert strips; a navigator with sections; a canvas with the engine's render, selection, handles and text edited in place; an inspector with Format, Layout, Theme and Notes; a sorter with sections; New from an outline, typed or drafted with the person's own provider (model.complete); Present on a dark stage inside the window; Export to .pptx, PDF and pictures; and every state the brief lists. Light and dark through shared tokens; state on mod.quickdeck_state; no fixture or replay path in the bundle. The manifest declares storage, files, deck and model, asks for the designs' 1536 x 1024 window (schema_minor 1) and moves to 0.2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
dev-fixture/engine.splash is a stand-in for the deck service that answers SERVICE.md's methods as #503 does (layouts by id, a notes preview in deck.slides, transitions in milliseconds, colour schemes by name); make_fixture.py fills the app's storage with the designs' sample decks and the engine's real catalogs. v1's fixture goes. tests/ui.py runs every designed screen and state in card-host, light and dark, beside its design, and the real engine's journey in a hidden desktop shell (on Linux on a private headless weston, as PDF Tools' ui.py does), then a restart. quickdeck_model_tests.rs runs the bundle's pure functions in a script VM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
The branch is rebased on main 202d7cc, which has the deck service. The shell runs (light, dark, restart) passed again with a shell built from that tree, with no script error or overrun; nothing changed for the app. The Linux frames and the receipt now name that head and the merged service instead of the earlier local merge with the service's branch. The card-host frames stand: the rebase changed no file of the app, its fixture or its tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
Quick Deck v2's inspector shows these as off, or by name only, today. The
service now reads each from the open document's model (deckcraft-model),
read-only, with no new engine command:
- deck.themes: color_schemes as [{name, colors}], the engine's built-in
schemes (builtin_color_schemes, which design.themes names only) with
their twelve colours in the engine's order
- deck.open and deck.state: theme (state), color_scheme, font_scheme (the
first master's, as document.inspect reports the theme) and footer
{slide_numbers, footer, footer_text, date, hide_on_title}, the keys
deck.design {footer} takes; in deck.state too, so an undo shows
- deck.format_state: anchor and autofit as the box draws them
(resolve::body: its own, else its layout's and master's, else the
engine's defaults), in deck.format's words
- deck.slide: a chart's legend, data_labels, gridlines and palette, and a
table's options {header_row, total_row, banded_rows, first_column,
last_column, banded_columns}, as deck.chart and deck.table take them
Review nits on #503:
- the reader-parity test now includes a package that repair visibly
changes (a slide 5,000 points wide, which repair narrows to 4,000), so
dropping repair from engine_read fails it (tried, then restored)
- an Office package whose main part is a document or a workbook (.docx,
.xlsx, written by Office or laid out as LibreOffice writes one) is
unsupported:, not damaged:
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 12, 2026
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.
Why
Quick Deck v2 calls an app-facing deck service with open documents, as
apps/quickdeck/design/SERVICE.md(branchdesign/quickdeck-v2, 717f951) specifies; its "Decisions" section settles the open questions. Until now the deck service was stateless: every call opened a file, ran, and dropped the engine session. That is right for the system agent's two tools, and wrong for an editor that needs its edits, undo history and selection to last between calls. This PR builds that contract, modelled on the pdf service v2 (#472/#479).What
src/docs.rs).deck.openanddeck.createkeep onedeckcraft_engine::Sessionper document under a handle (lowercase hex). A document belongs to the caller app, its storage scope and the handle; anyone else's handle isunknown_doc:. At most 8 open per caller and 32 in all (too_many_open:). A document is released bydeck.close(unsaved:unlessdiscard), when its isolate closes (a listener on App Hub'sservices::on_isolate_closed), when its storage scope changes or goes (swept on every call), and after 15 idle minutes for a caller with no isolate. The table is athread_localon the UI thread. An app session turns off the engine's autocorrect and smart quotes, so text is stored as typed.src/gate.rs). Each method builds every engine command and its parameters itself and refuses a key it doesn't take. ThenDoor::admit_methodadmits the whole call before any command runs. It is the doordeck.runuses, with the sameREVIEWEDlimits and held commands, and it counts against the method's own ceiling, with its reason beside it.deck.run's run ceilings (slides, shapes, cells, chart points, characters, media, geometry, copies) hold after every command, counted per slide between calls. A refused command puts the document back as it was: deck, selection and history.HistoryEntrythrough its publicHistory, and the history keeps at most 100 steps. A call that changes nothing adds no step and no revision.revandchangedcome from the engine's copy-on-write slide pointers, compared by content where the engine copied a slide it didn't change. A deck-wide change lists every slide, and a slide whose position changed is listed when it shows its number. Every change answers{rev, changed, edited, can_undo, can_redo}.open,create,close,state,save,slides,slide,hit,view,layouts,themes,fonts,shapes,text {doc},format_state,cover,slides_op,notes,transition,insert,set_text(replaces only the span that changed, so the rest keeps its runs),format,shape,picture,table,chart,design,undo,redo,showandexport. The v1 file methods and the system agent'sdeck.infoanddeck.runare unchanged (tools.jsonand the agent's deck tools are untouched).src/guard.rs, decision 4). It runs before the engine sees a byte, on every path that opens a file:deck.open,deck.cover, the v1 file methods and so the agent's tools. The zip's central directory is read with bounds: 10,000 entries, names of 1,024 bytes, 64 MiB an entry, 256 MiB in all, 100:1 past 100 KiB. It is read only in a shape the engine's own zip reader takes the same way, and then that reader (thezipcrate deckcraft opens decks with) must read the directory exactly as the guard did. The section below has the details. The guard decides what the file is: a native deck by deckcraft's own marker, or an Office package by its[Content_Types].xmlentry in the directory it verified. That verdict picks the engine's reader (engine_read, below), so a package written by LibreOffice, which puts that part late, opens too. Every entry is inflated against its declared size. Any audio or video part (by extension, declared content type, container signature, or a native deck's media type) isheld:. So is a zip inside the zip, with one exception: an Office package's embedded object underppt/embeddings/(a chart's workbook, an embedded document) is allowed, because neither the service nor deckcraft ever unzips it. deckcraft's only zip readers are its package and native readers, and it reads a chart from its cached values. Such a part still counts against every other bound: entries, name, declared and actual size, the ratio and the totals. A zip anywhere else in the package, or in a native deck, staysheld:. A lying or broken zip isdamaged:. An OLE file isprotected:with anEncryptedPackagestream andunsupported:otherwise; so is a PDF or a zip that isn't a presentation. Engine-side bounds stay with Engine robustness gaps from the command-door caps review: deck media, cad DXF splines, film export memory, photo.run caps #448, and deckcraft is not changed.deck.show(show_order, the click builds of each slide,loop,advance_afterwhen the show uses timings) anddeck.view {step}(each build at its end state), fromdeckcraft-animat deckcraft's pinned revision. It is added to the root[workspace.dependencies], and so aredeckcraft-fonts(fordeck.fontsandmissing_fonts) anddeckcraft-pptx(the package readerengine_readcalls), all at the same revision. The guard also depends onzipwith deckcraft's own version and features.src/cache.rs,src/covers.rs,src/writing.rs). A drawing is 48–3840 px wide, at most 4096 px a side and 16 MP. A call names at most 500 slides or shapes. A fill sets at most 1,024 table cells, notes take at most 20,000 characters, and a picture is at most 50 MP. A PDF rasters at most 160 MP; an image export writes at most 64 slides and 256 MP. These caps are checked before anything is written. The render cache keeps 16 MiB and 64 files, at most 32 of them covers. Slide drawings go before covers, each oldest first, and the cache is cleared before a write would fail withstorage_full:. Every write goes through a staging folder, all or nothing. A save replaces only the deck's own file, and only from the foreground; anything else is a new file.src/codes.rs), as SERVICE.md lists.crates/engine-areadoor (small and generic):Door::admit_method(method, cmds, most, area, unread)returns a typedRefusal. The method's own ceiling gives aCountrefusal; the rest go through the same checks asadmit.Unreadlets a method run afilecommand without its file access (shape.fill,design.background: refused if any reading key is present, even null), anddeck.runstill refuses them.admitandadmit_allkeep their exact messages.src/lib.rsengine_read). deckcraft'sopen_presentationsniffs a package by[Content_Types].xmlin its first 4 KiB only, and LibreOffice writes that part late (entry 13 to 78, 7 to 400 KB in). Soopen_presentationrefused every LibreOffice package even thoughdeckcraft_pptx::importreads them.engine_readtakes the guard's verdict and runs, step for step, whatopen_presentationruns after its sniff (crates/engine/src/cmd/file.rslines 58-78 at d0e57d7e, named in the code to re-check on a repin):deckcraft_format::load(59-61);deckcraft_pptx::import, thendeckcraft_format::repair(62-66);open_presentationitself (67-77).A test compares
engine_readwithopen_presentationon every format, so a step the pinned function grows fails it.zipcrate allocates it whole; beyond that isheld:.NO_ROOM, and exportsno_room(), which the deck'scodes::areauses instead of matching English words. The pdf service is unchanged.invalid:, "can't read the picture's size") rather than let past the 50 MP cap.deck.hittakes an emptyselectedlist as no selection.src/warm.rs: a call now waits for a running warm-up before the engine works. The engine's font database can't load one family from two threads at once: a lookup on one thread finds none of a family's faces, then finds its files already added by the other thread, and falls back to the last-resort face. The new render-after-reopen test caught a slide title drawn in Ubuntu Light this way, a drawing the render cache or a cover would keep. The wait is never longer than the warm-up itself, most of which (the catalog scan, the default families) the call would otherwise pay.apps/AGENTS.md, the deck service paragraph in both apps READMEs, and ADR 0013's deck row in both languages.tools.json, the system agent's deck tools. Engine work stays on the UI thread (Engine host services run inline on the UI thread under the SERVICES lock #399).Safety review per command
Classes are from
skill/safety.json(engine d0e57d7e). "Most" is the method's own ceiling on engine commands a call; every command still passes the door's allowlist and itsLimits, and no call is split to dodge a cap.open,statedocument.inspect {}(andopen_presentationon bytes the guard passed, not a command)createfile.new {theme, size, blank}; per slideslide.new {at, layout?, title?, body?}andslide.notes {text, index}, orslide.fromOutline {text}; thendocument.inspectfile.newmakes an in-memory deck (no path), within the door's slide area; the outline is at most 200 slides (OUTLINE_SLIDES); the service writes the filefile.new, then aslide.newand its notes for each of 200 slides)close,save,view,layouts,cover,fonts,text,show,exportsave_bytes,pdf_bytes,render_slide,render_layout,render_png,slides_to_outline, deckcraft-anim,FontDb::familiesfile.save,file.saveAs,file.saveTemplate(host) andfile.export(file) never runslidesdocument.inspect {}slideslide.inspect {index}hitslide.go,edit.select {ids}, thenSession::hit_testformat_stateslide.go,edit.selectortext.edit+text.selectParagraph,format.state {},text.exitthemes,shapesdesign.themes {},shape.presets {}on a fresh sessionslides_opnew / delete / sectionslide.new;slide.delete {ids}(never every slide);section.add,section.rename,section.remove(neverslides: true),section.moveslides_opduplicate / moveslide.duplicate {index}orslide.move {from, to}per slideslides_ophide / layoutslide.selectSlides {ids}, then oneslide.hideorslide.layouton them allslides_opresetslide.go {index},slide.resetper slidenotesslide.notes {text, index}transitionslide.selectSlidesandtransition.set; orslide.go,transition.set,transition.applyAllinsertslide.go;insert.textBox,shape.insert,insert.picture {path, rect},insert.table {rows, cols, rect, data}orinsert.chart; thentable.styleorchart.optionsinsert.picturefileinsert.pictureis a reviewed read (REVIEWED.file_reads,pathonly, neverdata): the service checks the path is inside storage, a file ≤ 64 MiB, a picture by its signature and ≤ 50 MP, then the door resolves it again through links. Door limits: 5,625 cells, 10,000 chart points, 100,000 ptset_textslide.go,text.edit,text.select,text.insert,text.select,text.delete,text.exit(ortext.setfor an empty box)formatslide.go,edit.selectortext.edit+text.select, up to 17format.*, up to 8format.indent/format.outdent,text.exitshapeslide.go, then one ofshape.setBounds,shape.move,shape.rotate,shape.flip,arrange.*,edit.duplicate,edit.delete,shape.fill,shape.line,shape.effects,shape.quickStyle,shape.lock,shape.visibleshape.fillfileshape.fillruns asUnread: colour, gradient or none only; the door refusespictureorpathfrom a method, anddeck.runrefusesshape.fillas beforepictureslide.go, thenpicture.change {path},picture.crop,picture.adjust,picture.resetorshape.altTextpicture.changefilepicture.changeis the reviewed read, checked asinsert.picturetableslide.go, onetable.*chartslide.go,chart.type,chart.data,chart.optionsdesignslide.go,design.theme,design.colors,design.fonts;design.slideSize; per slide namedslide.goanddesign.background, or one for all;design.headerFooterdesign.backgroundfiledesign.backgroundruns asUnread: colour, gradient or reset only, never a picture; slide area ≤ 1920 × 1080 ptundo,redoedit.undo {},edit.redo {}Import guard: reading alike
An independent review found that the guard counted a zip's entries from the end record's total, while the engine's zip reader (
zip3.0.0) counts the entries on this disk. A zip saying 1 and 2 therefore hid a second entry (a movie, a zip bomb) from every check, on all seven paths. The fix closes the class: the guard takes a zip only in a shape no two readers take differently, then has the engine's own reader read it.damaged:PK-led prefix with offsets counting from the zip, and the same with offsets counting from the fileread_alike:zip::ZipArchive::newon the same bytes must find no offset, the same directory and the same entries (raw names, sizes, header and data offsets, method, no encryption)deckcraft_format::sniff, which the engine tries first), else a package by its[Content_Types].xmlentry in the verified directory; the verdict picks the engine's reader; the media checks read[Content_Types].xmlandpresentation.jsonwhenever presentheld:The reader the guard consults is the engine's own: the deck service depends on
zipwith deckcraft's version and features, and a test checks thatCargo.lockgives it the samezipasdeckcraft-pptxanddeckcraft-format.Never reached by any method:
file.open,file.close,file.export,file.recovery.*(file);file.save,file.saveAs,file.saveTemplate(host);show.fromStart,show.fromCurrent,media.play/pause/seek/stop/toggle(device). The held ids stay held:insert.audio,insert.video,media.info,media.posterFrame,file.openBytes(#448). deckcraft has nocodeornetworkcommands.Tests
All on the Linux build host (x86_64), on this branch rebased onto main
32dcfbbf(after #502), at heada1a0b496.python3 tools/setup.py --check --cargoand both shell graph guards (tools/check-shell-graph.sh -p octosense, and-p octosense-homefromphone/) pass;Cargo.lockis main's plus the new deckcraft crates.cargo test --locked -p octosense-deck-service -p octosense-engine-area: the deck service's lib tests, 84 passed and 1 ignored (the timing test);first_call1 andskill1 passed; engine-area 28 passed (3 new door tests, 1 forno_room). The v1 tests and the door tests are unchanged.cargo clippy --locked -p octosense-deck-service -p octosense-engine-area --all-targets --no-deps: no findings in either crate.phone/:cargo test --locked --features mobile-apps,craft-engines -p octosense-shell host_tools, 126 passed;... system_chat, 82 passed.python3 tools/setup.py --check --cargopasses, on macOS and on the host.cargo test --release --locked -p octosense-deck-service --lib -- --ignored --nocapture timingsruns on a 30-slide deck, with fonts loaded first as the shell's warm-up loads them. Opening took 2.8 ms (import guard andread_alikeincluded) and the first slide at 1280 px 8.1 ms: 12.2 ms together, against an asserted limit of 1 s. The typical edits, against an asserted 100 ms: set_text 0.2 ms, format 0.1 ms, slide inspect 0.2 ms, slides inspect 1.3 ms, move 0.1 ms, and notes, hit, undo and redo under 0.1 ms each. A view after an edit took 6.8 ms, layouts 7.2 ms, save 1.1 ms, the.pptxexport 5.2 ms, the PDF export at 72 dpi 164 ms and a cover 3.3 ms.src/doc_tests.rs,src/guard.rs,crates/engine-area/src/door.rs):held:and every import guard refusal, before the engine reads a byte, ondeck.open,deck.coverand the v1 methods: a zip bomb, a lying declared size, more than 10,000 entries, a nested zip outsideppt/embeddings/, a chart workbook that is a zip bomb (refused by its directory entry's ratio, and the same with its stream broken, so it was never inflated or unzipped), media by name, type and signature, an encrypted OLE package, a legacy.ppt, broken zips;.pptxwith an embedded workbook (ppt/embeddings/Microsoft_Excel_Worksheet1.xlsx) opens, draws a cover and answersdeck.info. An embedded object is still refused over a bound (declared size, name, actual size) and as media, and spellings that only look embedded stayheld:(..,., empty segments, backslashes). The guard also passes the engine's own 200- and 500-slide decks;set_textkeeping the formatting outside the changed span;storage_full:;saving_and_a_render_after_a_save_and_a_reopenfailed in 2 of 3 full runs on the host, with the slide title drawn in the last-resort face. Every run since then has passed, 13 in a row.Not run: no shell or desktop run, and no Quick Deck UI (the app is another PR). The deck crates' tests did not run on macOS; only
setup.py --check --cargoran there. The reviewer's LibreOffice corpus check didn't run either: LibreOffice is not on the build host, and nothing may be installed there. The synthetic late-content-types package stands in for it. There are no timings on macOS. Engine crate tests are not in CI (#412).Contract
The contract is
apps/quickdeck/design/SERVICE.mdondesign/quickdeck-v2. Where this PR differs from the first contract, it follows that file's section "As built in #503", which the coordinator accepted on 11 Oct 2026 and the UI agent commits there. That section covers:shape.fillanddesign.backgroundpassing the gate as method-onlyUnreadcommands;insert.table {data}, anddeck.set_text's commands;slide.selectSlidesandslide.gowhere the engine copies the current slide;unsupported:ordamaged:);deckcraft-fonts;ppt/embeddings/, within every other bound, and isheld:anywhere else.🤖 Generated with Claude Code