Files
my-pi/pi-permission-system/docs/plans/archive/0033-fix-findsection-greedy-end.md
T

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 findSection stop 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.fails test for bug #33 to a passing assertion.

Non-Goals

  • Refactoring sanitizeGuidelinesSection — it uses the same findSection but is always followed by another section in practice; any latent issue there is covered by the same fix.
  • Changing the isTopLevelSectionHeader heuristic 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

  1. Section at EOF with trailing blanks onlyend reaches lines.length, same as today; no content is lost because only blank lines follow.
  2. Section immediately followed by another headerisTopLevelSectionHeader fires first, same as today.
  3. Section followed by non-bullet, non-indented prose — new isSectionBodyLine check fires, end stops before the prose. This is the fix.
  4. Guidelines section — same findSection is used, same fix applies.

Module-Level Changes

src/system-prompt-sanitizer.ts

  • Add isSectionBodyLine(line: string): boolean helper.
  • Update findSection loop to stop at non-body lines (see Design Overview).

tests/system-prompt-sanitizer.test.ts

  • Change test.fails for bug #33 to a regular test.
  • 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.

TDD Order

  1. Red → green: flip the existing test.fails to test and verify it fails before the fix.

    • Surface: tests/system-prompt-sanitizer.test.ts — the test.fails for bug #33.
    • Commit: test: expect content after Available tools section to be preserved (#33)
  2. Green: implement isSectionBodyLine and update findSection.

    • 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)
  3. 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)
  4. 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.