Files
WP-LIT-MCP/docs/superpowers/notes/2026-09-27-etappe-3a-rulings.md
2026-09-27 04:57:43 +02:00

112 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Etappe 3a – Entscheidungen (Rulings), Befunde und offene Punkte
Aus dem SDD-Ledger übernommen, 2026-09-27. Security-Audit: 2026-09-27-etappe-3a-security-audit.md.
## Offen für spätere Etappen
- Etappe 3b: vollständiges Ausblenden von Bibliotheks-IDs bei deaktivierter Gruppe (P13); gleiche Zielregeln für rohe post_link_url_* wie für {{link:}} (P10); Theme Builder, find-similar, make-global.
- Etappe 4: Presets können Lottie-Quelle/Interactions tragen – Preset-Schreiben braucht dieselben Builder-Sperren.
- Etappe U: try/catch um Divi-Aufrufe (P11), einheitliche Fehlercodes der Tools-Tab-Sperren, doppelte Helfer, Test-Feinschliff, „blurb ist leer“-Warnung, Rollen-Option in divi-write-smoke.
- Restrisiko (Low, P25): sehr ungewöhnliche Schreibweisen globaler Verweise auf Seiten, die der Benutzer nicht bearbeiten darf, können der Verwendungssuche entgehen (setzt Bearbeitungsrechte an der verweisenden Seite voraus).
## Pre-flight rulings
Pre-flight (opus): 0 Critical, 8 Important, 19 Minor, 4 Info; fixtures match byte for byte; {{link:ID}} to draft/private shows only ?page_id=. Full tables: preflight.md.
- Ruling P1 (I-1, → Task 9): lit/divi-replace-text on library entries goes through DiviWriter::commit() (dry run/confirm for global, site-wide cache clear, no Divi flags on library posts) or refuses library targets — implementer picks the variant that reuses commit(); F2 binding — cost if wrong: none
- Ruling P2 (I-2, → Task 11): rebuild the 3 test rows with Divi's \u0022 escaping and assert the parsed block is not opaque — cost if wrong: none
- Ruling P3 (I-3, → Task 11): StoredNodeGuard refuses, for users without unfiltered_html, non-Divi blocks, raw text parts, opaque blocks and rich-text HTML that is not kses-stable when moving stored nodes between posts — cost if wrong: editors copy less (visible refusal)
- Ruling P4 (I-4, → Task 2): only the link row uses URL mode in the new placeholder test — cost if wrong: none
- Ruling P5 (I-5, → Task 13): lit-library-insert reads the entry via LibraryAccess (edit_post) — cost if wrong: none
- Ruling P6 (I-6, → Task 12): lit/library-save reuses NewDraft (partial_write + recordPartial on failed rollback) — cost if wrong: none
- Ruling P7 (I-7, → Task 12): scope "global" additionally requires edit_global_library (Divi's rule, F12) — cost if wrong: none
- Ruling P8 (I-8, → Task 9): a confirmed global edit is refused when the user cannot edit_post one of the published pages that render the entry (stricter than Divi; security first) — cost if wrong: editors with partial rights can't change shared modules
- Ruling P9 (minors): confirm stays honour-based (D10); a disabled library group hides library IDs from the Divi read tools; lit/divi-node-copy follows lit/divi-insert exposure (E9); refuse the backup path when a user without unfiltered_html edits with revisions off or trash disabled (kses backup loses bytes); output schema gains affected, affected_count, backup_id; unknown dynamic field types are omitted (F4); dispatch notes per preflight (OutlineTest anchor ×3, >120-char lines, dead variable) — cost if wrong: small
- Note: common.md fixed — PostChangeResult was renamed to PostSaveFollowUp in 2b W3
## Progress
- Task 1: dispatched (sonnet, BASE cfc81dd)
- Task 1: complete (404f0f3, trailer normalized; 2-line config change verified by controller; 1597 tests)
- Task 2: dispatched (opus, BASE 404f0f3; P4, link read_post check)
- Task 2: implementer done (788943e; 1609 tests); note T16 docs: attachments refused by {{link:}}
- Task 2: review (opus): Approved; live: read_post enforced (private/foreign draft/trash refused, uniform message), ID forms refused, URL-only on all paths, no leak
- Ruling P10 (T2 minors → final wave): full-form post_link_url_* uses LinkTargets (same target rules as {{link:}}; trashed/unattached attachment consistent) + one shared ID parser; attachments refused by {{link:}} documented in T16 — cost if wrong: none
- Task 2: complete (404f0f3..788943e)
- Tasks 3–5: dispatched as one batch (sonnet, BASE 788943e, one commit per task)
- Tasks 3–5: review (sonnet): all Approved (858aa6a, a3a8070, b389f98 after trailer normalization; 1617 tests); global id in outline/node-get = library post ID only (accepted, like GlobalUsages); minors → final wave: preset-cap note only when presets requested; Outline duplicates Block::shortName prefix strip
- Tasks 3, 4, 5: complete (788943e..b389f98)
- Tasks 6–7: dispatched (opus, BASE b389f98, one commit per task)
- Tasks 6–7: implementer done (ce1ad7c, 2700fd6; 1646 tests); T7 deviations: unifyLineBreaks skips opaque blocks; deglobalize keeps invalid blockName instance opaque
- Tasks 6–7: review (opus): both Approved; byte fidelity independently verified incl. deglobalize == Divi's own PortabilityPost output; deviations accepted
- Ruling P11 (T6/T7 minors): → T12/T13 dispatch: refuse opaque/global roots before GlobalInstance::of (no nested instances); → final wave: try/catch Throwable around Divi calls in DiviLibraryStore/DiviRoles (E15 never fatal), stdClass localAttrs, self-closing of() for created childless roots, replaceChild attached-check, isSpecialtyColumn by effectiveName
- Tasks 6, 7: complete (b389f98..2700fd6)
- Task 8: dispatched (opus, BASE 2700fd6; P9 library-group hiding hook)
- Task 8: implementer done (48ecc62; 1660 tests); P9 hook LibraryAccess $visible + ExposurePolicy::groupEnabled, wired as libraryVisible (GROUP 'divi-library', live with T10); T9 must merge: $libraryWritable param + LibraryAccess::GROUP already exist
- Task 8: review (opus): Approved; live with temp editor/author/contributor/subscriber + role-editor right withdrawn; P9 hook verified with real ExposurePolicy
- Ruling P12 (T8 I-1): users without the Divi library right at all (canList false) get PostGuard::notFound (no existence/type oracle, like P9); missing edit_post on an entry stays `forbidden` (project convention for pages) — fix round 1 now — cost if wrong: less helpful hint for users lacking the role right
- Ruling P13 (T8 M-1/M-2 → final wave / T10): with the library group hidden, page outline/node-get omit global_id and the render-summary hint; T10 keeps groupEnabled semantics (group mode only)
- Task 8: fix round 1 (P12; 681bb75; 1661 tests; live: logged-out 1058 vs 99999 byte-identical not_found) — verified by controller against report + live claim
- Task 8: complete (2700fd6..681bb75). Note for T10+: canList()/canRead() still return forbidden when called directly (fine for list tool — no ID oracle)
- Task 9: dispatched (opus, BASE 681bb75; F2, P1, P8, P9 backup refusal + schema)
- Task 9: implementer done (d66ad36; 1684 tests; divi-write smoke ok; live dry run on 1059 lists 1053, bytes unchanged); concerns: backup id not named on failure after backup; P8 published only
- Task 9: review (opus): live verified dry run/confirm/revision/reference semantics/P8 refusal/no flags; changes requested: I-1 P8 gap (private page: editor without rights changed what it shows), I-2 backup_id lost on failed save after backup
- Ruling P8a (amends P8): confirmed global edit requires edit_post on every affected post with status publish, private or future (incl. TB layouts/other library entries found by the query); drafts/pending listed in dry run but not blocking — private/future render or will render — cost if wrong: editors blocked more often
- Ruling P14: WriteFailed after a backup carries backup_id + hint (WriteFailed::withDetails); dry run warns with blocked_ids when P8 would refuse (M-1); plural fix (M-2); shared status-parameterised query (M-3); _prime_post_caches before P8 loop (M-4); M-5/M-6 → final wave/T16
- Task 9: fix round 1 dispatched
- Task 9: fix round 1 (P8a, P14, M-1..M-4; 520a639; 1691 tests); re-review running
- Task 9: re-review clean (live: private/future block, draft not, blocked_ids in dry run, backup_id on failed save)
- Task 9: complete (681bb75..520a639)
- Task 10: dispatched (opus, BASE 520a639; P12, P13 if cheap, per-entry filtering)
- Task 10: implementer done (b7c7149; 1702 tests); P12 via item() for id; P13 → final wave
- Task 10: review (sonnet): Approved; live: group disabled → list unregistered + read tools not_found; nur lesen → list ok, writes forbidden; P12 id byte-identical; minor: MAX_SCAN cap silent → final wave
- Task 10: complete (520a639..b7c7149)
- Task 11: dispatched (opus, BASE b7c7149; P2, P3, stored $variable refs via VariableRef)
- Task 11: implementer done (f8275ad; 1741 tests; 30 malicious rows); concerns: readable global instances checked (localAttrs) not refused (per brief/preflight I-3); builder-written $variable refs often refused for editors (stricter than Divi); plain text kses-checked only when containing '<'; ValueCleaner::kindAt added
- Ruling P15: accept checking readable global instances (a reference renders existing library content; Divi lets library-right users insert them too); stricter $variable handling accepted (visible refusal, documented in T16) — cost if wrong: editors can't copy some builder nodes
- Task 11: review (opus, SEC): Approved (guard holds for P3/P15; Divi renders instances from published entries only, localAttrs+snapshot both checked). NEW Critical (pre-existing since 2b): module.decoration.interactions writable by editors → effect addAttribute onclick="…" → Divi's interactions script setAttribute → script on click; path is known in catalog so lit/divi-update can write it
- Ruling P16: add decoration.interactions to ValueCleaner::BUILDER_ONLY (S7 gate: nur im Divi-Builder für Benutzer ohne unfiltered_html) — closes main pipeline and StoredNodeGuard; separate commit with tests (full form, short form if any, stored guard, removal via null); also scan the catalog for other paths whose values Divi turns into attributes/HTML/script (e.g. other effect/attribute/script-like option groups) and gate them the same way, listing what was found — cost if wrong: editors can't set interactions via MCP
- Ruling P17 (T11 minors): guard URL rule `url($v) === trim($v)`; guard enforces VALUE_KEY key names and digits-only globalModule; STATE_KEYS reuses AttrWriter::STATES; >120-char test lines — in the same fix round
- Task 11: fix round 1 (P16 9d92808 interactions builder-only + scan: nothing else to gate; P17 e808832; 1748 tests; smoke ok)
- Ruling P18 (P17 too strict for relative links): StoredNodeGuard URL accept rule = url($v) === trim($v), OR the value has no scheme (no ':' before the first '/', '?', '#') and contains no whitespace, quotes, '<', '>' or control chars — relative links (kontakt/, /pfad, #anker, ?q=1) stay copyable, every scheme still must survive url() unchanged — cost if wrong: none (scheme-less values can't carry javascript:)
- Task 11: fix round 2 (P18; 16c6d59; 1750 tests); backslash hosts also refused, //host must survive url()
- Task 11: re-review: P16/P17/P18 verified live (interactions refused for editors incl. null removal + guard; 63 URL paths × entity-colon payloads safe; relative rule holds); NEW Important: divi/lottie lottie.innerContent.src → remote JSON loaded by lottie-web with expressions (eval) → script; editors could set it
- Ruling P19: divi/lottie lottie.innerContent (src and any animation-source keys) builder-only for users without unfiltered_html (same S7 gate as P16), in main pipeline and StoredNodeGuard; tests set/null/copy; guides list it — cost if wrong: editors can't set Lottie via MCP
- Task 11: fix round 3 (P19; 8fa285a; 1753 tests; smoke ok) verified by controller (pattern + tests)
- Note Etappe 4: Divi global presets can carry a Lottie src / interactions — preset writes (design system) must apply the same builder-only gates
- Task 11: complete (b7c7149..8fa285a)
- Task 12: dispatched (opus, BASE 8fa285a; P6, P7, P11, guard)
- Task 12: implementer done (5035092; 1783 tests; live: bytes equal Divi entry 1055, page unchanged, no flags); nested globals kept as references in normal entries (plan-conform, accepted)
- Task 12: review (opus): Approved; live: rights (edit_post source, publish_posts, divi_library/save_library, edit_global_library), guard refuses all builder-only kinds as editor, P11, P6 rollback + partial_write, terms/meta identical to Divi, exposure; bytes equal after CRLF→LF; minors → final wave: REASON_CREATE placement, test line length, partial_write hint for new entries
- Task 12: complete (8fa285a..5035092)
- Task 13: dispatched (opus, BASE 5035092; P5, P11, guard in copy mode, direct budget 22)
- Task 13: implementer done (7fe91c2; 1799 tests; smoke ok; live: 1059/1058 instance and 1055 copy byte-identical to fixture, probes cleaned)
- Task 13: review (opus): Approved; live: P5 (author vs admin/private/fixture forbidden, missing not_found), P11 incl. instance into global entry refused by shapeViolation, copy-mode guard refusals as editor, exposure 22 direct; minors → final wave (test line length, multi-root unit test, "zu bearbeiten" wording)
- Task 13: complete (5035092..7fe91c2)
- NEW (pre-existing, Important): editor saves via any Divi write tool rewrite untouched <p> → <p> (some content_save_pre filter still active despite kses removal); no CHANGED_ON_SAVE; tree_rev may mismatch stored content → investigation dispatched (opus) in parallel with Task 14
- Task 14: dispatched (opus, BASE 7fe91c2); parallel: editor-rewrite investigation (read-only on code)
- Task 14: implementer done (6f6c691; 1814 tests; smoke ok); source_tree_rev required check; not live-probed
- Investigation editor rewrite: NOT reproducible (45 inputs, editor+admin, plain wp_update_post in kses window and end-to-end lit/divi-insert: byte-identical, tree_rev == Rev::of(stored)); likely tool-chain \u decoding in the reviewer's probe (known artifact); CHANGED_ON_SAVE detection works (wp_strip_custom_css_from_blocks + Divi AttributeSecurity both detected). Ruling P20: no kses-window change; final wave adds a file-based editor regression test and switches synthetic-divi-filters.html to \u003c escapes — cost if wrong: none
- Task 14: review (opus): Approved; live via registered ability: source access P5/P12, guard refusals as editor (7 kinds), P11, same-page byte identity, restore point, source/target rev locks, exposure matrix; minors → final wave: shared source-node helper (dup with LibrarySave), OUTLINE_HINT reuse, test line length, 'after'=>null in test
- Task 14: complete (7fe91c2..6f6c691)
- Task 15: dispatched (opus, BASE 6f6c691; F4, P9 unknown types, allow-map equivalence test)
- Task 15: implementer done (8808a7d; 1827 tests; allow map byte-identical JSON snapshot); concerns → final wave: {{link:}} hint for attachments vs P10, choice fields show value not label (document)
- Task 15: review (sonnet): Approved; allow-map snapshot test mutation-verified (non-vacuous); sample post only editable by user (implementer fixed brief leak); live page 26 sources
- Task 15: complete (6f6c691..8808a7d)
- Task 16: dispatched (opus, BASE 8808a7d; docs must describe actual T1–T15 behaviour, collected points in dispatch)
- Task 16: implementer done (2729e28; 1829 tests; v0.4.0); library line in instructions only when group writable; spec 'Claude' wording → final wave
- Task 16 review (sonnet) and Task 17 (opus, BASE 2729e28) dispatched in parallel (disjoint files: docs vs tests/Integration + abnahme note)
- Task 16: review (sonnet): Approved (docs verified against code: 22 direct, result keys, P8a/P9/P12/P16/P19/P3/P15–P18, link rules, version, topics, neutral wording); → final wave: wp-lit-mcp.php plugin header still says "mit OAuth für claude.ai"; internal spec "Claude" wording
- Task 16: complete (8808a7d..2729e28)
- Task 17: done (186f7af; 6 smokes ALL CHECKS PASSED; 1829 tests); Ruling P21: T17 (test script + acceptance note only) is reviewed within the final whole-branch review instead of a separate task review — cost if wrong: none
- Task 17: complete (2729e28..186f7af)
- Final: whole-branch review (opus) + security audit (opus) dispatched in parallel on cfc81dd..186f7af
- Final review (opus): ready with fixes; must: I-1 node-copy from library skips add_library (+ library-insert exposure), I-2 P13 guide vs code
- Security audit (opus): no Critical/High/Medium; Low: L1 DoS in P8a/usages (loads full posts), L2 referenced-but-not-global entry skips dry run/P8a (live), L3 library-list search covers content + decodable cursor → existence oracle for authors (live), L4 dry run lists id/status/type of unreadable posts, L5 = I-1, L6 TB layouts accepted on WP caps only; hardening: guard vs first globalModule id, confirm not bound to dry run, "0123" refs
- Ruling P22: final fix wave per final-fix-brief.md (must + audit L1–L6 + cheap review items); P13 full hiding and P10 → Etappe 3b; guide sentence fixed now — cost if wrong: none
- Final fix wave: 17/17 (1b42f9b..f1937ca; 1868 tests; 6 smokes ok); decisions: referenced normal entries get dry run/confirm (drafts listed, only live block, no edit_global_library needed); cursor counts shown entries only; divi/global-layout check via Divi module list; link hint dropped for internal types; divi-write-smoke roles-option restore same issue → U; scoped re-review dispatched
- Final fix re-review (opus): items 1–17 addressed (L1/L3/L4/L6/item 8 live-verified); F1 open: usage search misses Divi 4 shortcode global_module="ID" and JSON-escaped globalModule key/value (Divi renders them) → author edited an entry used only on such admin pages without dry run/P8a (live); minor: 'Eintrag, die' → 'den'
- Ruling P23: GlobalUsages finds references fail-closed: SQL prefilter candidates = content containing 'globalModule' OR 'global_module' OR a Divi block opener with '\\u' escapes; then PHP-parse candidates: Divi 4 shortcodes (global_module attr, intval) and Divi 5 openers (json_decode attrs → globalModule intval); an escaped opener whose globalModule can't be decoded counts as a possible reference (blocking if not editable); guide sentence adjusted; wp_block embedding documented as parity (Info) — cost if wrong: more pages listed/blocking on odd content
- Final fix round 1 (P23; fec5d9d, 8f87ec6; 1887 tests; smokes ok): candidate prefilter globalModule/global_module/escaped ASCII letter; UsageMatcher parses Divi 5 (json_decode) + Divi 4; undecodable escaped opener = possible reference; parse cap 5000
- F1 re-check: live bypass fixed, prefilter not evadable; NEW G1 Divi 4 shortcode inside a Divi 5 attribute value (\u0022-escaped quotes) misparsed; G2 leading space / scientific notation cast by Divi (int); G3 global_module values casting to 0 render the NEWEST published layout (et_pb_load_global_module WP_Query p=0); _bin collation note
- Ruling P24: UsageMatcher scans decoded string values of every Divi 5 opener for Divi 4 shortcodes too; shortcode attrs parsed like WordPress (shortcode_parse_atts) and cast with (int) exactly like Divi; a global_module that casts to 0 counts as a reference to EVERY library entry (fail closed; rare); prefilter case-insensitive regardless of collation (LOWER() or explicit _ci COLLATE); guide wording without absolute claim; after this round remaining exotic forms are documented Low (breaker) — cost if wrong: coarse over-listing on odd content
- Final fix round 2 (P24; b6d791d; 1901 tests; smokes ok; live: shortcode in Divi 5 text value with ", leading space, abc, GLOBAL_MODULE found; other-ID control not found)
- Ruling P25 (breaker): accept after round 2 without another re-review — fail-closed rule in place, remaining exotic spellings need edit rights on the referencing page, documented as residual Low in the guide and rulings; G3 side effect (an invalid global_module on a real page makes every entry edit need confirm) accepted as fail-closed — cost if wrong: a crafted unusual spelling on a page the author can't edit escapes P8a (Low)
- Etappe 3a complete: cfc81dd..b6d791d