Development record
sanitizeRuleBody indents every continuation line of a rule body by two spaces, which defuses the line-start contract the host-side parser splits on but not CommonMark: a two-space-indented heading line inside a list item still renders as a heading INSIDE that item. A repo-overridden domain can therefore put an unmarked heading in front of any reader that renders the injected block as markdown, wearing no repo-override label of its own. The line-start contract holds and this is already stronger than the pre-fix behaviour, which indented nothing at all; closing the CommonMark half needs a decision on whether rule bodies get escaped, fenced, or left as the parser contract defines them. Evidence: sanitizeRuleBody in internal/core/rules/rules.go.
sanitizeRuleBody indents every continuation line of a rule body by two spaces, which defuses the line-start contract the host-side parser splits on but not CommonMark: a two-space-indented heading line inside a list item still renders as a heading INSIDE that item. A repo-overridden domain can therefore put an unmarked heading in front of any reader that renders the injected block as markdown, wearing no repo-override label of its own. The line-start contract holds and this is already stronger than the pre-fix behaviour, which indented nothing at all; closing the CommonMark half needs a decision on whether rule bodies get escaped, fenced, or left as the parser contract defines them. Evidence: sanitizeRuleBody in internal/core/rules/rules.go.
Addendum (autonomous run A, lane drainS2, 2026-09-26): the CommonMark half is wider than continuation lines. The FIRST line of a body is defused only for the line-start contract: in CommonMark - # x is a list item whose content is a heading, so a one-line rule forges a heading too. A setext underline (=== or --- under a text line) and an HTML block opening <h2> do the same from any line, and an HTML block of that kind can interrupt a paragraph. That is why the fix is a ruling rather than a mechanical close: closing every construct means either a code-safe rendering that flattens legitimate structure, or a fence-aware escaper that is complete only by enumeration and changes the raw text the model reads.
Remedy grounds (2026-09-29)
- Why: the record's three forks, each stated with the change and the test that proves it; ruling AN (for the technical facilitator) is unanswered, so no fork is picked.
- Sources (consulted 2026-09-29): OWASP GenAI LLM01:2025, mitigation 6 'segregate external content' (https://genai.owasp.org/llmrisk/llm01-prompt-injection/); CommonMark 0.31.2, section 2.4 (backslash escapes do not work in raw HTML) and section 4.5 (a closing fence must be at least as long as the opening one) (https://spec.commonmark.org/0.31.2/). A computed-length fence is the only form the specification shows to be complete without enumeration; escaping is incomplete by the specification's own statement for HTML blocks.
- Rejected: a third-party Markdown sanitiser, a new dependency needing sign-off for a problem a fence closes in a few lines.
Frontmatter
| id | iss-2609020219265817 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2609020219265817-sanitizerulebody-indents-every.md
open on GitHub ↗ · commit history ↗
mentions3
- dcn-26092600000000062026-09-26 — Three of the rules loader's security records close, and one stays owed to the product thinker (autonomous run A orchestrator's lane brief, taken by the implementer of lane drainS2). (1) The home directory is never a session's repo root (iss-2609020219198779, answering the owed question "is a home-directory git toplevel a legitimate config scope, or excluded outright?" as the brief rules it): its `.abcd/` is the user layer, so the root walk passes over the home and a toplevel that is the home resolves like a non-repo directory. The lane narrowed the brief's "or an ancestor of HOME": a toplevel that contains the home, the shape of a hermetic harness that points `HOME` inside its checkout, stays the root because git vouched for it and its own `.abcd/` is its own; only the stop at the home is removed. A session whose working directory is the home still reads a `.abcd/` there as the working directory's, the posture question recorded on 2026-09-25. (2) A bundled guardrail that an override withholds is named on every load (iss-174): for COMMITTING, LOAD and PII, each bundled recall keyword, alias or rule missing from a list an override set goes to stderr with the file whose list is in force, and the merge stays per field. The other bundled domains are left out because a repository restates them in its own words, and a note on every restatement would bury the one that matters. Still owed to the product thinker: whether security-bearing lists should union with the bundled entries or take a replace-versus-extend marker instead (itd-117's finer-grained-merging follow-up). (3) The foreign-uid refusal says what it still reads (iss-2609251522588539): the note, the configuration chapter and the install how-to now say that a `.abcd/` at the working directory is read, as AGENTS.md has since 0434d475. (4) iss-2609020219265817 is deferred past v0.11.0, not closed. Every CommonMark heading construct in a rule body (ATX on any line, the first line included, setext, and HTML h1-h6) can be closed only by a code-safe rendering that flattens legitimate structure, or by a fence-aware escaper that is complete only by enumeration and changes the raw text the model reads. So "escaped, fenced, or left to the line-start contract" is the product thinker's ruling.
- iss-2610120656366937The rules loader fences every rule of any domain a user or repo override names (internal/core/rules/rules.go ~965, fix for iss-2609020219265817), including a domain whose override only changes recall or state, so bundled rule bodies lose their markdown in that case. Over-fencing only; no attack path.
- spc-2610111627213138memory-recall