mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
---
|
||||
issue: 33
|
||||
issue_title: "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
|
||||
|
||||
```typescript
|
||||
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).
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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 only** — `end` reaches `lines.length`, same as today; no content is lost because only blank lines follow.
|
||||
2. **Section immediately followed by another header** — `isTopLevelSectionHeader` 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.
|
||||
Reference in New Issue
Block a user