Skip to content

deck service v2: open documents for Quick Deck v2 - #503

Merged
ymote merged 4 commits into
mainfrom
feat/deck-service-v2
Oct 11, 2026
Merged

ymote merged 4 commits into
mainfrom
feat/deck-service-v2

Conversation

@ymote

@ymote ymote commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Why

Quick Deck v2 calls an app-facing deck service with open documents, as apps/quickdeck/design/SERVICE.md (branch design/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

  • Open documents (src/docs.rs). deck.open and deck.create keep one deckcraft_engine::Session per document under a handle (lowercase hex). A document belongs to the caller app, its storage scope and the handle; anyone else's handle is unknown_doc:. At most 8 open per caller and 32 in all (too_many_open:). A document is released by deck.close (unsaved: unless discard), when its isolate closes (a listener on App Hub's services::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 a thread_local on the UI thread. An app session turns off the engine's autocorrect and smart quotes, so text is stored as typed.
  • One gate (src/gate.rs). Each method builds every engine command and its parameters itself and refuses a key it doesn't take. Then Door::admit_method admits the whole call before any command runs. It is the door deck.run uses, with the same REVIEWED limits 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.
  • One undo step per call. The steps the engine pushed for a call are folded into one labelled HistoryEntry through its public History, and the history keeps at most 100 steps. A call that changes nothing adds no step and no revision. rev and changed come 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}.
  • Methods: 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, show and export. The v1 file methods and the system agent's deck.info and deck.run are unchanged (tools.json and the agent's deck tools are untouched).
  • Import guard (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 (the zip crate 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].xml entry 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) is held:. So is a zip inside the zip, with one exception: an Office package's embedded object under ppt/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, stays held:. A lying or broken zip is damaged:. An OLE file is protected: with an EncryptedPackage stream and unsupported: 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.
  • Present: deck.show (show_order, the click builds of each slide, loop, advance_after when the show uses timings) and deck.view {step} (each build at its end state), from deckcraft-anim at deckcraft's pinned revision. It is added to the root [workspace.dependencies], and so are deckcraft-fonts (for deck.fonts and missing_fonts) and deckcraft-pptx (the package reader engine_read calls), all at the same revision. The guard also depends on zip with deckcraft's own version and features.
  • Caps and cache (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 with storage_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.
  • Errors start with a stable code (src/codes.rs), as SERVICE.md lists.
  • crates/engine-area door (small and generic): Door::admit_method(method, cmds, most, area, unread) returns a typed Refusal. The method's own ceiling gives a Count refusal; the rest go through the same checks as admit. Unread lets a method run a file command without its file access (shape.fill, design.background: refused if any reading key is present, even null), and deck.run still refuses them. admit and admit_all keep their exact messages.
  • The engine's reader, picked by the guard (src/lib.rs engine_read). deckcraft's open_presentation sniffs a package by [Content_Types].xml in its first 4 KiB only, and LibreOffice writes that part late (entry 13 to 78, 7 to 400 KB in). So open_presentation refused every LibreOffice package even though deckcraft_pptx::import reads them. engine_read takes the guard's verdict and runs, step for step, what open_presentation runs after its sniff (crates/engine/src/cmd/file.rs lines 58-78 at d0e57d7e, named in the code to re-check on a repin):
    • a native deck goes through deckcraft_format::load (59-61);
    • a package goes through deckcraft_pptx::import, then deckcraft_format::repair (62-66);
    • outline text goes through open_presentation itself (67-77).
      A test compares engine_read with open_presentation on every format, so a step the pinned function grows fails it.
  • ZIP64 end record: at most 1 KiB of extensible data, since the zip crate allocates it whole; beyond that is held:.
  • Review fixes beside the guard:
    • engine-area builds its quota refusals from one constant, NO_ROOM, and exports no_room(), which the deck's codes::area uses instead of matching English words. The pdf service is unchanged.
    • A picture whose header gives no size is refused (invalid:, "can't read the picture's size") rather than let past the 50 MP cap.
    • deck.hit takes an empty selected list 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.
  • 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.
  • Not changed: the pdf service, deckcraft, 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 its Limits, and no call is split to dodge a cap.

Method Engine commands Class Decision Most
open, state document.inspect {} (and open_presentation on bytes the guard passed, not a command) safe read; a read never changes the document 1
create file.new {theme, size, blank}; per slide slide.new {at, layout?, title?, body?} and slide.notes {text, index}, or slide.fromOutline {text}; then document.inspect safe file.new makes 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 file 401 (file.new, then a slide.new and its notes for each of 200 slides)
close, save, view, layouts, cover, fonts, text, show, export none: save_bytes, pdf_bytes, render_slide, render_layout, render_png, slides_to_outline, deckcraft-anim, FontDb::families — engine encoders and renderers touch no file; the service writes, staged; file.save, file.saveAs, file.saveTemplate (host) and file.export (file) never run 0
slides document.inspect {} safe read 1
slide slide.inspect {index} safe read 1
hit slide.go, edit.select {ids}, then Session::hit_test safe the selection only, put back 2
format_state slide.go, edit.select or text.edit + text.selectParagraph, format.state {}, text.exit safe the selection only and a read, put back 5
themes, shapes design.themes {}, shape.presets {} on a fresh session safe catalog reads 1
slides_op new / delete / section slide.new; slide.delete {ids} (never every slide); section.add, section.rename, section.remove (never slides: true), section.move safe ok 1
slides_op duplicate / move slide.duplicate {index} or slide.move {from, to} per slide safe the session ceilings after each 500 (one a slide named)
slides_op hide / layout slide.selectSlides {ids}, then one slide.hide or slide.layout on them all safe the selection, then one command and one snapshot 2
slides_op reset slide.go {index}, slide.reset per slide safe the slide current first, so no other slide is copied per command 1,000 (two a slide)
notes slide.notes {text, index} safe at most 20,000 characters 1
transition slide.selectSlides and transition.set; or slide.go, transition.set, transition.applyAll safe the engine's catalog kinds, 0 to 59 s 3
insert slide.go; insert.textBox, shape.insert, insert.picture {path, rect}, insert.table {rows, cols, rect, data} or insert.chart; then table.style or chart.options safe; insert.picture file insert.picture is a reviewed read (REVIEWED.file_reads, path only, never data): 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 pt 3
set_text slide.go, text.edit, text.select, text.insert, text.select, text.delete, text.exit (or text.set for an empty box) safe ≤ 100,000 characters, ≤ 1,000 line breaks 7
format slide.go, edit.select or text.edit + text.select, up to 17 format.*, up to 8 format.indent/format.outdent, text.exit safe door limits on bullet size and text outline 29
shape slide.go, then one of shape.setBounds, shape.move, shape.rotate, shape.flip, arrange.*, edit.duplicate, edit.delete, shape.fill, shape.line, shape.effects, shape.quickStyle, shape.lock, shape.visible safe; shape.fill file shape.fill runs as Unread: colour, gradient or none only; the door refuses picture or path from a method, and deck.run refuses shape.fill as before 2
picture slide.go, then picture.change {path}, picture.crop, picture.adjust, picture.reset or shape.altText safe; picture.change file picture.change is the reviewed read, checked as insert.picture 2
table slide.go, one table.* safe door limits on selected and filled cells 2
chart slide.go, chart.type, chart.data, chart.options safe ≤ 10,000 data points 4
design per master in use (≤ 16) slide.go, design.theme, design.colors, design.fonts; design.slideSize; per slide named slide.go and design.background, or one for all; design.headerFooter safe; design.background file design.background runs as Unread: colour, gradient or reset only, never a picture; slide area ≤ 1920 × 1080 pt 1,066
undo, redo edit.undo {}, edit.redo {} safe one folded step 1

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 (zip 3.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.

Way two readers part Closed by Fixture (refused before the engine reads a byte)
Entry count: total vs this disk (zip32), and the same in the ZIP64 record The counts must agree, else damaged: The review's proof of concept, with the hidden second entry a movie and then a zip bomb; ZIP64 counts disagreeing. A test shows the engine's reader seeing the movie.
Declared directory size never checked The directory must fill exactly the space it declares, up to the end record (or ZIP64 record), with every header inside it A directory shorter and one longer than declared, a header past the count, and a directory that starts elsewhere
Which end record (several signatures, one in a comment, bytes after it) The last end record, its comment ending the file, and no second end-record signature after it Two end records back to back; one hidden in the other's comment; bytes after the comment
Prepended data (the reader's archive-offset relocation) Every entry must start before the directory, and the first at offset 0 A PK-led prefix with offsets counting from the zip, and the same with offsets counting from the file
Extra fields (a ZIP64 field of 24+ bytes overrides all three values; an Info-ZIP Unicode path renames an entry) Extras are read with the reader's own rules. What it would refuse is refused first: other NTFS or extended-time shapes, AES, other methods, encryption. Unicode path and comment fields are refused too. A ZIP64 field moving an innocent slide onto a hidden movie, now found as the engine would; every refused extra shape; a ZIP64 field too short
Anything else (twin names it merges, methods, data offsets) read_alike: zip::ZipArchive::new on the same bytes must find no offset, the same directory and the same entries (raw names, sizes, header and data offsets, method, no encryption) "é" in UTF-8 and in code page 437, which the reader merges; the same name twice
Reader fallback to another end record The guard refuses everything the reader would refuse in this end record, the ZIP64 record and every header. So the reader takes that end record first and never reads counts the guard didn't validate. covered by the rows above
Format A native deck by deckcraft's marker (deckcraft_format::sniff, which the engine tries first), else a package by its [Content_Types].xml entry in the verified directory; the verdict picks the engine's reader; the media checks read [Content_Types].xml and presentation.json whenever present a deckcraft package rewritten with its content types last and past 4 KiB opens on all seven paths; the same layout with a movie, a zip bomb or disagreeing counts is refused
A ZIP64 end record's extensible data (allocated whole by the reader) At most 1 KiB, else held: at the cap (passes) and one byte past it

The reader the guard consults is the engine's own: the deck service depends on zip with deckcraft's version and features, and a test checks that Cargo.lock gives it the same zip as deckcraft-pptx and deckcraft-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 no code or network commands.

Tests

All on the Linux build host (x86_64), on this branch rebased onto main 32dcfbbf (after #502), at head a1a0b496. python3 tools/setup.py --check --cargo and both shell graph guards (tools/check-shell-graph.sh -p octosense, and -p octosense-home from phone/) pass; Cargo.lock is 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_call 1 and skill 1 passed; engine-area 28 passed (3 new door tests, 1 for no_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.
  • From 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 --cargo passes, on macOS and on the host.
  • cargo test --release --locked -p octosense-deck-service --lib -- --ignored --nocapture timings runs on a 30-slide deck, with fonts loaded first as the shell's warm-up loads them. Opening took 2.8 ms (import guard and read_alike included) 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 .pptx export 5.2 ms, the PDF export at 72 dpi 164 ms and a cover 3.3 ms.
  • What the new tests cover (src/doc_tests.rs, src/guard.rs, crates/engine-area/src/door.rs):
    • every method's happy path;
    • unknown and foreign handles, and the 9th open of a caller and the 33rd in all;
    • release on close, on isolate close (a real Makepad isolate with a storage scope), on a scope change and when idle;
    • path escapes and every cap;
    • held: and every import guard refusal, before the engine reads a byte, on deck.open, deck.cover and the v1 methods: a zip bomb, a lying declared size, more than 10,000 entries, a nested zip outside ppt/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;
    • a .pptx with an embedded workbook (ppt/embeddings/Microsoft_Excel_Worksheet1.xlsx) opens, draws a cover and answers deck.info. An embedded object is still refused over a bound (declared size, name, actual size) and as media, and spellings that only look embedded stay held: (.., ., empty segments, backslashes). The guard also passes the engine's own 200- and 500-slide decks;
    • one undo step per call for every change method, undo and redo across calls with the limit of 100, and a failing call that changes nothing;
    • set_text keeping the formatting outside the changed span;
    • a render after a save and a reopen, and a full save never replacing another file;
    • the cache's file, byte and cover caps, and the cache cleared before storage_full:;
    • every v2 method refusing keys it doesn't take, and the session ceilings after every command.
  • Determinism: before the warm-up wait, saving_and_a_render_after_a_save_and_a_reopen failed 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 --cargo ran 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.md on design/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.fill and design.background passing the gate as method-only Unread commands;
  • the table fill through insert.table {data}, and deck.set_text's commands;
  • slide.selectSlides and slide.go where the engine copies the current slide;
  • the extra caps, and the import guard's details (the ratio's 100 KiB exemption, name length, ZIP64, and which zips are unsupported: or damaged:);
  • the clarifications of answers and units, and deckcraft-fonts;
  • the warm-up wait, with its reason;
  • decision 7: a nested zip is allowed only as an Office package's embedded object under ppt/embeddings/, within every other bound, and is held: anywhere else.

🤖 Generated with Claude Code

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>
ymote and others added 4 commits October 11, 2026 13:18
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
ymote force-pushed the feat/deck-service-v2 branch from 099750f to a1a0b49 Compare October 11, 2026 20:28
@ymote
ymote marked this pull request as ready for review October 11, 2026 20:29
@ymote

ymote commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on a1a0b49 (tools/ci-local.sh --only all --linux-host --offload, 14m30s, 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 1s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 5m18s
desktop.yml / desktop Desktop tools and scripts PASS 16s
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 1m38s
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 0s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 41s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 3s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 1s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 4s
phone.yml / home Compile Home and its bundled modules PASS 3m44s
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 3m23s
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 26s
phone.yml / home (linux) Build opt-in AppCard tests PASS 2m46s
phone.yml / home (linux) Test opt-in AppCard PASS 1s
phone.yml / home (linux) Build craft-engine relay and system-chat tests PASS 2m54s
phone.yml / home (linux) Test craft-engine relay and system chat PASS 4s
phone.yml / home (linux) Build linked Terminal tests PASS 2m27s
phone.yml / home (linux) Test linked Terminal PASS 1s
phone.yml / home (linux) Build Wasm and runtime policy tests PASS 1m17s
phone.yml / home (linux) Test Wasm and runtime policy PASS 11s
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 19s
apps.yml / services (linux) OctoSense release updater PASS 7s
apps.yml / services (linux) App peers PASS 29s
apps.yml / services (linux) AI providers config PASS 6s
apps.yml / services (linux) Connected provider protocols and Linux host adapter PASS 46s
apps.yml / services (linux) L0 in-card chat (sys.chat, host side) PASS 9s
apps.yml / services (linux) System toolbox (workflow templates) PASS 1m30s
apps.yml / kernel-security (linux) python3 tools/setup.py PASS 0s
apps.yml / kernel-security (linux) The pinned octos revision PASS 0s
apps.yml / kernel-security (linux) Build octos (unless cached) PASS -
apps.yml / kernel-security (linux) Real-kernel tests PASS 41s
apps.yml / apps (linux) python3 tools/setup.py PASS 0s
apps.yml / apps (linux) Host services (Mail, AI providers, News) PASS 2m07s
apps.yml / apps (linux) Connected accounts and Rinx editor PASS 1m32s
apps.yml / apps (linux) Shell AI services (crates/ai-host) PASS 2m56s
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 7s
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 1m45s

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

Native GitHub acceptance passed on the same head: embedded-browser.yml.

@ymote
ymote merged commit 202d7cc into main Oct 11, 2026
4 of 9 checks passed
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
ymote deleted the feat/deck-service-v2 branch October 11, 2026 21:32
ymote added a commit that referenced this pull request Oct 12, 2026
Brings #500 up to main (72 commits, including the pdfcraft repin #502,
the deck service #503 and the WebAssembly deadline fix #505) so it can
be gated on its exact head. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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