0092 — Desktop document-generation baseline + versioned file artifacts
Status: Accepted (phased — D1 ships with this ADR; D2/D3 follow in-track)
Context
ADR 0058 D2 gates plugin installs in the frozen desktop sidecar: a plugin whose requires_pip entries aren't already importable in the read-only PyInstaller bundle is refused at install time (graph/plugins/installer.py) with "install it on a server/Docker build instead."#1953 / PR #1954 added an optional tier (missing soft deps warn-and-continue), but that only degrades gracefully — the feature still doesn't run on desktop.
Two real hits keep landing on this gate:
- cowork (the knowledge-work skill pack) declares
python-docx,openpyxl,python-pptx,reportlabas hardrequires_pip. On the desktop runtime the whole plugin is refused — so its docx/xlsx/pptx/pdf skills can't produce a file. - protobanana is refused over
pillow>=10(a soft dep it imports lazily).
The demographic protoAgent is courting here — the "own your stack" crowd migrating off Claude Cowork / OpenAI Codex to a local-first agent they control — expects that agent to actually produce documents. "The desktop app can't make a .docx" is a credibility gap for that positioning, not a plugin edge case. cowork is a first-party flagship pack, not an arbitrary third-party plugin, so it warrants a platform answer rather than a per-plugin workaround.
There is a second, independent gap. The artifact plugin (ADR 0038) versions renderable text source — a version is a code string (HTML / Markdown / SVG / React), diffed as text (update_artifact is an old_string → new_string edit, capped at max_code_kb). It has no bytes / mime / blob path. So even once the doc stack is bundled and cowork writes a real .docx, that file is a loose binary in a scoped work folder with no version history — the artifact plugin never sees it. For knowledge work, the versioning has to hold for the files too.
Decision
D1 — The desktop bundle baseline includes the document-generation stack (ships with this ADR)
Bake python-docx, openpyxl, python-pptx, reportlab (with Pillow + lxml arriving transitively) into the frozen desktop app, on by default:
- A committed
apps/desktop/sidecar/requirements-docs.txt, installed in the desktop-build CI sidecar step. - A
DOC_COLLECT_ALLlist inbuild_sidecar.py,find_spec-guarded exactly like the existing GoogleOPTIONAL_COLLECT_ALLtier, so a lean local freeze without the extra still succeeds.--collect-all(not bare hidden-imports) is required: reportlab ships font/AFM data, python-docx/pptx ship their default.docx/.pptxtemplates as package data, and an import-scan misses all of it (the library raises at runtime opening its default template).
This does not contradict ADR 0058's rejection of shipping git+pip — it bakes libraries at build time, which is precisely 0058's sanctioned mechanism (the same way the plugins/ tree and Google libs are bundled). It expands the baseline, it does not add a runtime installer.
Effect: once these are importable in the bundle, _deps_satisfied short-circuits and cowork's hard requires_pip is simply satisfied — cowork installs and works on desktop with zero manifest change. protobanana's pillow>=10 refusal also disappears (Pillow is now bundled).
D2 — A "download artifact" tier in the artifact plugin (follow-up PR)
Extend the artifact plugin so a version can carry bytes + mime + a derived text preview in addition to (or instead of) the text code:
- Store the blob per version (for download) and extract a readable projection (
docx→text,xlsx→sheet table,pptx→outline) so the panel renders an inline preview and the version history stays diffable — not an opaque blob. - Render a download card for the binary rather than iframing it; existing text artifacts are untouched (purely additive
kind). - New config: max blob size + blob retention, alongside the existing
max_versions.
D3 — An explicit save_file_artifact(path, title) tool (follow-up PR)
The seam by which a generated file becomes a versioned artifact is an explicit tool the skill calls after writing — deterministic, testable, agent-controlled, and it works when there's no HTML source (cowork authors .docx directly via python-docx, it doesn't render from an artifact). cowork's doc skills call it in ~/dev/cowork-plugin so their outputs land as versioned download-artifacts. (Auto-watching a work folder was considered and deferred: it versions scratch files too and needs watcher/debounce/dedup infra for no gain over the explicit call.)
Consequences
- Bundle size grows ~15–25 MB (lxml, Pillow, reportlab binaries + fonts) on a desktop app that already ships a Python and a Node runtime — acceptable for the capability, and the driver is what the demographic needs to work out of the box.
- Maintenance: the doc stack becomes part of the maintained desktop baseline (security bumps, per-platform wheel availability — all four ship mac arm64/x86_64 + Windows wheels).
- Verification: the frozen sidecar smoke step (
scripts/live_smoke.py --bin) should assert both thatimport docx, openpyxl, pptx, reportlabworks and thatimportlib.metadata.version("python-docx")resolves in the actual frozen binary — the latter is what the D2 gate (installer._importable) checks first, and the dist≠import name gap (python-docx→docx) is why the build--copy-metadata's each distribution alongside--collect-all. CI can only simulate the frozen path viaPROTOAGENT_PLUGIN_FROZEN=1. - Scope boundary: this is the first-party flagship answer. The general third-party dep story — installing any plugin's unbundled deps at runtime on the frozen app — stays the forthcoming ADR 0093 (the pip-less wheel installer, #1631 Scope A). 0092 and 0093 are complementary: bake the flagship stack (0092), install arbitrary plugin deps at runtime (0093).
References
- ADR 0058 — the frozen D2 dep gate this expands
- ADR 0038 — the artifact/generative-UI surface D2 extends
- ADR 0093 (forthcoming) — the complementary general pip path (#1631 Scope A)
- #1631, #1953 / PR #1954 (optional tier)
apps/desktop/sidecar/build_sidecar.py,apps/desktop/sidecar/requirements-docs.txt,plugins/artifact/__init__.py- cowork-plugin, protobanana-plugin (motivating first-party / third-party cases)