docs(quick-261018-k9s): add the D-17 module in summercms.go
This commit is contained in:
@@ -5,10 +5,10 @@ current_phase: "19.1"
|
||||
current_phase_name: Chat satellites
|
||||
status: executing
|
||||
stopped_at: Completed 19.1-05-PLAN.md
|
||||
last_updated: "2026-10-18T12:02:40.895Z"
|
||||
last_updated: "2026-10-18T12:55:02.551Z"
|
||||
last_activity: 2026-10-18
|
||||
last_activity_desc: Phase 19.1 execution started
|
||||
state_head: 782a6720bf2bcdce1dcaa3c1c9a98c69f1d44998
|
||||
last_activity_desc: "Completed quick task 261018-k9s: add the D-17 module in summercms.go (modules/breakwater); 19.1-02 forum plan unblocked"
|
||||
state_head: 98bc6de8f57fda4bd3ccc1880ba22b9ce1d6582a
|
||||
progress:
|
||||
total_phases: 41
|
||||
completed_phases: 33
|
||||
@@ -32,7 +32,7 @@ See: .planning/PROJECT.md (updated 2026-10-12)
|
||||
Phase: 19.1 (Chat satellites) — EXECUTING
|
||||
Plan: 4 of 6
|
||||
Status: Ready to execute
|
||||
Last activity: 2026-10-18 — Phase 19.1 execution started
|
||||
Last activity: 2026-10-18 — Completed quick task 261018-k9s: add the D-17 module in summercms.go (modules/breakwater); 19.1-02 forum plan unblocked
|
||||
|
||||
Progress: [████████░░] 80%
|
||||
|
||||
@@ -891,7 +891,6 @@ Recent decisions affecting current work:
|
||||
- Phase 8 UI gate: scripts/check-phase8-ui.mjs --final-gate's real Playwright browser matrix (32 UI-SPEC scenarios) is unimplemented (deliberate fatal() at check-phase8-ui.mjs:458, never authored by 08-05); stage_ui_harness must keep failing closed until a Playwright spec is authored. User approved Phase 8 closure on 2026-09-24 with this gap carried forward -- see 08-10-SUMMARY.md and .planning/phases/08-oauth2-1-authorization-server/deferred-items.md.
|
||||
- pending: push sm-user-plugin — 12.1-05 Task 3 condition (c) failed (`git ls-remote --tags origin v0.1.3` empty). Push summercms.go master and annotated tag v0.1.3 (local on df5cace) first, then the plugin, then fonoteka.go. Recorded in 12.1-05-SUMMARY.md.
|
||||
- 16.2-05 open UAT items: re-save HTML post 1 untouched (D-03 evidence vacuous), D-19 category edit check, root vhost install (frame-src), unpublish/delete test post 105
|
||||
- D-17 Render/Purify missing: 19.1-02 halted after the D-16 forum-policy note (2740ab2). modules/ has cabana.RenderMarkdown only. Run /msd-quick in summercms.go for a markdown module exporting Render(markdown) and Purify(html) with the named forum policy, then re-run /msd-execute-phase 19.1. Do not create sm-forum-plugin until that lands.
|
||||
|
||||
### Quick Tasks Completed
|
||||
|
||||
@@ -935,6 +934,7 @@ Recent decisions affecting current work:
|
||||
| 36 | summercms.io copy Alpha 0.1 → 0.2 (vue b5d6dbb, app 6a13a87), deployed | 2026-10-15 | 853ea1f | — |
|
||||
| 261016-0fm | Admin minors for v0.2.1: theme switch, language switch, settings nav on form pages | 2026-10-16 | 66fd4ae | [261016-0fm-admin-minors-for-v0-2-1-theme-switch-lan](./quick/261016-0fm-admin-minors-for-v0-2-1-theme-switch-lan/) |
|
||||
| 261016-s8x | PaymentGateway: banner bottom margin and hide disabled sections from admin navigation | 2026-10-16 | ab9f385 | [261016-s8x-paymentgateway-banner-bottom-margin-and-](./quick/261016-s8x-paymentgateway-banner-bottom-margin-and-/) |
|
||||
| 261018-k9s | add the D-17 module in summercms.go: modules/breakwater Render(markdown)/Purify(html) with the forum bluemonday policy | 2026-10-18 | 98bc6de | [261018-k9s-add-the-d-17-module-in-summercms-go](./quick/261018-k9s-add-the-d-17-module-in-summercms-go/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
phase: quick-261018-k9s
|
||||
plan: 01
|
||||
quick_id: 261018-k9s
|
||||
slug: add-the-d-17-module-in-summercms-go
|
||||
type: execute
|
||||
mode: quick
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- modules/breakwater/breakwater.go
|
||||
- modules/breakwater/breakwater_test.go
|
||||
- modules/breakwater/README.md
|
||||
- README.md
|
||||
- docs/architecture/introduction.md
|
||||
- docs/services/markdown.md
|
||||
autonomous: true
|
||||
requirements: [D-16, D-17]
|
||||
source:
|
||||
- .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md (D-16, D-17, D-18)
|
||||
- .planning/notes/2026-10-18-bluemonday-forum-policy.md
|
||||
- .planning/STATE.md blocker "D-17 Render/Purify missing"
|
||||
|
||||
estimate:
|
||||
tokens: 70000
|
||||
raw_tokens: 70000
|
||||
tasks: 2
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A plugin can import one framework package and turn user Markdown into HTML that contains no script, iframe, img, event handler or javascript:/data: URL, while GFM tables, fenced code, strikethrough, links and autolinks render (D-16, D-17)."
|
||||
- "The same package sanitizes already-HTML with the identical allow-list, so trainings-style stored HTML goes through the policy without a Markdown pass (D-17 purify callers)."
|
||||
- "Blank or whitespace-only input renders and purifies to the empty string, as PHP MarkdownRenderer does."
|
||||
- "Every link the policy keeps carries rel nofollow; every fully-qualified http/https link also carries target _blank (PHP HTML.Nofollow + HTML.TargetBlank)."
|
||||
- "The 19.1-02 forum gate finds `func Render(` and `func Purify(` under modules/ outside cabana.RenderMarkdown, so forum execution unblocks."
|
||||
- "go vet ./..., go test -short ./... and summer docs:build --check are green; go.mod and go.sum are unchanged."
|
||||
artifacts:
|
||||
- "modules/breakwater/breakwater.go — package breakwater: unexported goldmark GFM engine, unexported bluemonday forum policy, exported Render and Purify"
|
||||
- "modules/breakwater/breakwater_test.go — PHP ForumMarkdownRenderTest corpus, links/autolinks, blank input, Purify allow-list"
|
||||
- "modules/breakwater/README.md — standard module README structure"
|
||||
- "README.md — module table row for breakwater"
|
||||
- "docs/architecture/introduction.md — breakwater in the framework modules table"
|
||||
- "docs/services/markdown.md — docs page: Markdown and HTML sanitizing for user content"
|
||||
key_links:
|
||||
- "Render = goldmark (extension.GFM, default safe renderer) -> Purify; Purify = forum policy Sanitize. Both return \"\" on blank input."
|
||||
- "The policy is built once at package init and shared; bluemonday policies are read-only once built (seaglass precedent)."
|
||||
- "Docs: every `breakwater.X` code span in README/docs must exist in the package (docs checker)."
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add the D-17 framework module: `modules/breakwater`, a Markdown renderer and HTML sanitizer for user-submitted content, exporting `Render(markdown string) string` and `Purify(html string) string` with the named forum policy from the 2026-10-18 decision note (1:1 with the PHP `golem15/forum` HTMLPurifier config). It is the gate the forum plans (19.1-02) and later the Wavepath `purify()` callers consume; `cabana.RenderMarkdown` stays untouched as the admin preview gate and `seaglass` stays the admin rich-text policy.
|
||||
|
||||
Purpose: 19.1-02 is halted on the STATE.md blocker "D-17 Render/Purify missing". Per D-17 the renderer is a framework module delivered by a quick task, not forum code.
|
||||
Output: the `breakwater` package with tests, its README, a root README row, the introduction.md table row and a `docs/services/markdown.md` page; one commit.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/CLAUDE.md
|
||||
@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/.planning/notes/2026-10-18-bluemonday-forum-policy.md
|
||||
@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md
|
||||
@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/modules/seaglass/seaglass.go
|
||||
@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/modules/seaglass/README.md
|
||||
@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/modules/cabana/field_markdown.go
|
||||
@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/internal/docsite/render.go
|
||||
@/media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/classes/markdown/MarkdownRenderer.php
|
||||
@/media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/tests/security/ForumMarkdownRenderTest.php
|
||||
|
||||
Facts verified at planning time (2026-10-18, live reads):
|
||||
- `go.mod` already pins `github.com/microcosm-cc/bluemonday v1.0.27` (line 24) and `github.com/yuin/goldmark v1.8.6` (line 34). No dependency is added or bumped.
|
||||
- `modules/` has no `breakwater`; the word appears nowhere in `modules/`, `docs/`, `README.md` or `.planning/notes/`.
|
||||
- goldmark v1.8.6 default renderer (no unsafe option): raw HTML blocks and inline raw HTML are written as the comment `<!-- raw HTML omitted -->` (renderer/html/html.go:390,398,656); a link or autolink whose destination fails `IsDangerousURL` is written with an empty href (html.go:518,591,620).
|
||||
- bluemonday v1.0.27: `AllowURLSchemes` turns on parseable-URL checking and leaves relative URLs disallowed; an `a` whose only attribute was dropped is not in the default no-attribute element set, so the tag is removed and its text kept (seaglass smoke test documents the same); `AddTargetBlankToFullyQualifiedLinks(true)` also adds `noopener` to `rel` (sanitize.go:717-752); comments are dropped unless allowed.
|
||||
- 19.1-02-PLAN.md line 174-175 gate: `grep -n 'func Render(' modules --glob '*.go' | grep -v RenderMarkdown` and `grep -n 'func Purify(' modules --glob '*.go'` must each print a line.
|
||||
- Docs checker (`internal/docsite/check_identifiers.go`): code spans of the form `pkg.Ident` / `pkg.Ident(...)` must name an identifier that exists in `modules/<pkg>`; `src=` snippets and links/anchors are checked too. Module READMEs are ingested as API reference pages automatically.
|
||||
- Root README module table is alphabetical (`bouncer` row precedes `cabana`); introduction.md "The framework modules" table has rows Plugins and the container / HTTP / Data / Admin / Services / Console and tooling / Utilities.
|
||||
- `docs/services/*.md` front-matter: `title`, `description`, `section: services`, `order` (highest existing order is 160, frontend-and-ajax.md).
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer" tdd="true">
|
||||
<name>Task 1: breakwater package — Render and Purify with the named forum policy, end-to-end under test</name>
|
||||
<files>modules/breakwater/breakwater.go, modules/breakwater/breakwater_test.go</files>
|
||||
<read_first>
|
||||
- .planning/notes/2026-10-18-bluemonday-forum-policy.md (the locked policy: D-16/D-17)
|
||||
- modules/seaglass/seaglass.go (package doc style, package-level policy var, bluemonday builder calls)
|
||||
- modules/cabana/field_markdown.go (package-level goldmark engine precedent; do not modify this file)
|
||||
- internal/docsite/render.go lines 26-31 (in-tree `goldmark.New(goldmark.WithExtensions(extension.GFM), ...)` form)
|
||||
- /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/classes/markdown/MarkdownRenderer.php and tests/security/ForumMarkdownRenderTest.php (reference behavior and corpus)
|
||||
</read_first>
|
||||
<precondition>`grep -n 'bluemonday v1.0.27\|goldmark v1.8.6' go.mod` prints two lines and `ls modules/breakwater` fails (the package does not exist yet).</precondition>
|
||||
<behavior>
|
||||
Test file `modules/breakwater/breakwater_test.go`, package `breakwater_test`, importing `git.golem15.com/golem15/summercms/modules/breakwater`; table-driven where natural; `strings.ToLower` the output before negative checks as the PHP test does:
|
||||
- TestRenderXSS (the PHP `test_xss_sanitization` corpus verbatim: "<script>alert(1)</script>\n\n[x](javascript:alert(1))\n\n<iframe src=x></iframe>\n\n<img src=x onerror=alert(1)>"): the lowercased output does not contain `<script`, `javascript:`, `<iframe`, `onerror`, `<img`, `<script` or `<!--`; it still contains the text `x` (the link text survives when the link is dropped).
|
||||
- TestRenderStripsRawHTML: `Render("<script>alert(1)</script>")` and `Render("text <b onclick=\"x()\">bold</b> more")` contain no `alert(1)`, no `onclick`, no `<!--` and no `<`; the second still contains `text` and `more`. Raw HTML is stripped as PHP `html_input=strip`, not escaped into visible text.
|
||||
- TestRenderGFM (the PHP `test_gfm_table_and_code_render` corpus verbatim: "| A | B |\n|---|---|\n| 1 | 2 |\n\n```php\necho 'wave';\n```"): output contains `<table>`, `<th>A</th>`, `<td>1</td>`, `<pre>` and `<code` and does not contain `class=` (the language class is not in the allow-list, as in PHP). Plus `Render("~~gone~~ **kept**")` contains `<del>gone</del>` and `<strong>kept</strong>`; `Render("# Title\n\n> quote\n\n- a\n- b\n\n---")` contains `<h1>`, `<blockquote>`, `<ul>`, `<li>` and `<hr`.
|
||||
- TestRenderLinks: `Render("[site](https://example.com/a?b=1&c=2)")` contains `href="https://example.com/a?b=1&c=2"`, `target="_blank"`, and a `rel` attribute whose value contains both `nofollow` and `noopener`; `Render("visit https://example.com/x now")` (GFM autolink) contains `<a href="https://example.com/x"` with `nofollow` and `target="_blank"`; `Render("[mail](mailto:a@example.com)")` contains `href="mailto:a@example.com"` and `nofollow`; `Render("[d](data:text/html;base64,AAAA)")`, `Render("[v](vbscript:x)")` and `Render("[t](tel:+48123456)")` contain none of `data:`, `vbscript:`, `tel:` or `<a`, but still contain the link text; `Render("[rel](/threads/1)")` contains no `href` (relative URLs are not allowed by the locked policy) and still contains `rel` as text.
|
||||
- TestBlankInput: `Render("")`, `Render(" \n\t ")`, `Purify("")` and `Purify(" \n ")` all return exactly "".
|
||||
- TestPurifyAllowList: `Purify` on `<p onclick="x()">Hi <b>b</b> <i>i</i> <img src=x onerror=alert(1)><script>alert(1)</script></p><figure><img src="a.jpg"></figure><h2 class="t" id="h">H</h2><a href="https://e.com/p" target="_top" rel="opener">l</a><a href="tel:+48">t</a><sub>s</sub><sup>p</sup><ins>i</ins><del>d</del><hr><table><thead><tr><th>h</th></tr></thead><tbody><tr><td>c</td></tr></tbody></table><blockquote><pre><code>c < d</code></pre></blockquote><ul><li>u</li></ul><ol><li>o</li></ol><h6>6</h6><br>` keeps `<p>`, `<b>b</b>`, `<i>i</i>`, `<h2>H</h2>`, `<sub>s</sub>`, `<sup>p</sup>`, `<ins>i</ins>`, `<del>d</del>`, `<hr`, `<table>`, `<thead>`, `<th>h</th>`, `<tbody>`, `<td>c</td>`, `<blockquote>`, `<pre>`, `<code>c < d</code>`, `<ul>`, `<ol>`, `<li>`, `<h6>6</h6>`, `<br`, the `a` with `href="https://e.com/p"`, `target="_blank"` and a `rel` containing `nofollow` and `noopener`; the lowercased output contains none of `onclick`, `onerror`, `<img`, `<script`, `<figure`, `class=`, `id=`, `target="_top"`, `rel="opener"`, `tel:`, `alert(`; the text `t` of the dropped tel link survives. A second case: `Purify("<p>plain</p><!-- c --><style>p{}</style><form><input></form><svg onload=x></svg><iframe src=x></iframe>")` returns a string that contains `<p>plain</p>` and none of `<!--`, `<style`, `<form`, `<input`, `<svg`, `onload`, `<iframe`.
|
||||
- TestConcurrentUse: run `Render` and `Purify` from 8 goroutines (`t.Parallel` sub-tests or a WaitGroup) over the GFM corpus and assert each result equals the single-goroutine result (package-level engine and policy are shared).
|
||||
</behavior>
|
||||
<action>
|
||||
Create package `breakwater` at `modules/breakwater/breakwater.go` (module path `git.golem15.com/golem15/summercms/modules/breakwater`), following the seaglass file layout: package doc comment, imports, a package-level policy, exported funcs. Name choice (Claude's discretion, D-17): a breakwater takes the force of the open sea; user Markdown and HTML break on the policy and only calm, allow-listed HTML reaches the shore. No other beach name is used anywhere.
|
||||
|
||||
Imports: `bytes`, `strings`, `regexp`, `github.com/microcosm-cc/bluemonday`, `github.com/yuin/goldmark`, `github.com/yuin/goldmark/extension`. The package imports neither seaglass nor cabana nor `golang.org/x/net/html` (no byte-stable comparison is wanted here; the forum stores the policy output on write, D-18) — it is a leaf module. No new entries in `go.mod`/`go.sum` (both libraries are pinned; D-16 note).
|
||||
|
||||
Package-level values, built once:
|
||||
- `var engine = goldmark.New(goldmark.WithExtensions(extension.GFM))` — the in-tree form from internal/docsite/render.go, without docsite's parser options. The renderer keeps goldmark's default safe mode: the unsafe-HTML renderer option is never set, so raw HTML in Markdown input is written as an omitted-HTML comment and dangerous link destinations are blanked (PHP `html_input=strip` + `allow_unsafe_links=false`). Document in a comment that `goldmark.Markdown` is shared across goroutines the way cabana's `markdownEngine` is (parse state is per call).
|
||||
- `var policy = newPolicy()` with `func newPolicy() *bluemonday.Policy` that starts from the empty `bluemonday.NewPolicy()` (not from bluemonday's user-generated-content preset that seaglass builds on, D-16 note) and sets, 1:1 with the PHP HTMLPurifier config (cite "per D-16"):
|
||||
- `AllowElements("p", "br", "strong", "em", "b", "i", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "blockquote", "pre", "code", "a", "table", "thead", "tbody", "tr", "th", "td", "hr", "del", "ins", "sub", "sup")` — no `img`, no `figure`, nothing else.
|
||||
- `AllowAttrs("href").OnElements("a")`.
|
||||
- `AllowAttrs("rel").Matching(<regexp accepting one or more space-separated tokens from nofollow|noopener|noreferrer>).OnElements("a")` and `AllowAttrs("target").Matching(<regexp accepting exactly _blank>).OnElements("a")` — this is the `a[href|rel|target]` of the PHP list; a caller-supplied value the policy would not write itself is dropped (HTMLPurifier's empty `Attr.AllowedRel` / `Attr.AllowedFrameTargets` drop them too), and the policy re-adds its own values below.
|
||||
- `AllowURLSchemes("http", "https", "mailto")` — no `tel`, no relative URLs (D-16 note: "not the extra relative/tel allowances seaglass adds"). A link whose href is refused loses its tag and keeps its text.
|
||||
- `RequireNoFollowOnLinks(true)` (PHP `HTML.Nofollow`).
|
||||
- `AddTargetBlankToFullyQualifiedLinks(true)` (PHP `HTML.TargetBlank`); bluemonday adds `noopener` with it, which is the tabnabbing mitigation (T-k9s-04). `RequireNoReferrerOnFullyQualifiedLinks` is NOT added: the locked policy list is the four bullets of the decision note; if the 19.1 golden test wants `noreferrer` parity it is a follow-up to that note, not a silent addition here.
|
||||
- Comments stay disallowed (bluemonday default), which is what removes goldmark's omitted-HTML comment.
|
||||
|
||||
Exported API (Claude's discretion on signatures, D-17; PHP returns a string and never errors):
|
||||
- `func Render(markdown string) string` — returns "" when `strings.TrimSpace(markdown) == ""`; otherwise converts with `engine.Convert([]byte(markdown), &buf)` into a `bytes.Buffer` and returns `Purify(buf.String())`. Single return value: goldmark's `Convert` only reports its writer's error and `bytes.Buffer.Write` never fails, so the error is unreachable; state that in a comment next to the discarded error rather than inventing an error path consumers must handle. Doc comment names the behavior: GFM (tables, strikethrough, autolinks), raw HTML stripped, unsafe links dropped, then the policy.
|
||||
- `func Purify(html string) string` — returns "" when the input is blank; otherwise `policy.Sanitize(input)`. Doc comment: the same allow-list on already-HTML, for content stored as HTML (PHP `purify()` for trainings), never a Markdown pass.
|
||||
Do not export the policy (a `*bluemonday.Policy` is mutable; seaglass keeps its policy unexported for the same reason).
|
||||
|
||||
Package doc comment (first lines of the file): what the module is, that it is the gate for user-submitted content, and the one-line contrast with seaglass (admin rich text, UGC policy, no nofollow, images) and `cabana.RenderMarkdown` (admin field preview, regex gate, no GFM) — the executor writes those names in prose; the package must not import either.
|
||||
|
||||
Write `modules/breakwater/breakwater_test.go` first (RED), implementing every case in `<behavior>`; then the package (GREEN). Keep the test free of Docker and network: `go test -short ./modules/breakwater` must pass in well under 10 seconds.
|
||||
|
||||
Do NOT commit at the end of this task: CLAUDE.md requires a module's exported API, its README and the affected docs pages to land in the same commit, so Task 2 makes the single commit for both tasks.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && go vet ./modules/breakwater && go test -short -count=1 ./modules/breakwater && git diff --exit-code go.mod go.sum && test "$(grep -v '^\s*//' modules/breakwater/breakwater.go | grep -c 'WithUnsafe')" -eq 0 && test "$(grep -v '^\s*//' modules/breakwater/breakwater.go | grep -c 'UGCPolicy')" -eq 0 && test "$(grep -v '^\s*//' modules/breakwater/breakwater.go | grep -c 'modules/seaglass"\|modules/cabana"')" -eq 0 && grep -rn 'func Render(' modules --include='*.go' | grep -v RenderMarkdown && grep -rn 'func Purify(' modules --include='*.go'</automated>
|
||||
</verify>
|
||||
<done>`modules/breakwater` compiles, vets and passes its tests under `-short`; `Render` and `Purify` exist with the signatures above; go.mod/go.sum are byte-identical to HEAD; the file sets no unsafe renderer option, does not start from the UGC policy and imports neither seaglass nor cabana; the 19.1-02 grep gate (`func Render(` outside RenderMarkdown, `func Purify(`) prints a line each. Nothing committed yet.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: README, root module table, introduction.md row, docs/services/markdown.md; docs checker, full gates, single commit</name>
|
||||
<files>modules/breakwater/README.md, README.md, docs/architecture/introduction.md, docs/services/markdown.md</files>
|
||||
<read_first>
|
||||
- modules/seaglass/README.md (the standard README structure and tone to mirror)
|
||||
- README.md lines 92-120 (module table: columns Module | Purpose | Name mnemonic, alphabetical)
|
||||
- docs/architecture/introduction.md lines 22-33 (framework modules table)
|
||||
- docs/services/rate-limiting.md lines 1-12 (services page front-matter and opening)
|
||||
- docs/backend/forms.md line 450 (how seaglass is referenced from docs — link form `[seaglass](../../modules/seaglass/README.md)`)
|
||||
</read_first>
|
||||
<action>
|
||||
1. `modules/breakwater/README.md`, standard structure per CLAUDE.md Documentation rules (per D-17 "The module's README and docs/ page follow the Documentation rules"):
|
||||
- H1 `# breakwater`.
|
||||
- One-sentence summary (reused verbatim in the root table): "Markdown renderer and HTML sanitizer for user-submitted content: GitHub Flavored Markdown through goldmark, then a fixed bluemonday allow-list that adds nofollow and target=_blank to links."
|
||||
- Import line: `import "git.golem15.com/golem15/summercms/modules/breakwater"`.
|
||||
- `**Name:**` mnemonic: "A breakwater takes the force of the open sea; user Markdown and HTML break on the policy and only calm, allow-listed HTML reaches the shore."
|
||||
- `## Overview`: what it is for (forum posts, comments, any Markdown or HTML a site user submits that a plugin stores and serves); that the policy is the PHP `golem15/forum` HTMLPurifier config 1:1 and the Markdown pass is goldmark GFM with raw HTML stripped and unsafe links dropped; a short "Not this module" paragraph pointing to [seaglass](../seaglass/README.md) (administrator rich text, bluemonday UGC, keeps images and links as written) and to `cabana.RenderMarkdown` (the admin Markdown field preview with its regex gate) and saying plugins must not use either for user content.
|
||||
- `## Features`: the allow-list (elements, `a` with href/rel/target), the three URL schemes, no relative URLs (a dropped link keeps its text), `rel="nofollow"` on every link and `target="_blank"` plus `noopener` on fully-qualified links, raw HTML in Markdown stripped (not escaped into text), comments removed, blank input gives "", safe for concurrent use.
|
||||
- `## Usage`: a plain fenced Go block (no `src=`; the docs checker only verifies `src=` snippets) showing `html := breakwater.Render(post.Content)` stored on write and `clean := breakwater.Purify(training.ContentHTML)` for stored HTML, with one comment line each.
|
||||
- `## API reference` table with exactly two rows: `breakwater.Render` and `breakwater.Purify` (both identifiers must exist — the docs checker verifies every `pkg.Ident` code span against the package).
|
||||
- `## Dependencies`: SummerCMS modules: none. Third-party: `github.com/microcosm-cc/bluemonday` v1.0.27 (decision note `.planning/notes/2026-10-18-bluemonday-forum-policy.md`, plain text path, not a link), `github.com/yuin/goldmark` v1.8.6 with `extension.GFM`. Standard library: the packages actually imported.
|
||||
- `## Testing`: `go test ./modules/breakwater/...` and that the tests run in memory with no external services.
|
||||
2. Root `README.md`: insert one table row in the module table, alphabetically after the `bouncer` row and before `cabana`: `| [breakwater](modules/breakwater/README.md) | <summary sentence verbatim> | <mnemonic verbatim> |`. Use Edit on that one spot; do not rewrite the file.
|
||||
3. `docs/architecture/introduction.md`: in "The framework modules" table, append `[breakwater](../../modules/breakwater/README.md)` to the `Utilities` row after sieve (a leaf utility with no SummerCMS dependency, like sieve). Edit only that row.
|
||||
4. New page `docs/services/markdown.md` with front-matter `title: Markdown and HTML sanitizing`, `description: Render user Markdown to safe HTML and sanitize stored HTML with the breakwater allow-list; which gate serves admin fields instead.`, `section: services`, `order: 165`. Body, H1 matching the title, then: when a plugin needs it (content a site user submits: forum posts, comments, imported HTML), the two calls with the same plain Go block as the README, the policy as a short table (allowed elements; `a` attributes; URL schemes; link rewriting), the strip-not-escape behavior for raw HTML, blank input, and a "Which gate" section with three bullets: `breakwater` for user content, [seaglass](../../modules/seaglass/README.md) for administrator rich text saved through cabana's `richeditor` field (link [Forms](../backend/forms.md)), `cabana.RenderMarkdown` for the admin Markdown field preview only. Link the module README as `[breakwater](../../modules/breakwater/README.md)`. Every `breakwater.X`, `seaglass.X` and `cabana.X` code span on the page must name an existing identifier.
|
||||
5. Gates, all from the repo root: `go install ./cmd/summer` then `summer docs:build --check` (identifiers, links, anchors, command names); `go vet ./...`; `go test -short ./...` (the full tree, not only the module). Fix any checker finding in the docs text, never by weakening the checker.
|
||||
6. Single commit for both tasks (CLAUDE.md: exported API, README and docs in the same commit; code and planning docs separate — this plan's quick dir is NOT staged, the orchestrator commits it). Stage explicitly by path: `git add modules/breakwater/breakwater.go modules/breakwater/breakwater_test.go modules/breakwater/README.md README.md docs/architecture/introduction.md docs/services/markdown.md`. Before staging run `git status --porcelain` and leave any other session's uncommitted files (STATE.md, state.json, other modules) alone. Commit message: `feat(breakwater): Markdown renderer and HTML sanitizer with the forum policy (D-17)` with a short body naming Render/Purify, the 1:1 PHP HTMLPurifier policy, and that go.mod is unchanged. No co-author or attribution trailer of any kind (CLAUDE.md "No co-author tags" overrides the generic attribution reminder). Do not push, do not tag.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && go install ./cmd/summer && summer docs:build --check && go vet ./... && go test -short ./... && grep -c '\[breakwater\](modules/breakwater/README.md)' README.md | grep -qx 1 && grep -c 'modules/breakwater/README.md' docs/architecture/introduction.md | grep -qx 1 && test -f docs/services/markdown.md && grep -q '^section: services' docs/services/markdown.md && grep -q '^\*\*Name:\*\*' modules/breakwater/README.md && grep -q 'breakwater.Render' modules/breakwater/README.md && grep -q 'breakwater.Purify' modules/breakwater/README.md && git diff --exit-code go.mod go.sum && MSG=$(git log -1 --format=%B) && case "$MSG" in 'feat(breakwater)'*) ;; *) false;; esac && test "$(printf '%s' "$MSG" | grep -ci 'co-authored-by')" -eq 0 && git diff --quiet HEAD -- modules/breakwater README.md docs/architecture/introduction.md docs/services/markdown.md</automated>
|
||||
</verify>
|
||||
<done>Module README in the standard structure; root README row and introduction.md Utilities row present once each; `docs/services/markdown.md` exists in the services section; `summer docs:build --check`, `go vet ./...` and `go test -short ./...` green; go.mod/go.sum unchanged; exactly one commit `feat(breakwater): ...` without attribution trailers contains all six files and the working tree has no uncommitted changes to them. Nothing pushed or tagged.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| site user -> stored content_html | Untrusted Markdown (forum posts, comments) crosses into HTML that the public site renders verbatim; `Render` is the only gate (D-17, D-18). |
|
||||
| imported/stored HTML -> served HTML | Trainings-style raw HTML stored by users or imported from PHP crosses through `Purify` before it is served. |
|
||||
| plugin author -> framework gate | A plugin picks which sanitizer to call; the wrong pick (cabana regex gate, seaglass UGC policy) silently weakens the boundary. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-k9s-01 | Tampering | breakwater.Render — raw HTML in Markdown (`<script>`, `<iframe>`, `<img onerror>`, `<svg onload>`) | high | mitigate | goldmark default safe renderer replaces raw HTML blocks and inline HTML with an omitted-HTML comment; the policy disallows comments, so nothing of the raw markup survives (strip, not escape). Pinned by TestRenderXSS and TestRenderStripsRawHTML (Task 1). |
|
||||
| T-k9s-02 | Tampering | breakwater.Render / Purify — unsafe URL schemes (`javascript:`, `data:`, `vbscript:`, `tel:`) in links and autolinks | high | mitigate | goldmark blanks dangerous destinations; bluemonday `AllowURLSchemes("http","https","mailto")` parses every href and drops any other scheme or a relative URL, removing the `a` tag and keeping its text. TestRenderLinks and TestPurifyAllowList. |
|
||||
| T-k9s-03 | Elevation of privilege | plugin code calling `cabana.RenderMarkdown` (regex gate, no GFM) or `seaglass.Sanitize` (UGC policy: images, no nofollow) for user content | medium | mitigate | README "Not this module" paragraph and the docs page "Which gate" section name breakwater as the only gate for user content; 19.1-02 Task 1 greps `func Render(`/`func Purify(` under modules/ and refuses a local renderer (Pitfall 6). Compile-time enforcement is not possible; residual risk accepted at ASVS L1. |
|
||||
| T-k9s-04 | Spoofing | links with `target="_blank"` (reverse tabnabbing via `window.opener`) | medium | mitigate | `AddTargetBlankToFullyQualifiedLinks(true)` makes bluemonday add `rel="noopener"` alongside `target="_blank"`; caller-supplied `rel`/`target` are restricted to the values the policy writes; `RequireNoFollowOnLinks(true)` on every link. Asserted in TestRenderLinks and TestPurifyAllowList. |
|
||||
| T-k9s-05 | Information disclosure | goldmark `Convert` error path | low | accept | Single-return `Render`; the only error source is the writer and `bytes.Buffer` never fails. Documented in code; no partial unsanitized output can be returned because the buffer always passes through `Purify`. |
|
||||
| T-k9s-06 | Denial of service | pathological Markdown (deep nesting, huge tables) in `Render` | low | accept | goldmark and bluemonday are linear-time tokenizers already in production for cabana previews and seaglass; request size limits belong to the consuming plugin's route (19.1-02), not to this leaf module. |
|
||||
| T-k9s-SC | Tampering | npm/pip/cargo/go installs | high | mitigate | No install task: both libraries are already pinned in go.mod (bluemonday v1.0.27, goldmark v1.8.6). `git diff --exit-code go.mod go.sum` is in both tasks' verify; any new `require` fails the plan. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
From the repo root after Task 2:
|
||||
- `go test -short -count=1 ./modules/breakwater -v` lists TestRenderXSS, TestRenderStripsRawHTML, TestRenderGFM, TestRenderLinks, TestBlankInput, TestPurifyAllowList, TestConcurrentUse, all PASS.
|
||||
- `grep -rn 'func Render(' modules --include='*.go' | grep -v RenderMarkdown` and `grep -rn 'func Purify(' modules --include='*.go'` each print one line (the 19.1-02 D-17 gate).
|
||||
- `summer docs:build --check`, `go vet ./...`, `go test -short ./...` green; `git diff --exit-code go.mod go.sum` clean.
|
||||
- `git show --stat HEAD` lists exactly: modules/breakwater/breakwater.go, modules/breakwater/breakwater_test.go, modules/breakwater/README.md, README.md, docs/architecture/introduction.md, docs/services/markdown.md.
|
||||
- Unchanged: modules/cabana/field_markdown.go, modules/seaglass/* (`git diff --exit-code HEAD~1 -- modules/cabana modules/seaglass` after the commit is empty).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- D-16: the named policy matches the PHP HTMLPurifier config 1:1 (allow-list, three schemes, nofollow, target=_blank), built from `bluemonday.NewPolicy()`, not from the UGC policy; conversion is goldmark GFM with raw HTML stripped and unsafe links dropped.
|
||||
- D-17: a framework module (`breakwater`) exports `Render(markdown string) string` and `Purify(html string) string`; cabana.RenderMarkdown is untouched; README and docs page follow CLAUDE.md Documentation rules.
|
||||
- D-18 readiness: the PHP ForumMarkdownRenderTest corpus (XSS cases, GFM table, fenced code) plus links/autolinks and blank input are covered by Go tests the forum golden test can extend.
|
||||
- No new dependencies; one commit; nothing pushed; STATE.md untouched by the executor (the orchestrator clears the "D-17 Render/Purify missing" blocker and re-runs `/msd-execute-phase 19.1`).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/.planning/quick/261018-k9s-add-the-d-17-module-in-summercms-go/261018-k9s-SUMMARY.md` (orchestrator template). Note in it: the module name chosen (`breakwater`), the commit hash, and that 19.1-02 may now proceed (its gate greps `func Render(` / `func Purify(`).
|
||||
</output>
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
phase: quick-261018-k9s
|
||||
plan: 01
|
||||
quick_id: 261018-k9s
|
||||
slug: add-the-d-17-module-in-summercms-go
|
||||
status: complete
|
||||
subsystem: modules/breakwater
|
||||
tags: [markdown, sanitizer, bluemonday, goldmark, forum, D-16, D-17]
|
||||
requirements: [D-16, D-17]
|
||||
dependency_graph:
|
||||
requires: []
|
||||
provides:
|
||||
- breakwater.Render(markdown string) string
|
||||
- breakwater.Purify(html string) string
|
||||
affects:
|
||||
- 19.1-02 forum plans (D-17 gate: func Render( / func Purify( under modules/)
|
||||
- later Wavepath purify() callers (trainings)
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns:
|
||||
- package-level goldmark engine and bluemonday policy built once and shared (seaglass / cabana precedent)
|
||||
- named bluemonday policy built from NewPolicy, not the UGC preset
|
||||
- testable Example functions as the source of docs/ src= snippets
|
||||
key_files:
|
||||
created:
|
||||
- modules/breakwater/breakwater.go
|
||||
- modules/breakwater/breakwater_test.go
|
||||
- modules/breakwater/example_test.go
|
||||
- modules/breakwater/README.md
|
||||
- docs/services/markdown.md
|
||||
modified:
|
||||
- README.md
|
||||
- docs/architecture/introduction.md
|
||||
decisions:
|
||||
- "Module name: breakwater (D-17 left the name to the quick task)."
|
||||
- "Render has a single string return: goldmark Convert only reports its writer's error and bytes.Buffer never fails; the buffer always passes through Purify."
|
||||
- "RequireNoReferrerOnFullyQualifiedLinks is not set: the locked policy is the four bullets of the 2026-10-18 note; noopener comes with AddTargetBlankToFullyQualifiedLinks."
|
||||
- "Caller-supplied rel/target on a are restricted by regexp to the values the policy writes (nofollow|noopener|noreferrer tokens, _blank); anything else is dropped and rewritten."
|
||||
metrics:
|
||||
duration: "about 20 minutes"
|
||||
completed: 2026-10-18
|
||||
tasks: 2
|
||||
files: 7
|
||||
actuals:
|
||||
tokens: 5100
|
||||
tasks: 2
|
||||
commits: 1
|
||||
plan_head_before: 5ccf726e8ae9241ce7570b820669345405fa1a4e
|
||||
plan_head_after: 98bc6de
|
||||
commits: 1
|
||||
---
|
||||
|
||||
# Quick 261018-k9s: breakwater, the D-17 Markdown renderer and HTML sanitizer Summary
|
||||
|
||||
`modules/breakwater` exports `Render(markdown) string` (goldmark GFM in default safe mode, then the named forum policy) and `Purify(html) string` (the policy alone), with the bluemonday policy built from `NewPolicy()` 1:1 with the PHP `golem15/forum` HTMLPurifier config; one commit, no new dependencies.
|
||||
|
||||
**Module name chosen:** `breakwater` (a breakwater takes the force of the open sea; user Markdown and HTML break on the policy and only calm, allow-listed HTML reaches the shore).
|
||||
|
||||
**Commit:** `98bc6de` — `feat(breakwater): Markdown renderer and HTML sanitizer with the forum policy (D-17)`.
|
||||
|
||||
**19.1-02 may now proceed.** Its gate greps print one line each:
|
||||
|
||||
```
|
||||
modules/breakwater/breakwater.go:90:func Render(markdown string) string {
|
||||
modules/breakwater/breakwater.go:105:func Purify(html string) string {
|
||||
```
|
||||
|
||||
The orchestrator clears the STATE.md blocker "D-17 Render/Purify missing" (this executor did not edit STATE.md) and re-runs `/msd-execute-phase 19.1`.
|
||||
|
||||
## What was built
|
||||
|
||||
| Task | Result | Commit |
|
||||
|------|--------|--------|
|
||||
| 1. breakwater package (tracer, TDD) | `modules/breakwater/breakwater.go` and `breakwater_test.go`: RED (build failed, package absent) then GREEN on the first run; 7 tests pass. | 98bc6de (shared with Task 2 per the plan) |
|
||||
| 2. README, root table row, introduction.md row, docs/services/markdown.md, gates, single commit | All four doc artifacts plus `example_test.go` (see Deviations); `summer docs:build --check`, `go vet ./...`, `go test -short ./...` green. | 98bc6de |
|
||||
|
||||
The policy (`newPolicy`, per D-16): `AllowElements` of exactly the PHP list (no `img`, no `figure`), `href`/`rel`/`target` on `a` (the latter two regexp-restricted to policy-written values), `AllowURLSchemes("http", "https", "mailto")` (no `tel:`, no relative URLs), `RequireNoFollowOnLinks(true)`, `AddTargetBlankToFullyQualifiedLinks(true)` (which also adds `noopener`). Comments stay disallowed, which is what removes goldmark's `<!-- raw HTML omitted -->` marker. The package imports neither seaglass nor cabana nor `golang.org/x/net/html`; `cabana.RenderMarkdown` and `modules/seaglass` are untouched (`git diff --exit-code HEAD~1 -- modules/cabana modules/seaglass` is empty).
|
||||
|
||||
## Gate output
|
||||
|
||||
- `go vet ./...` — exit 0, no output.
|
||||
- `go test -short ./...` — exit 0 (all `ok` / `no test files`; the only failures seen during the task were the docs-tree tests before the `src=` fix below, resolved).
|
||||
- `go install ./cmd/summer && summer docs:build --check` — `docs:build: no problems found` (run as `$(go env GOPATH)/bin/summer`; `~/go/bin` is not on this shell's PATH).
|
||||
- `git diff --exit-code go.mod go.sum` — clean; bluemonday v1.0.27 and goldmark v1.8.6 were already pinned.
|
||||
- `go test -short -count=1 ./modules/breakwater -v` — PASS: TestRenderXSS, TestRenderStripsRawHTML, TestRenderGFM, TestRenderLinks, TestBlankInput, TestPurifyAllowList, TestConcurrentUse, ExampleRender, ExamplePurify (0.012s).
|
||||
- `git show --stat HEAD` lists the plan's six files plus `modules/breakwater/example_test.go` (7 files, 430 insertions, 1 deletion — the rewritten introduction.md row).
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] docs/ Go fences must carry `src=`; added `modules/breakwater/example_test.go`**
|
||||
- **Found during:** Task 2, gate `go test -short ./...` (cmd/summer docs-tree tests) and `summer docs:build --check`: `docs/services/markdown.md:13: snippet: go code block has no src= reference`.
|
||||
- **Issue:** The plan assumed the docs checker only verifies `src=` snippets, so it prescribed a plain fenced Go block on the docs page. `internal/docsite/check_policy.go` rejects every Go fence in a `docs/` page without `src=` (module READMEs are exempt; the README's plain block passed).
|
||||
- **Fix:** Following the in-tree convention (`docs/services/events.md` → `modules/festival/example_test.go#ExampleBus_Fire`), added `modules/breakwater/example_test.go` with `ExampleRender` and `ExamplePurify` (their `// Output:` lines make them real tests of the documented output), split the docs page block into two `src=modules/breakwater/example_test.go#Example…` blocks and ran `summer docs:sync`. The checker was not weakened.
|
||||
- **Files modified:** modules/breakwater/example_test.go (new), docs/services/markdown.md.
|
||||
- **Commit:** 98bc6de (the plan's single commit; it therefore has seven files, not six).
|
||||
|
||||
**2. [Rule 1 - Bug] Corrected a docs claim about inline raw HTML**
|
||||
- **Found during:** Task 2, before the gates.
|
||||
- **Issue:** The first draft of `docs/services/markdown.md` said `text <b onclick="x()">bold</b> more` renders to `<p>text more</p>`. goldmark replaces each raw tag with an omitted-HTML comment but keeps the inner text, so the real output is `<p>text bold more</p>` (verified with a throwaway test, since removed).
|
||||
- **Fix:** Docs text corrected to the real output. The plan's own test (`TestRenderStripsRawHTML`) only asserts `text` and `more` survive, which is still true.
|
||||
- **Files modified:** docs/services/markdown.md.
|
||||
- **Commit:** 98bc6de.
|
||||
|
||||
## Tracer feedback gate
|
||||
|
||||
Task 1 is `type="tracer"`; interactive mode with `human_verify_mode` at its default (`end-of-phase`) and an automated-only `<verify>`: re-ran the verify (vet, module tests, go.mod clean, no `WithUnsafe`/`UGCPolicy`/seaglass/cabana imports, both gate greps) — passed, so execution continued to Task 2 without a checkpoint.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None. Both exported functions are fully wired; no placeholder values.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None beyond the plan's register. T-k9s-01/02/04 are pinned by TestRenderXSS, TestRenderStripsRawHTML, TestRenderLinks and TestPurifyAllowList as the plan assigned. No new network endpoint, auth path, file access or schema surface was introduced.
|
||||
|
||||
## Not done by the executor (orchestrator's part)
|
||||
|
||||
- Docs commit of this SUMMARY and the quick-task PLAN (`.planning/quick/261018-k9s-…` left untracked on purpose).
|
||||
- STATE.md: blocker "D-17 Render/Purify missing" to be cleared; ROADMAP.md untouched (quick task).
|
||||
- Nothing pushed, nothing tagged.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
All five created files exist on disk; commit 98bc6de is in the log; `modules/cabana/field_markdown.go` and `modules/seaglass/` are byte-identical to HEAD.
|
||||
Reference in New Issue
Block a user