ADR-013: DOCX Through System Pandoc With a Two-Pass Image Allowlist¶
Status¶
Accepted (2026-10-04)
Context¶
DOCX reports are assembled from Markdown fragments written by the LLM (src/epic_news/utils/docx_report/docx_builder.py) and converted by pandoc through pypandoc. Two problems had to be solved:
- Which pandoc.
pypandoc-binarybundled an x86_64 pandoc that does not run natively on Apple Silicon, and it pinned the pandoc version to the wheel. The project switched topypandocplus a system pandoc (Homebrew locally,aptin CI and the Docker image). - Untrusted Markdown. Fragments are LLM output, partly derived from scraped web content. An image such as
ormakes pandoc read a local file or call a URL and embed the result in a DOCX that is then emailed.
pandoc --sandbox looked like the answer but fails with distro builds: Debian bookworm (2.17) and Ubuntu (3.1) packages are not built with embedded data files, so the DOCX writer stops with Could not find data file data/docx/[Content_Types].xml (exit 97) under the sandbox. A Lua filter alone is not enough either: pandoc 2.x fetches DOCX media before running filters.
Decision¶
- Use system pandoc:
pandocis installed byaptin.github/workflows/ci.ymland in the Dockerfile runtime stage;pypandoc(notpypandoc-binary) is the Python dependency. - Convert in two passes:
- Markdown → pandoc JSON with
--lua-filter safe_images.lua. JSON output fetches no resources. The filter keeps only images whose path is underoutput/(relativeoutput/...or absolute under the resolved output root) and replaces every other image (URLs, anyscheme:URI,.., other absolute paths) with its alt text. The root arrives as theepic_image_rootmetadata field, which the filter deletes so it never reaches the DOCX properties. The same pass also runsstrip_rules.lua(#218), which drops everyHorizontalRule: LLM fragments separate paragraphs with---, and headings already structure the report. Markdown is read asmarkdown-yaml_metadata_blockso---stays a rule instead of breaking YAML parsing. - JSON → DOCX with
--toc --standalone --reference-doc. Pandoc only sees allowed images, so it can only fetch those. - Do not use
--sandbox.
Consequences¶
- Works with any pandoc version (verified with Debian 2.17 and Homebrew 3.12); no extra binary to download or pin per architecture.
- Reports can include images produced by our own code (charts written under
output/); anything the LLM points at elsewhere is dropped. - Developers must have pandoc on
PATH. An old virtualenv that still containspypandoc_binarylogs "Bad CPU type" and falls back to the system binary; fix withuv sync --all-extras --reinstall-package pypandoc. - Tests cover both passes, including that the DOCX pass never receives a forbidden image (
tests/utils/holiday_report/test_docx_builder.py).