Split SECURITY.md into audited specs, publish per-audience security pages - #538
Conversation
…ocs/security - docs/specs/security.md is the public face — guarantees, what is not defended, known gaps, how it is checked, reporting — published whole at https://dormouse.sh/docs/security: new page and rail entry, README and homepage links; the generator now resolves relative repo links to GitHub at build time and fails on a missing target - docs/specs/security-{application,supply-chain,ci,audit}.md carry the FAIL IF checklists, one file per audit domain, each with a rationale file: 17,419 words in one file become 12,788 across five plus 5,970 of evidence below the fold, every check preserved (99 bullets) - Root SECURITY.md is now the GitHub security policy pointer, budgeted and ratcheted with the specs - Audit prompts claim specs by file under their **Scope line; spec-lint check 16 proves each security spec has exactly one owner (self-tested) - e2e-lint pins its phrases in security-application.md; every SECURITY.md citation in code, specs, installers, and workflows is repointed to the spec that now owns the heading - The INCONCLUSIVE audit issue headline now says it is not a finding, which the contract already claimed Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzKs92Rgryer7JRn9DPNv2
…ot copies - `/docs/self-host` and `/supply-chain` now show the security spec's own guarantee rows, undefended edges, and known gaps for their audience, rendered from docs.security.json; `securityAudiences` splits the table and both lists by the spec each entry's links name, and an entry naming no spec, a spec in neither group, or both fails the build - `SITE_ROUTES` sends a repository link into a published file to its page rather than GitHub, so the umbrella keeps repo-relative links spec-lint verifies down to the fragment; `assertRouteFragments` holds a fragment into a published page against that page's headings - security-pages.test.tsx pins the rendered entries by their repo links and requires the audit-method link rather than forbidding others; the "installer rejects Funnel" claim is gone with the hand-written copy (it is `manage verify` that fails) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVadJTAp3djwEFhFdsGMxG
…age per audience - docs/specs/security-remote.md: the Remote Control checks, moved verbatim (48 FAIL IF); docs/specs/security-local.md: loopback listeners moved, plus first-draft sections for terminal output, browser panes, the dor control socket, and persisted state (15 FAIL IF, 7 new), each claim verified against code and reviewed — one inversion and five over-strong claims caught and corrected before publishing - The umbrella gains five local guarantees, three undefended edges, and one known gap (the standalone log is written at the umask and records the dor socket path); push rows say push is opt-in, and remote-security-model.md states the rule: a Host that never enrolls to a server sends none - The pages now render three audiences from the spec: /docs/security shows the local application and the pipeline, /docs/self-host remote control, /supply-chain the supply chain; the umbrella page narrows its own three blocks to its audience and points at the other two - application-security owns both new specs; every citation, lint, and prompt repointed; dor-cli.md's "re-checked on every use" corrected to what the code does (checked before every bind); the OSC 52 registry row now names its owner Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVadJTAp3djwEFhFdsGMxG
… a server Enrollment is where the relay, the phone, and push begin, so the precondition is stated once at the top of security-remote.md and in the umbrella's intro, and the Web Push clause returns to its verbatim form. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVadJTAp3djwEFhFdsGMxG
The self-host runbook and the supply-chain inventory both read as if the coordinating server were part of every install. It is only needed for phone push notifications and remote control, so say that where each page introduces it, and drop the Claude Code name from the runbook's agent instruction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKfi4TNKoqQNELstoeKAu2
Deploying mouseterm with
|
| Latest commit: |
0c8ec27
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://8ecf21e9.mouseterm.pages.dev |
| Branch Preview URL: | https://security-fixup.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
The split holds up. I re-ran the lints against the PR's own AGENTS.md (the PR-run harness restores that file from base, so a naive node scripts/spec-lint.mjs here reports six phantom "spec not indexed" problems): spec-lint, spec-lint-selftest (17 load-bearing checks), e2e-lint, deploy-lint, loopback-lint, and public-docs-lint are all green. I also confirmed checkSecurityFold is load-bearing rather than vacuous by planting a Reserved: paragraph and a ## Future heading in docs/specs/security.md — both go red, and inlineToText(node.children) really does see paragraph text.
"Every check is preserved" checks out: the old SECURITY.md carried 92 - FAIL IF bullets and I traced every one into the new specs (106 now). The ones my first pass flagged as missing — the build.rs runtime pin, ci.yml's generator gate, the .vscode/ diff-window bullet, the application-security fan-out, the VERDICT: first-line rule — are all there, reworded or hard-wrapped.
Four things below, all doc/test-level; nothing wrong with shipped behavior. The fragment one is the only one I'd call a gap rather than a nit: this PR adds assertRouteFragments precisely so a link into a published page can't name a heading that page doesn't render, then writes three such links in page components, where that check cannot see them.
One coordination note: #531 also edits SELF_HOST.md and scripts/spec-word-budgets.json, so whichever lands second will conflict on the budgets JSON. It does not rename ## Keeping the relay up while the laptop sleeps, so the fragment docs/specs/security.md links there survives either order.
- security-pages.test.tsx holds every `#fragment` these three pages write into each other against the anchors the target page actually renders. `assertRouteFragments` cannot see them: it walks the generated Markdown, and these links are written in page components. Verified load-bearing by renaming `## What the installer does` in SELF_HOST.md — the generator stays green and only this check goes red. - SECURITY.md said "four audited checklists" above five links. - generate-docs.js: the `/docs/security` doc comment was stranded above `SECURITY_AUDIENCES` by a reorder; moved onto `buildSecurity` and extended to say what `pageBlocks` narrows. - SecurityDocs.tsx and website-docs.md said the spec is "published whole" at `/docs/security`; the page renders `pageBlocks`, so its guarantees table and two lists carry one audience's entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKfi4TNKoqQNELstoeKAu2
dormouse-bot
left a comment
There was a problem hiding this comment.
All four fixes check out, and the new fragment check is load-bearing: I renamed ## What the installer does in SELF_HOST.md and it goes red with /supply-chain links /docs/self-host#what-the-installer-does: expected [ 'security-model', …(14) ] to include 'what-the-installer-does'. Reading id="…" out of the rendered <main> rather than headings is the right call — the only ids inside <main> come from AnchoredHeading (DocsLayout's one other id, docs-nav-drawer, is outside it), so the check can't be satisfied by a non-anchor. Lints all green against the PR's own AGENTS.md: spec-lint, its self-test (17 checks), e2e-lint (20), deploy-lint (82), loopback-lint (5), public-docs-lint (13), installer-verify-test (46), and website/ at 191 tests.
One thing this commit missed. The correction it applies — the page is every section, not the file verbatim — landed in SecurityDocs.tsx, generate-docs.js, and two of the three spots in website-docs.md, but three other places still make the un-corrected claim, and two of them are the ones a reporter reads first. SECURITY.md says the spec is "published as written at" the URL, docs/specs/security.md's own front matter says "Published as written at", and docs/specs/website-docs.md's Surface table (line 31) still says "Whole docs/specs/security.md, minus title and front matter" while line 13 in the same file now says "every section of it". Measured off the generated docs.security.json, the page renders 9 of the file's 21 guarantee rows, 6 of 13 undefended edges, and 4 of 8 known gaps — so "as written" overstates it by about two thirds of the rows, and the reader following that link from SECURITY.md gets no signal the rest exist. Suggestions inline.
Two smaller ones I'd leave to your judgement rather than suggest: the PR description carries the same sentence ("/docs/security publishes the umbrella spec whole"), and SECURITY_DELTA's docstring in generate-docs.js opens "docs/specs/security.md is published whole — the guarantees, what is not defended, the gaps … are the point of the page — so nothing but the file's own title and its spec front matter is withheld". That one is true of the delta, which is what it documents; it only reads wrong because it reaches forward to the page.
Budget note for the docs/specs/security.md suggestion: that file is at 1849 words against a 1850 budget, so the reword needs node scripts/spec-lint.mjs --ratchet docs/specs/security.md in the same commit. SECURITY.md (37 words of headroom) and website-docs.md (16) absorb theirs.
The page renders 9 of the spec's 21 guarantee rows, 6 of 13 undefended edges, and 4 of 8 known gaps, so "published as written" overstates it. Last commit corrected that in SecurityDocs.tsx and two spots in website-docs.md and missed three: - SECURITY.md, the first thing a reporter reads before following the link - docs/specs/security.md's own front matter, the file the narrowing is applied to; +6 words, so its budget is ratcheted to 1900 - website-docs.md's Surface table, which still said "Whole" one screen below the route list this PR changed to "every section of it" SECURITY_DELTA's docstring stays about the delta but no longer reaches forward to the page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKfi4TNKoqQNELstoeKAu2
|
Both judgement calls taken:
Your counts reproduce exactly off the generated At |
dormouse-bot
left a comment
There was a problem hiding this comment.
All three landed at 75c6ac77 and read correctly — SECURITY.md now qualifies the link a reporter follows, docs/specs/security.md's front matter states the split at the file it is applied to, and website-docs.md line 31 agrees with line 13. The ratchet is right: 1856 words against the new 1900 budget, and re-running --ratchet on it is a no-op. spec-lint (31 specs, 62 files), spec-lint-selftest, e2e-lint, deploy-lint, loopback-lint, and public-docs-lint are all green at that head.
Both judgement calls read well too — scoping SECURITY_DELTA's docstring to the delta and pointing at audienceBlocks for the narrowing keeps each comment about the thing it sits on.
One instance left, inline: AGENTS.md line 61, which this PR added.
The spec index this PR added carried the same page claim the last two commits corrected elsewhere, and it is the file every agent session loads first. Budget ratcheted to 2800: the qualification cannot be stated in fewer words than the claim it replaces. The two remaining "published whole" phrases in website-docs.md, and the three in public-docs-lint.mjs, are the file-vs-excerpt sense — every section of the file publishes, so it may carry nothing staged — and stay correct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKfi4TNKoqQNELstoeKAu2
Why
SECURITY.mdwas one 17k-word file mixing the public promise with the auditor's checklist, and the website's security copy was hand-written prose that could drift from it. This splits the file by audit domain, makes the spec the single source, and renders each audience its own page from that source.What changed
Specs.
docs/specs/security.mdis the public face — guarantees, what is not defended, known gaps, how it is checked, reporting. TheFAIL IFchecklists move into one file per audit domain:security-local.md,security-remote.md,security-supply-chain.md,security-ci.md,security-audit.md, each with a rationale file below the fold. Every check is preserved. RootSECURITY.mdbecomes the GitHub security-policy pointer.The old
security-application.mdsplits by audience:security-remote.mdtakes the Remote Control checks verbatim (48FAIL IF);security-local.mdtakes loopback listeners plus new sections for terminal output, browser panes, thedorcontrol socket, and persisted state (15FAIL IF, 7 new). Each new claim was verified against the code — one inversion and five over-strong claims were caught and corrected before publishing.Website.
/docs/securitypublishes every section of the umbrella spec, with the guarantees table and the two lists narrowed to that page's audience — the spec file on GitHub is where every row appears together./docs/self-hostand/supply-chainrender the spec's own guarantee rows, undefended edges, and known gaps for their audience out ofdocs.security.jsonrather than keeping copies;securityAudiencessplits the table and both lists by the spec each entry's links name, and an entry naming no spec, a spec in neither group, or both fails the build. Dropping the hand-written copy also drops one claim that was wrong (the installer does not reject Funnel —manage verifyis what fails). Final commit reframes the coordinating server as optional on both pages, since it is only needed for phone push and remote control.Lints. spec-lint check 16 proves each security spec is claimed by exactly one audit prompt, self-tested.
assertRouteFragmentsholds a link into a published page against that page's headings, so repo-relative links stay verified down to the fragment.e2e-lint,deploy-lint, and every citation are repointed at the new files.Testing
pnpm testgreen: 2148 lib, 175 website, 62 standalone, plus all spec/deploy/e2e lints and their self-tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01MKfi4TNKoqQNELstoeKAu2