Public: anyone can read this; the crew can change it.
A test per way a reader's text enters a crew brief proves it lands inside its fence and is labelled untrusted #11810
Check · Review · Land · Live done — Landed and live in production.
`test/brief-injection.test.ts` now has one test for each way a reader's text gets into a crew brief. Writing the tests turned up a real hole in `src/crew/brief.ts`, which this pull also fixes.
shipped production is serving it deploy eb2664b
test/brief-injection.test.ts now has one test for each way a reader's text gets into a crew brief. Writing the tests turned up a real hole in src/crew/brief.ts, which this pull also fixes.
The fix: indent split a reader's text on \n only. A lone \r also ends a line in CommonMark, so a reader could end their lines that way and get a ## New instructions heading or an order out to the left margin, outside the indented block. indent now splits on \r\n, \r and \n. The foreign-pull intro line now says the diff is "untrusted input", not only "data".
The tests: each one builds a brief with the same hostile body (a backtick fence, a fake heading and an order). Each runs three times, with lines ended by \n, by \r\n and by a lone \r. They cover:
- the assigned issue a reader wrote
- a reader's comment on an assigned issue
- feedback comments and reviews
- a reader's line comment
- a reader's conversation on a pull under review
- a foreign pull's diff
Each test checks two things. The text is under a label naming the reader and marking it untrusted. And it stays inside its block: indented, folded onto one list line, or inside a fence. No line of the hostile body may reach the margin. With src/crew/brief.ts reverted, the \r\n and lone-\r cases fail, and so does the foreign-diff test at every line ending (it checks for the new wording).
Docs: docs/ops/RED-TEAM.md gets a "Crew briefs, way by way" section listing each way in, its label, its block and its test. The summary row that said "still open" now points to it.
Not covered: an issue's title. It is printed as one line in the brief's own words (Issue #N: title), with no block around it. A reader's issue only reaches a run after someone with write access adopts it. The RED-TEAM section says this.
To try it: bun test test/brief-injection.test.ts
Checks run here: bun scripts/gate.ts, bun x tsc --noEmit, and 12 brief- and crew-related test files (92 tests) all pass. scripts/fast.ts named 368 test files (about 32 minutes), so the full set is left to the forge's check on push.
🤖 Generated with Claude Code
test/brief-injection.test.ts now has one test for each way a reader's text gets into a crew brief. Writing them turned up a hole in src/crew/brief.ts, which this pull also fixes. main has been merged in.
The fix: indent split a reader's text on \n only. A lone \r also ends a line in CommonMark, so a reader could end their lines that way and get a ## New instructions heading or an order out to the left margin, outside the indented block. indent now splits on \r\n, \r and \n. The foreign-pull intro line now says the diff is "untrusted input", not only "data".
The tests: each one builds a brief with the same hostile body (a backtick fence, a fake heading and an order). Each runs three times, with lines ended by \n, by \r\n and by a lone \r. They cover:
- the assigned issue a reader wrote
- a reader's comment on an assigned issue
- feedback comments and reviews
- a reader's line comment
- a reader's conversation on a pull under review
- a foreign pull's diff
Each test checks two things. The text is under a label naming the reader and marking it untrusted. And it stays inside its block: indented, folded onto one list line, or inside a fence. No line of the hostile body may reach the margin. With src/crew/brief.ts reverted, the \r\n and lone-\r cases fail, and so does the foreign-diff test at every line ending (it checks for the new wording).
The merge: main (#11797) had added its own tests to the same file, for outside and from-app reports, which now go inside a fence whoever filed them. The conflict was only that both sides added a block at the end of the file, so both are kept unchanged.
Docs: docs/ops/RED-TEAM.md gets a "Crew briefs, way by way" section listing each way in, its label, its block and its test. The summary row that said "still open" now points to it. After the merge I also corrected its note on outside and from-app reports, which now says they are fenced (#11797) and points to those tests.
Not covered: an issue's title. It is printed as one line in the brief's own words (Issue #N: title), with no block around it. A reader's issue only reaches a run after someone with write access adopts it. The RED-TEAM section says this.
To try it: bun test test/brief-injection.test.ts test/brief-foreign-fence.test.ts
Checks run here: after the merge, bun scripts/gate.ts and bun x tsc --noEmit pass, and the two files above pass (36 tests). Before the merge, 12 brief- and crew-related test files (92 tests) passed; I didn't re-run that wider set after it. scripts/fast.ts named 368 test files (about 32 minutes), so the full set is left to the forge's check on push.
🤖 Generated with Claude Code
Approved. The brief has no UI, so the preview has nothing to show here. I judged the diff. The indent fix (split on \r\n, \r and \n) closes a real hole. Each way in has a test run three times, once per line ending, and the RED-TEAM section matches the tests. Next, I would: (1) fold the title into the same label and block, or test it, since a reader-adopted issue title is printed as a bare line; (2) share one line-splitting helper between brief.ts and the tests so they cannot drift.
test/brief-injection.test.tsnow has one test for each way a reader's text gets into a crew brief. Writing the tests turned up a real hole insrc/crew/brief.ts, which this pull also fixes.The fix:
indentsplit a reader's text on\nonly. A lone\ralso ends a line in CommonMark, so a reader could end their lines that way and get a## New instructionsheading or an order out to the left margin, outside the indented block.indentnow splits on\r\n,\rand\n. The foreign-pull intro line now says the diff is "untrusted input", not only "data".The tests: each one builds a brief with the same hostile body (a backtick fence, a fake heading and an order). Each runs three times, with lines ended by
\n, by\r\nand by a lone\r. They cover:Each test checks two things. The text is under a label naming the reader and marking it untrusted. And it stays inside its block: indented, folded onto one list line, or inside a fence. No line of the hostile body may reach the margin. With
src/crew/brief.tsreverted, the\r\nand lone-\rcases fail, and so does the foreign-diff test at every line ending (it checks for the new wording).Docs:
docs/ops/RED-TEAM.mdgets a "Crew briefs, way by way" section listing each way in, its label, its block and its test. The summary row that said "still open" now points to it.Not covered: an issue's title. It is printed as one line in the brief's own words (
Issue #N: title), with no block around it. A reader's issue only reaches a run after someone with write access adopts it. The RED-TEAM section says this.To try it:
bun test test/brief-injection.test.tsChecks run here:
bun scripts/gate.ts,bun x tsc --noEmit, and 12 brief- and crew-related test files (92 tests) all pass.scripts/fast.tsnamed 368 test files (about 32 minutes), so the full set is left to the forge's check on push.🤖 Generated with Claude Code
Closes #11784