Skip to content

feat(ingest): each embedded docx image names its part, digest and size (#732, slice 2) - #978

Merged
github-actions[bot] merged 7 commits into
mainfrom
fix/732-docx-image-digest-r2
Nov 10, 2026
Merged

github-actions[bot] merged 7 commits into
mainfrom
fix/732-docx-image-digest-r2

Conversation

@cdeust

@cdeust cdeust commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Refs #732 (tranche 2 ; l'issue reste ouverte).

Cause racine.
La tranche 1 (#893) inventoriait chaque image embarquée avec son id de relation docx (r:embed/r:id), sans résoudre cet id vers la part du paquet qui porte les octets. Rien ne permettait donc d'identifier les octets d'une image, prérequis de leur conservation (critère « bytes, digest, position and document version »).

  • core/docx_relationships.py (pur) : word/_rels/document.xml.rels → {id: part} pour chaque relation interne, cible résolue selon ECMA-376 Part 2 Annex A.3 (relative à word/, absolue depuis la racine, segments ./..). Les cibles TargetMode="External" et vides sont absentes. XML malformé → DocumentParseError.
  • infrastructure/docx_media_reader.py : lit la part de relations (plafond MAX_DOCUMENT_BYTES d'ADR-0516, UTF-8) et passe chaque part nommée en SHA-256 par blocs de 64 KiB (mémoire bornée).
  • handlers/ingest_document_media.py : composition ; chaque attachment résolu reçoit part, digest (sha256:<hex>), size. Image liée, part absente, image Confluence : champs vides ("", "", null). state reste unsupported : les octets sont identifiés, pas conservés.
  • Description de l'outil, docs/module-inventory.md et CHANGELOG mis à jour.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would change existing behavior)
  • Refactor (no functional change; rules/coding-standards.md compliance)
  • Documentation only
  • Audit-finding closure (cite the finding ID)

Test plan

  • pytest tests_py/core/test_docx_relationships.py tests_py/infrastructure/test_docx_media_reader.py tests_py/handlers/test_ingest_document*.py + tous les tests qui référencent ingest_document/DocumentAttachment/module-inventory : 178 passed, 3 skipped.
  • pytest tests_py/core/ + tests document : 3961 passed, 162 skipped, 1 échec préexistant sur main (test_wiki_coverage_dashboard.py::test_unreadable_page_is_skipped_not_fatal, environnement root).
  • ruff check / ruff format --check (0.17.3) sur les fichiers touchés : OK. python scripts/check_craftsmanship.py : OK.

Mutations visées par les nouveaux tests : résolution .. au-dessus de la racine, cible vide ou faite de seuls segments de point, TargetMode externe vs interne, relation sans Id, élément hors espace de noms OPC, plafond exact (max_bytes égal / dépassé), lecture par blocs au-delà de 64 KiB, part vide (taille 0), CRC invalide, part absente du zip.

Audit notes

Non vérifié.

  • Gate de mutation (ADR-1122) : scripts/mutation_check.sh n'a pas pu tourner en local (téléchargement de torch refusé par le proxy de l'environnement).
  • pyright non lancé localement ; job unit complet de la CI non rejoué.

Tranches restantes de #732 (hors de cette PR).

  1. Conserver les octets (état preserved-only) avec une référence de source accessible, côté stockage.
  2. États extracted et failed explicites.
  3. OCR/vision optionnels avec fournisseurs déclarés.
  4. Propriété des références partagées à la correction/suppression.
  5. Rejeu d'une fixture illustrée publique dans Claude et Codex.

https://claude.ai/code/session_018kyZYr4VjkzmxX2THRjfx7

Continuation

Continues #961, which was approved on its head but is in conflict with the main branch and has spent its three reviews. Same commits, plus one: the merge of the main branch, with the CHANGELOG conflict resolved by keeping both entries.

https://claude.ai/code/session_018kyZYsWnKSjpX4oNV19PCF


Generated by Claude Code

#732, slice 2)

The attachment inventory from slice 1 (#893) named a docx image only by its
relationship id. The id is now resolved through word/_rels/document.xml.rels
(ECMA-376 Part 2 §9.3, Annex A.3) to the package part holding the bytes,
and that part is streamed through SHA-256. Linked images, absent parts and
Confluence images keep part/digest/size empty; state stays unsupported
until a later slice stores the bytes. Malformed relationships fail before
any write, like the main part.

Refs #732

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYr4VjkzmxX2THRjfx7
…and size (#732, slice 2)

fixup! feat(ingest): each embedded docx image names its part, digest and size (#732, slice 2)

unit (run 42750861159): test_every_storage_primitive_is_an_audited_site read archive.open(name) as open-write, the first positional argument of an open on an unresolved receiver being taken as its mode. The member is opened for reading: say so with mode="r".
…and size (#732, slice 2)

unit (run 42755857850): the mutation gate (ADR-1122) left 12 survivors and found no test importing ingest_document_media.

- docx_relationships: the all-'..' guard was redundant (popping word/ already yields ''), so it is now 'if not named'. Id and Target are read without a default and a relationship missing either is skipped; a missing-Target test pins it.
- docx_media_reader: the tests assert that the docx path appears in read errors, and that every read asks for at most 64 KiB. mode="r" (the ZipFile.open default) and 'UTF-8' vs 'utf-8' are registered equivalents.
- ingest_document_media: the lookup has no default; tests_py/handlers/test_ingest_document_media.py covers it directly.
Conflict: CHANGELOG.md, both entries under Added are kept.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYsWnKSjpX4oNV19PCF
Signed-off-by: Claude <[email protected]>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T19:20:42.225786Z 6dda9ad PR opened
🔒 Security Review ✅ Completed 2026-11-10T19:27:53.625399Z 6dda9ad PR opened

Security findings

Advisory findings (1)

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 3m 2s —— View job


Review 1 of 3: PR #978 is done

  • Gather the diff and context (head 6dda9ad)
  • Run five reviews (code quality, security, performance, tests, documentation)
  • Check findings against the repo's rules
  • Post the review with gh pr comment (review comment)

I found no Blockers and listed three Notes in the review comment:

  • No decompressed-size cap on hashed image members.
  • A missing image part is reported the same as a linked image.
  • The tool description says size is "empty" when it is null.

I did not run pytest, ruff or the craftsmanship gate here because the tool permissions blocked them, so CI is the check.

Verdict: approve

@claude

claude Bot commented Nov 10, 2026

Copy link
Copy Markdown

Review 1 of 3: PR #978 (head 6dda9ad)

I ran five reviews (code quality, security, performance, test coverage, documentation) over the whole diff. I found no Blockers.

The PR respects the layer rules: core/docx_relationships.py imports only core, infrastructure/docx_media_reader.py imports only infrastructure, and the handler is the only place that combines them. Each new constant carries a # source:, and every file and method is under the size limits. The archive.open(..., mode="r") call counts as a read for the ADR-1110 storage-primitive scanner. Both mutation-equivalence entries are correct. A relationship target is only ever used as a zip member name, and .. never climbs above the root, so there is no path traversal. A broken container or broken relationships part fails before any write.

I did not run pytest, ruff or the craftsmanship gate here: the tool permissions blocked them, so CI is the check.

Notes, no action before merge

  • docx_media_reader.py:125-145: image members have no decompressed-size cap. A small zip-bomb docx makes the server hash gigabytes, CPU-bound and before any write. Memory stays bounded. Checking getinfo(part).file_size against a cap with a # source: would close this, as ADR-0516 already does for the main part.
  • ingest_document_media.py:47-51: a missing .rels part, or an r:embed pointing at a member that isn't in the zip, gives the same empty part as a linked image. Part names are also matched case-sensitively and without percent-decoding, while OPC §9.1.1.1 compares them case-insensitively. A later slice could add a distinct state for these.
  • ingest_document.py:72-73: the tool description says the fields are "else empty", but size is null there, not empty.

View job run
Verdict: approve

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 6dda9ad1fa

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

part = parts.get(attachment.reference)
if part is not None and part in digests:
digest, size = digests[part]
attachment = replace(attachment, part=part, digest=digest, size=size)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

P2 Badge Security: Cap DOCX attachment metadata before serializing it

When a local user or agent ingests an attacker-authored DOCX, many image elements can reuse one relationship with a long, valid ZIP member name. This line places the full part name on every attachment, and _ingestion_result serializes every entry without a response budget. A 17,712-byte file produced 33,566,893 bytes of attachment JSON (4,000 images × 8,203 characters); scaling within both 1 MiB XML caps can exhaust MCP memory during SDK text/structured conversion. Cap attachment count and target length, and enforce a cumulative metadata/output budget.

Useful? React with 👍 / 👎.

@github-actions
github-actions Bot merged commit eb93794 into main Nov 10, 2026
18 checks passed
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.

2 participants