Threat Model — Authoring (draft the document)
Phase 3.5. Write the deliverable to the section structure in output-structure.md. Read principles.md first — the style bar is "describe the project as it is, not as it should be," and "write so a human can read it": short, direct sentences (one idea each), plain words, active voice, real verbs over nominalizations, and short bulleted lists or table rows instead of piled-up noun stacks. Target the reading level of good developer documentation, not a research paper — accuracy first, but never at the cost of plain prose.
The deliverable deliberately mixes both kinds of content in one document:
- Unstructured — plain prose and short bulleted lists carrying the reasoning (scope, adversary model, properties provided / not provided, false friends, downstream responsibilities, known misuses).
- Structured — meaningful tables embedded inline: the §1.7 per-input-operand
input-trust table and contract-dimension matrix, the §1.8 output-taint
statements, the §1.17 closed disposition set, and the §1.1 boxed triager
quick-start. (The separate
machine-readable
threat-model.yamlisthreat-model-sidecar's job.)
Assemble from the upstream artifacts
- §1.2/§1.3 from the recon component-family carve and out-of-scope inventory.
- §1.4/§1.5/§1.7/§1.8 from the surface analysis (reachability preconditions, side-effects inventory, per-input-operand table, contract-dimension matrix, output taint).
- §1.6/§1.9/§1.10/§1.11/§1.12/§1.13/§1.14/§1.15 seeded from recon's mined maintainer positions and promoted as interview answers arrive.
- §1.1 header, §1.16, §1.17, §1.18 authored here to bind the whole together.
- Prior-policy back-map appendix from recon whenever
SECURITY.mdor an equivalent authoritative model existed; retain every source claim until a maintainer explicitly approves removing the map.
Provenance discipline (non-negotiable)
- Every non-trivial claim carries exactly one of (documented, source),
(maintainer, YYYY-MM), (assumption, QN), (inferred, QN). The
QNon an assumption or inferred tag resolves to §1.18. No hedge-tags ("implicit", "documented in purpose", "generally known"). - Prefer documented disclaimers to open questions. When code + docs show a
guarantee is simply not made (no thread-safety, no resource bound, no failure
atomicity), record it as a documented §1.12 disclaimer — the absence is
verifiable. Reserve
unresolved/ inferred for dimensions where a guarantee plausibly exists but was not confirmed. This is the main lever for cuttingMODEL-GAPwithout weakening closure safety. It is also the easiest thing in the model to overdo, so keep it inside the three bounds inoutput-structure.md: an absent guarantee is a disclaimer, but an absent behaviour you scanned for is a §1.5 assumption; a disclaimer carries only as far as the source you cite actually reaches; and a guarantee "plausibly exists" when the project has historically fixed reports of that class. Give every disclaimer a boundary and a tier — triage fails closed on a missing tier, so an untiered disclaimer escalates every report it should have answered. - An unratified property — inferred or assumption — never carries a
security-criticaltier. If the guarantee is unratified, leave the matrix rowunresolvedand put the choice to the maintainer in §1.18. Publishing an unratified security-critical guarantee is worse than publishing a gap: integrators build on §1.11. - Assumption vs inferred. Use (assumption, QN) for a conservative default
you are willing to act on now (it may close low-blast-radius reports under the
relaxedpolicy); use (inferred, QN) when the question is genuinely open (escalate-only under every policy). Do not relabel a guess as documented to make it close — that launders the author's inference into the project's authority and is forbidden. - Every inferred and assumption tag has a matching §1.18 item that states a proposed answer. Mapping is one-directional: inferred/assumption → question required; extra edge-case/meta questions are allowed.
- Keep the header's draft-confidence count (documented / maintainer /
inferred, plus assumption when used) current, and declare the triage
policy (
strictdefault, orrelaxed). A draft with no inferred / assumption is fully reviewed or overclaiming; mostly unratified is not ready to publish. - Retain tags in the published version — a closed report cites (maintainer, 2025-03), and bare prose is not defensible. Footnotes are fine; keep the chain of authority intact.
- A citation you cannot fill is fixed by opening the file, never by deleting the column. When a Provenance cell resists — you know the claim is right but cannot name a locator — read the source until you can, or tag the row inferred with a §1.18 question. Dropping the column keeps every claim's authority while removing its evidence, and it is invisible in review: the section still looks complete. §1.7, §1.8, §1.10 and §1.15 all require the column for this reason.
Section-specific must-dos
- §1.1 — version binding, reporting cross-reference, status (incl.
unratified draftwhen §3.7 applies), triage-policy declaration (strictdefault /relaxed), provenance legend, glossary pointer (inline a short plain-language glossary in the model — or copyglossary.mdalongsidethreat-model.mdin the target repo and link that copy — so non-expert readers can decode the jargon; use the reference glossary as the source to inline or copy from, and never link that skills-repo path from the published model — it will not resolve in the target repo), draft-confidence count, sibling models, generation metadata (producing model/agent + version, effort level, and the plugins/skills actually used, or "human-authored"), and the boxed triager quick-start whose steps reference sections that actually exist. For the backtest note, on the initial 3.5 draft write exactly- **Backtest note**: _pending phase 3.6_and nothing more. Drafting is phase 3.5; you have not run the backtest and cannot know its result. Phase 3.6 replaces the placeholder with real figures, and self-check Gate 3 fails if the placeholder survives to publication — which is the point. A drafted-in note saying the backtest passed is how a model comes to certify a gate that never ran. On any later pass, never overwrite a real backtest note and never author one yourself. Authoring runs again in the 3.7 revision loop, normally after 3.6, so re-applying the placeholder here would delete the one figure set the gate exists to check and fail the model on a gate it had passed. The quick-start must open with the triage-policy step and close with the provenance gate — which tag may close, which must escalate, and the security-critical and silence floors — plus theclosed/provisional/escalatedstatus vocabulary. A box that routes to a disposition without gating the close is the single most common way a published model licenses a close it is not entitled to make. - §1.7 — a table, not prose; mark any untabled remainder from the surface timebox. Include control kinds and the per-family contract-dimension matrix. Every matrix row is claimed, disclaimed, N/A with reason, or unresolved.
- §1.7-§1.12 contract closure — promote claimed rows to §1.11 (or their owning environment/output section), disclaimed rows to §1.3/§1.12, and unresolved rows to proposed-answer §1.18 questions. No row may remain implicit.
- §1.8 — state the taint of every output channel, including the "output is as untrusted as input" one-liner where it applies; promote structural output invariants to §1.11.
- §1.11 — each property carries a violation symptom and a severity
tier; resource properties state a threshold, not just a direction. The
section closes with a
### Worked routing examplestable of 2–4 de-identified rows, at least one routingVALID, exported by phase 3.6. On a re-run, preserve what 3.6 exported rather than re-deriving it. - §1.12 — at least as substantive as §1.11; call out false friends (CRC≠MAC, hash≠collision-resistant, PRNG≠CSPRNG, sandbox≠isolation) and name the well-known attack classes for this category (compression bombs, XXE, ReDoS, billion-laughs) — one sentence each.
- §1.13 — at least as substantive as §1.11; fold in every §1.6 dev-only knob and every risky §1.8 "must not assume" as a positive obligation.
- Stateful APIs — state failure postconditions where relevant, including callback/collaborator exceptions and partial mutation.
- §1.17 — the closed disposition set, each citing its licensing section,
including
dependency-contract; add the all-status closure constraint. Optionally add the Mermaid triage-decision flowchart when it aids a non-expert triager — it must mirror the precedence order exactly, not introduce a second rule. - Closure safety — an inferred claim never licenses a closing
disposition, regardless of status. An assumption closes only what the
declared triage policy permits (
strict: never;relaxed: low-blast-radius provisional closes only), and never a security-criticalproperty-disclaimed,KNOWN-NON-FINDING, ordependency-contract. An accepted model has no inferred or assumption claims; retain review/draft status while any §1.18 item remains.
Style rules
- Plain prose and short lists. Tables only when every cell is meaningful — no templated tables with empty cells.
- When a property is not guaranteed, say so plainly ("Constant-time comparison is not provided" beats silence).
- Do not hedge into uselessness ("may or may not be safe depending on usage"). If
you cannot get a clear answer, record an
unresolvedmatrix row and a proposed-answer §1.18 question. - Cut anything that belongs in a code review/audit, restates the README, or is a
generic platitude. Every section is substantive or marked
Not applicable — <reason>.
Output
Root-level threat-model.md, beside threat-model.yaml and
threat-model.json, ready for the
threat-model-backtest gate and the threat-model-sidecar derivation. Keep it
to one sitting (3–8 pages) — sprawl is a smell.


