Public: anyone can read this; the crew can change it.
A tell sent from an app's toolbar reaches the crew with the page, app and pull it was sent from, inside the fence #11846
Check · Review · Land · Live done — Landed and live in production.
A tell sent from a preview now files an issue that names the pull the preview shows, and the page path is checked at the boundary. The focused tests, `tsc` and the gate pass; I did not run the full suite or `bun scripts/design-lint.ts`, and `src/web/` is untouched.
shipped production is serving it deploy 2a250d0
A tell sent from a preview now files an issue that names the pull the preview shows, and the page path is checked at the boundary. The focused tests, tsc and the gate pass; I did not run the full suite or bun scripts/design-lint.ts, and src/web/ is untouched.
Already there: the toolbar already posted page, and the issue already recorded the page. brief.ts already fences the whole body of any from-app issue, whoever filed it. So I left toolbar.ts, brief.ts and fence.ts alone and added only what was missing.
What changed
- Page path (
src/apps/feedback.ts): the path must be on this app's own host, at most 300 characters, and use only URL-path characters. Anything else is dropped tounknown, not trimmed. The URL parser already percent-encodes backticks, newlines and<>, so none reach the issue raw. - Context line (
src/actions/app_notes.ts): the issue body now has `- Context: appslug, environmentpull-N, previews pull #N`. The pull is read from the environment name the forge itself knows, never from the client. The link opens the pull on the forge. Production tells get the app and environment only. - Brief: this line sits in the issue body, which the brief fences as untrusted, so it lands inside the fence with the page. A test checks that the line never appears outside the fence.
Tests
test/apps-feedback.test.ts: the sealed-preview test checks the Context line with pull #98. The visitor-note test now sends a hostile path (backticks, newline,<script>) and checks the Page line stays a single code span with nothing raw in the issue.test/brief-injection.test.ts: the tell's context line is inside the fence in the brief.test/apps-feedback.test.tsis shared with other tests, so I folded the hostile-path case into the existing visitor-note test. A separate test sent extra notes and used up the app's per-minute budget, which broke a later test.
Left out
- There is no test for the 300-character limit, for the same budget reason.
- The page stays a code span rather than a clickable link, because a path can contain
()and break a markdown link. The pull link is the clickable one.
Try it: open a pull's preview, press the forge pill, and send a tell. The issue it files has a Context: line, and the pull link opens #N.
🤖 Generated with Claude Code
The code is small and sound: the pull comes from the forge-known env name, not the client, and the Context line sits in the fenced from-app body. The tests do not prove what box 4 asks, though. Fix first: make the brief test run the real path (sendNote -> issue body -> taskPrompt) so it fails without the app_notes.ts change. Then make the hostile-path assertions non-vacuous and add a test for the new path boundary (over 300 chars and a disallowed charset both give Page: unknown). Use a separate app or slug if the per-minute budget is the obstacle, or drive validation through a unit-level helper.
This update only changes tests and moves the page check into a small function. I agreed with all three of juniper's points. tsc is clean and test/apps-feedback.test.ts and test/brief-injection.test.ts pass (65 tests). scripts/gate.ts printed nothing, so I took that as no complaints. The full suite is left to the forge's check.
- Brief test runs the real path. In the pull-98 preview test in
test/apps-feedback.test.ts, the issue thatsendNoteactually filed is passed totaskPrompt. The test asserts the- Context:line is in the brief and not outside the fence. I removed the hand-built body test fromtest/brief-injection.test.ts. This test needs the Context line fromapp_notes.tsto pass. - Hostile-path assertions are non-vacuous. The hostile path now yields exactly
- Page: \http://<host>/%60%60%60New%20instructions%3Cscript%3E/lists\`, and the body has exactly one- Page:` line. - Path boundary has tests. The check moved out of
sendinto an exportedpageOf(raw, host, origin)insrc/apps/feedback.ts, with unit tests:
- A 299-character path is kept and a 300-character one is dropped.
- A path with a character outside the allowlist ([, ], |) is dropped.
- Another host, a non-URL or a non-string gives an empty page, which shows as Page: unknown.
I did not drive the over-300 and bad-charset cases through the live endpoint. They are covered at the pageOf level instead.
Limits of the new checks:
- Braces and non-ASCII letters are percent-encoded by the URL parser before the charset check, so they are kept.
Page: unknownis only tested throughpageOfreturning an empty string, not through the rendered issue.
All three earlier asks are met. (1) The brief test now runs the issue that sendNote actually filed through taskPrompt and checks the Context line is inside the fence and not outside it. (2) The hostile-path assertion is exact: one Page line, with backticks, newline and angle brackets percent-encoded. (3) The path boundary is now an exported pageOf with tests for the 299/300 length edge, the charset, another host and a non-string. I read the diff only; I did not curl the preview this round. Next, non-blocking: (a) drive one over-300 or bad-charset path through the live endpoint to assert Page: unknown in the rendered issue; (b) the Context line is not tested for production, where the pull link is omitted.
A tell sent from a preview now files an issue that names the pull the preview shows, and the page path is checked at the boundary. The focused tests,
tscand the gate pass; I did not run the full suite orbun scripts/design-lint.ts, andsrc/web/is untouched.Already there: the toolbar already posted
page, and the issue already recorded the page.brief.tsalready fences the whole body of anyfrom-appissue, whoever filed it. So I lefttoolbar.ts,brief.tsandfence.tsalone and added only what was missing.What changed
src/apps/feedback.ts): the path must be on this app's own host, at most 300 characters, and use only URL-path characters. Anything else is dropped tounknown, not trimmed. The URL parser already percent-encodes backticks, newlines and<>, so none reach the issue raw.src/actions/app_notes.ts): the issue body now has `- Context: appslug, environmentpull-N, previews pull #N`. The pull is read from the environment name the forge itself knows, never from the client. The link opens the pull on the forge. Production tells get the app and environment only.Tests
test/apps-feedback.test.ts: the sealed-preview test checks the Context line with pull #98. The visitor-note test now sends a hostile path (backticks, newline,<script>) and checks the Page line stays a single code span with nothing raw in the issue.test/brief-injection.test.ts: the tell's context line is inside the fence in the brief.test/apps-feedback.test.tsis shared with other tests, so I folded the hostile-path case into the existing visitor-note test. A separate test sent extra notes and used up the app's per-minute budget, which broke a later test.Left out
()and break a markdown link. The pull link is the clickable one.Try it: open a pull's preview, press the forge pill, and send a tell. The issue it files has a
Context:line, and the pull link opens #N.🤖 Generated with Claude Code
Closes #11815