fix(web): restore styling under CSP, and give KB articles a detail page - #13
Open
TbusOS wants to merge 1 commit into
Open
fix(web): restore styling under CSP, and give KB articles a detail page#13TbusOS wants to merge 1 commit into
TbusOS wants to merge 1 commit into
Conversation
Two defects made the web UI unusable for its Knowledge Base. First, every page rendered with no styling. All CSS is one inline <style> emitted by layout(), and the response carried `default-src 'self'` — a policy that drops inline styles unless they are allowed by 'unsafe-inline' or matched by a nonce. Measured: 0 CSS rules applied under the shipped header, 36 under either 'unsafe-inline' or no header at all. The same HTML opened over file:// was always fine, which is why this never showed up outside a browser hitting the server. Each response now carries a fresh nonce and a matching style-src 'nonce-...'. Reaching for 'unsafe-inline' would have fixed the symptom while admitting any injected style — the opposite of what the header is there for. Second, KB articles had no detail page. /kb listed them, the titles were not links, and no /kb/<topic> route existed, so a reader could see a row and nothing else — while TASKS lists T-119 (KB Article page) as done. There is now a detail route rendering chapters, a table of contents built from the headings, and a metadata panel, plus web/markdown.py: a small renderer covering the constructs the chapter guide asks for. It escapes before emitting on every branch, keeps code spans out of the emphasis pass, and refuses link targets that are not same-document or http(s). Chapter names from `chapters:` and the topic from the URL are both author-controlled, so both are constrained to the article directory. 21 tests added (nonce/CSP pairing, detail rendering, traversal refusal, escaping, markdown blocks and inline). Full unit run: same 36 pre-existing failures before and after, 1355 -> 1376 passed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12.
1. Styling under CSP — per-response nonce
layout()emits one inline<style>; the response shippeddefault-src 'self', which drops inline styles. The browser was thereforerendering every page with its own defaults.
Each response now generates a fresh nonce, stamps it on the style block, and
sends a matching
style-src 'nonce-...'.'unsafe-inline'was deliberatelynot used — it would restore the look while admitting any injected style,
which is the thing the header is there to stop.
_nonce_attr()rejects a nonce that is not base64url-safe, so a caller cannotbreak out of the attribute.
Measured before/after on the same document:
rgb(250,249,245)No CSP violations reported by the browser after the change.
2. KB article detail page
/kb/<topic>route, mirroring the existing/memory/pattern.render_kb_detail()— chapter bodies, a table of contents built from eachchapter's headings, and a metadata panel (lifecycle / scope / author /
chapters / word count) with a digest freshness badge.
/kblist titles now link to their article.web/markdown.py— a small renderer for the constructs the chapter guideasks authors to use: headings with stable anchors, paragraphs, fenced code,
tables, lists, block quotes, rules, and the inline set. Not a CommonMark
implementation, and it says so; anything outside that set renders as text
rather than being guessed at.
Input treated as untrusted
Both the topic in the URL and the
chapters:entries inREADME.mdareauthor-controlled and are constrained to the article directory. A chapter entry
containing a separator or a leading dot is skipped; a topic that is not a plain
directory name yields 404. An unreadable article and a missing one both answer
404, so the response does not disclose directory shape.
Every renderer branch escapes before emitting. Code spans are lifted out before
the emphasis pass so their contents are never reinterpreted. Link targets that
are not same-document, relative, or http(s) degrade to their label — a
javascript:target never becomes anhref.Tests
21 added: nonce/CSP pairing, per-response freshness, absence of
'unsafe-inline', malformed-nonce rejection, detail rendering, list linking,404 paths, traversal refusal (parametrised), chapter-content escaping,
out-of-directory chapter entries, and the markdown block/inline/safety set.
Full unit run — identical 36 pre-existing failures before and after
(
test_scope,test_pool_git_sync,test_observer_install,test_canonical_uri,test_task_hash; all subprocess/git related and unrelatedto this change). Passing count 1355 → 1376.
Not in this change
The design mockups under
docs/design/carry a full system(
anthropic.css+app.css, ~36 KB) that the served UI does not use. Adoptingit means serving static assets rather than inlining, which is a larger change
and a separate discussion. This PR makes the article readable within the
existing style vocabulary; it does not reproduce the mockup.