Files
Guillaume Meyer (The Opinionated Man)andGitHub 09e64c4ded feat: optional MarkLLM text-watermark verification harness (#53)
* feat: optional MarkLLM text-watermark verification harness

Add an optional external backend wrapping THU-BPM/MarkLLM (Apache-2.0)
so a specific statistical text-watermark scheme (KGW / SynthID-Text)
can be verified before/after a Layer B rewrite.

- detect_text_watermark.py: detect/watermark subcommands, external
  checkout at a pinned commit, exit codes 0/1/2/3, --json
- rewrite_text.py --markllm-scheme: before/after detection around the
  rewrite, reports a `cleared` flag; never fails the rewrite when the
  backend is unavailable
- setup_markllm.sh + requirements-markllm.txt (pinned deps) +
  Dockerfile.markllm + Makefile bootstrap/smoke/docker targets
- tests/test_markllm_detect.py: 16 mock-based cases (no torch in CI)
- Docs: verification-harness caveat (same-config-only, not a
  vendor-detector oracle) in README, SKILL.md, removal-matrix, vendor-notes

* chore: tidy merged Unreleased changelog list

* security: harden the MarkLLM harness (offline, caps, supply-chain)

Addresses the PR security review:

- detect_text_watermark.py: --offline loads the scoring model from the HF
  cache only (local_files_only + HF_HUB_OFFLINE, no remote code), and the
  algorithm config is capped at 1 MiB so a crafted huge file is refused
  before either this script or upstream reads it into memory
- rewrite_text.py: WATERMARKS_MARKLLM_RLIMIT_AS (env, POSIX) optionally
  applies RLIMIT_AS to the MarkLLM subprocess; off by default because
  torch/CUDA needs large address spaces
- Dockerfile.markllm: drop the unpinned torch install (it is pinned in
  requirements-markllm.txt) and verify the cloned upstream commit SHA
- tests: offline flag, config-too-large, and preexec/rlimit cases
- docs: hardening knobs in README + SKILL.md; changelog updated
2026-08-14 09:56:16 -07:00

3.5 KiB

Removal matrix

Target Method Script / action Side effects Verifiable today?
Invisible Unicode / exotic spaces / bidi / tags Strip / normalize inspect_text.py, clean_text.py, clean_file.py Minimal Yes (codepoint report)
Statistical text watermark (SynthID-class / Kirchenbauer) Multi-pass paraphrase / humanize / back-translate / structural Agent Layer B + optional rewrite_text.py Meaning/style drift No without vendor key/detector; MarkLLM harness (detect_text_watermark.py) verifies a specific scheme config before/after
C2PA on PNG/JPEG/WebP Drop APP11 / PNG caBX / RIFF C2PA / exiftool clean_image.py Loses provenance metadata Yes
SVG metadata / XMP Drop <metadata>, xmpmeta clean_file.py Loses SVG metadata Yes (re-inspect)
PDF XMP / info exiftool -all= preferred clean_file.py Loses PDF metadata; degraded without exiftool Partial
DOCX props / customXml Rewrite OOXML zip clean_file.py Loses doc properties Yes
ODT meta:generator Scrub meta.xml clean_file.py Loses generator tag Yes
HTML generator / JSON-LD provenance Strip tags clean_file.py Loses meta Yes
Markdown AI frontmatter keys Drop keys clean_file.py Loses YAML keys Yes
Pixel image watermark (SynthID-media / StegaStamp / Tree-Ring / StableSignature) CtrlRegen regeneration (external backend) clean_ctrlregen.py / clean_image.py --remove-pixel ctrlregen Regenerates pixels; heavy compute; detail drift at higher strength No without official detector; reverse-SynthID score is a local surrogate
Audio / video watermarks (SynthID-media) Out of scope
C2PA soft binding (in-content link to manifest) Out of scope (survives our metadata strip) Vendor detector only
Data-driven model backdoors Out of scope

Default pipeline

  1. Inspect (inspect_file.py or specific inspect_*).
  2. Deterministic clean — Layer A text and/or container/image metadata; for images, optionally add pixel removal (--remove-pixel ctrlregen) after the metadata strip.
  3. Always offer Layer B rewrite for prose (paraphrase → optional strong pass: humanize / back-translate / structural).
  4. Prefer a non-origin, open-weight rewrite model when available (avoid re-stamping).
  5. Layer A again after rewrite.
  6. Report: Layer B is best-effort; residual risk remains.
  7. Optional verification: rewrite_text.py --markllm-scheme kgw|synthid runs a MarkLLM before/after detection (external detect_text_watermark.py harness) to show a specific scheme config clears. Same-config-only; not a vendor-detector oracle.

Code vs prose

  • Prose / Markdown / HTML body: full A + B.
  • Code: Layer A + formatter; statistical marks are weak; offer code rewrite (comments/docstrings/string-literal wording + local identifier renames) with user OK.

Layer B strengths

Strength When
paraphrase Default; explicit word-choice + syntax churn
humanize Zero-shot "write like a human" token reshuffle
backtranslate Stronger token reshuffle via pivot language
structural Strongest; most drift (outline → human prose)
code Comments/docstrings/string-literal wording + local identifier renames

Frontier production watermarks are currently token-by-token (streaming constraint); paragraph-level robust methods (SemStamp / PostMark) are not yet deployed, so paraphrase-class attacks remain effective today.