7.4 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 33 | sanitizeAvailableToolsSection silently removes content after the last recognised section header |
Fix findSection greedy end boundary
Problem Statement
findSection in src/system-prompt-sanitizer.ts defaults end to lines.length when no subsequent top-level section header is found.
This means any content after the last recognised section — plain prose, custom instructions, trailing notes — is silently included in the section range and deleted by removeLineSection.
The bug is masked in production because the real Pi system prompt always places Guidelines: after Available tools:, so end is always updated before EOF.
Unit tests from #21 exposed the bug with a minimal prompt where "Other content" follows the tools section.
Goals
- Make
findSectionstop at the end of the section's own content (header + bullet/indented lines), not at EOF. - Preserve all content that follows the section being removed.
- Flip the existing
test.failstest for bug #33 to a passing assertion.
Non-Goals
- Refactoring
sanitizeGuidelinesSection— it uses the samefindSectionbut is always followed by another section in practice; any latent issue there is covered by the same fix. - Changing the
isTopLevelSectionHeaderheuristic beyond what's needed to fix the boundary. - Adding new config fields or schema changes.
Background
Relevant modules
| File | Role |
|---|---|
src/system-prompt-sanitizer.ts |
Contains findSection, removeLineSection, sanitizeAvailableToolsSection, sanitizeGuidelinesSection |
tests/system-prompt-sanitizer.test.ts |
Existing tests including the test.fails for #33 |
Permission surface
None — this is a system-prompt sanitisation bug, not a permission-policy change. However, the impact is security-adjacent: silently deleting post-section content could remove user-authored safety instructions from the system prompt.
Design Overview
Current behaviour
let end = lines.length; // greedy: eat to EOF
for (let index = start + 1; ...) {
if (isTopLevelSectionHeader(lines[index])) {
end = index;
break;
}
}
Proposed change
Treat the section as the header line plus all contiguous "section body" lines that follow it. A line is part of the section body if it is:
- blank, or
- a bullet (
- …), or - indented (starts with whitespace).
The first line that is non-blank, non-bullet, non-indented, and not a recognised section header marks the end of the section. A recognised section header also ends the section (preserving existing behaviour).
function isSectionBodyLine(line: string): boolean {
const trimmed = line.trim();
if (trimmed.length === 0) return true; // blank
if (trimmed.startsWith("- ")) return true; // bullet
if (line !== line.trimStart()) return true; // indented
return false;
}
Then findSection becomes:
let end = start + 1;
for (let index = start + 1; index < lines.length; index += 1) {
if (isTopLevelSectionHeader(lines[index])) {
end = index;
break;
}
if (!isSectionBodyLine(lines[index])) {
end = index;
break;
}
end = index + 1;
}
This ensures trailing blank lines between the section and the next non-section content are consumed by the section (avoiding stray blank lines after removal), while non-section prose is preserved.
Edge cases
- Section at EOF with trailing blanks only —
endreacheslines.length, same as today; no content is lost because only blank lines follow. - Section immediately followed by another header —
isTopLevelSectionHeaderfires first, same as today. - Section followed by non-bullet, non-indented prose — new
isSectionBodyLinecheck fires,endstops before the prose. This is the fix. - Guidelines section — same
findSectionis used, same fix applies.
Module-Level Changes
src/system-prompt-sanitizer.ts
- Add
isSectionBodyLine(line: string): booleanhelper. - Update
findSectionloop to stop at non-body lines (see Design Overview).
tests/system-prompt-sanitizer.test.ts
- Change
test.failsfor bug #33 to a regulartest. - Add new cases:
- Content after
Guidelines:section is preserved when Guidelines is the last section. - Content after both sections removed; trailing prose survives.
- Section at EOF (no trailing content) still works.
- Section followed by blank lines then prose — prose survives, extra blanks collapsed.
- Content after
TDD Order
-
Red → green: flip the existing
test.failstotestand verify it fails before the fix.- Surface:
tests/system-prompt-sanitizer.test.ts— thetest.failsfor bug #33. - Commit:
test: expect content after Available tools section to be preserved (#33)
- Surface:
-
Green: implement
isSectionBodyLineand updatefindSection.- Surface:
src/system-prompt-sanitizer.ts. - The flipped test should now pass.
- Commit:
fix: stop findSection at first non-body line instead of EOF (#33)
- Surface:
-
Add edge-case tests.
- Content after
Guidelines:preserved. - Both sections removed, trailing prose survives.
- Section at EOF with only trailing blanks.
- Commit:
test: add edge cases for findSection boundary (#33)
- Content after
-
Verify all existing tests still pass.
npm test— no regressions.
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Could this silently weaken a permission? | No policy logic changes. The fix only affects system-prompt text manipulation. However, the bug itself weakens safety by silently removing user content; fixing it restores the intended behaviour. |
isSectionBodyLine is too conservative and leaves bullet lines outside the section |
The heuristic mirrors the existing section format (header + bullets). Tests explicitly cover bullet-only and mixed content. |
isSectionBodyLine is too liberal and still eats non-section content |
Non-blank, non-bullet, non-indented lines stop the scan. The reproducer from the issue ("Other content") is the direct test. |
| Guidelines section has the same latent bug | Same findSection is used — the fix applies to both. Added edge-case test confirms. |
Open Questions
None — the fix is well-scoped and the issue's proposed approach aligns with the design above.