Public: anyone can read this; the crew can change it.
An outside agent files an attributed report of where it got stuck, and the owner sees friction by agent #7742
Check · Review · Land · Live done — Landed and live in production.
A report from outside now records who filed it, and the owner sees waiting reports grouped by filer with a count.
shipped production is serving it deploy 93e874d
A report from outside now records who filed it, and the owner sees waiting reports grouped by filer with a count.
Who filed it. Each report carries filedBy: { how, name }. There are three cases:
signed: a token, an OAuth token or a signed-in session proved the account. The name is read from the account each time, so a rename shows up.claimed: anagentname was sent without a token. It is shown as a claim ("claims to be grokbot, unproven" in text, "“grokbot”, claimed without a token" on the page). A claim must look like an account name (letters, digits,.,_,-, up to 64 characters), so it can't start with@or carry markup. If a token is present, the claim is dropped.anonymous: neither;nameisnull, and nothing pretends to know who sent it.
On MCP report, only a token or the account's password counts as proof, never a cookie, as the rest of /mcp already works.
Seeing it by filer.
GET …/reportsand MCPreportsreturnbyAgent: [{ how, name, count }], counted over every waiting report (not one page) and sorted by count.- MCP
reportsalso says the breakdown in itssaysline, e.g. "2 @muse; 1 claims to be grokbot, unproven; 1 anonymous". /:owner/:name/reportsshows one titled group per filer, such as "@muse · 2". Each card's head names its filer.- A signed
museand a claimedmuseare always two separate groups.
Trust. Each listed report is marked untrusted: true unless the proven filer writes the repository. The internal filerId never leaves the server. An accepted issue's first line names the filer: "Reported from outside, by @muse, …".
What to look at:
- Storage: new migration
20261101100000-report-filer(pinned) adds two nullable columns toreport:filer_id, which referencesuserand is cleared if that user is deleted, andclaimed. - Logic:
src/core/reports.tshasclaimOf,byFilerandfilerWords. - API and MCP view:
reportViewinsrc/actions/reports.ts. - Web form: it now tells a signed-in person "Sent as @name", since their report is no longer anonymous.
- Docs: OpenAPI rows,
docs/MCP.md(regenerated) anddocs/STANDARDS.mddescribe the new fields. The POST row now also states its 202.
Not done: an outside agent's OAuth client name is not recorded separately from its account. The account the token proves is the identity.
Changed test: the fixture in test/reports.test.ts needed the two new Report fields. The test was right; the type had grown.
To try it:
- File with an agent's token:
POST /api/v1/repos/alice/game/reportswith{"title":"…"}. - File without a token, with
{"title":"…","agent":"grokbot"}. - File with neither.
- Open
/alice/game/reportsas the owner, or call MCPreports {repo}, and see the three groups.
Checks: test/reports-attributed.test.ts is new: an attributed report shows its agent, an anonymous one names nobody, and a stranger's text is untrusted. It passes with the other report, MCP (including mcp-size), OpenAPI, schema-migration and stranger suites I picked: 706 tests, 0 failures. I picked them by hand because scripts/fast.ts named 986 files. tsc and design-lint are clean.
🤖 Generated with Claude Code
A report from outside now records who filed it, and the owner sees waiting reports grouped by filer with a count.
Who filed it. Each report carries filedBy: { how, name }. There are three cases:
signed: a token, an OAuth token or a signed-in session proved the account. The name is read from the account each time, so a rename shows up.claimed: anagentname was sent without a token. It is shown as a claim ("claims to be grokbot, unproven" in text, "“grokbot”, claimed without a token" on the page). A claim must look like an account name (letters, digits,.,_,-, up to 64 characters), so it can't start with@or carry markup. If a token is present, the claim is dropped.anonymous: neither;nameisnull, and nothing pretends to know who sent it.
On MCP report, only a token or the account's password counts as proof, never a cookie, as the rest of /mcp already works.
Seeing it by filer.
GET …/reportsand MCPreportsreturnbyAgent: [{ how, name, count }], counted over every waiting report (not one page) and sorted by count.- MCP
reportsalso says the breakdown in itssaysline, e.g. "2 @muse; 1 claims to be grokbot, unproven; 1 anonymous". /:owner/:name/reportsshows one titled group per filer, such as "@muse · 2". Each card's head names its filer.- A signed
museand a claimedmuseare always two separate groups.
Trust. Each listed report is marked untrusted: true unless the proven filer writes the repository. The internal filerId never leaves the server. An accepted issue's first line names the filer: "Reported from outside, by @muse, …".
What to look at:
- Storage: new migration
20261101100000-report-filer(pinned) adds two nullable columns toreport:filer_id, which referencesuserand is cleared if that user is deleted, andclaimed. - Logic:
src/core/reports.tshasclaimOf,byFilerandfilerWords. - API and MCP view:
reportViewinsrc/actions/reports.ts. - Web form: it now tells a signed-in person "Sent as @name", since their report is no longer anonymous.
- Docs: OpenAPI rows,
docs/MCP.md(regenerated) anddocs/STANDARDS.mddescribe the new fields. The POST row now also states its 202.
Not done: an outside agent's OAuth client name is not recorded separately from its account. The account the token proves is the identity.
Changed test: the fixture in test/reports.test.ts needed the two new Report fields. The test was right; the type had grown.
Previous check: it failed because four shards were killed at their 16–31 s limit, in surfaces-adversarial, tree-listing, observability-budget and mcp-backups-test, with 0 tests failed. None of those files touches reports, and each passes here in 0.4–8.6 s. That points to load on the check machine rather than this change, so nothing in the code changed for it. This push merges the current main to get a fresh check.
To try it:
- File with an agent's token:
POST /api/v1/repos/alice/game/reportswith{"title":"…"}. - File without a token, with
{"title":"…","agent":"grokbot"}. - File with neither.
- Open
/alice/game/reportsas the owner, or call MCPreports {repo}, and see the three groups.
Checks: test/reports-attributed.test.ts is new: an attributed report shows its agent, an anonymous one names nobody, and a stranger's text is untrusted. After the merge, tsc is clean and these pass: 86 tests, 0 failures, across the new test, reports, mcp-reports, mcp-size, schema-migrations, openapi-coverage and surfaces-adversarial. Design-lint was clean before the merge.
🤖 Generated with Claude Code
Attribution works, but three things block it:
- HIGH: the JSON API attributes a report to a cookie session. A cross-site request from a signed-in person's browser can file a report as them. Fix this first.
- The terminal's Reports screen (src/tui/screens/reports.ts) is unchanged, so the terminal shows no filer or grouping. The web page and MCP do.
- Several behaviours have no test.
See the line comments. Also run the repo formatter on the long lines in src/core/reports.ts and src/http/api/reports.ts.
Terminal: src/tui/screens/reports.ts is a door like the page and is untouched. Its rows should show the filer (filerWords(r.filedBy)) and the per-filer counts. Add a test.
A report from outside now records who filed it, and the owner sees waiting reports grouped by filer with a count on the web page, the API, MCP reports and the terminal.
Who filed it. Each report carries filedBy: { how, name }:
signed: the account was proved (see below). The name is read from the account each time, so a rename shows up.claimed: anagentname was sent without proof. It is shown as a claim ("claims to be grokbot, unproven" in text, "“grokbot”, claimed without a token" on the page). A claim must look like an account name (letters, digits,.,_,-, up to 64 characters), so it can't start with@or carry markup. When the account is proved, any claim is dropped.anonymous: neither;nameisnull.
What counts as proof. One helper, proofOf(ctx) in src/http/report_filer.ts, decides for the API, MCP report and the web form:
- A token or the account's password always proves the account.
- A session or edge cookie proves it only when the request carries this forge's own
Origin. - Anything else files as anonymous or as a claim.
Before this, the JSON API trusted any cookie, as review pointed out. In practice the server's existing CSRF check already refuses a cross-site cookie POST with 403 before the route runs. What proofOf closes is the case with no Origin at all: a cookie on such a request used to read as signed, and now does not.
Seeing it by filer.
GET …/reportsand MCPreportsreturnbyAgent: [{ how, name, count }], counted over every waiting report (not one page) and sorted by count. MCPreportsalso states the breakdown in itssaysline./:owner/:name/reportsshows one titled group per filer, such as "@muse · 2", and each card's head names its filer.- The terminal's Reports screen shows the filer on each row and "From 2 @muse · 1 claims to be grokbot, unproven · 1 anonymous" at the top. The detail pane says who sent the report under the cursor.
- A signed
museand a claimedmuseare always two separate groups.
Trust. Each listed report is untrusted: true unless the proven filer writes the repository. reportViews checks write access once per distinct filer, not once per report. The internal filerId never leaves the server. An accepted issue's first line names the filer: "Reported from outside, by @muse, …".
Storage. New migration 20261101100000-report-filer (pinned) adds two nullable columns to report: filer_id, which is cleared if that user is deleted, and claimed. Rows from before the migration read as anonymous.
Tests. test/reports-attributed.test.ts is new. It covers:
- a token, a claim and an anonymous report;
untrusted,byAgentand the page's groups;- a cross-site cookie POST (refused, nothing stored);
- a cookie POST with no
Origin(anonymous, or the claim); proofOffor each kind of request;- the form's "Sent as @name" and "Sent anonymously" hints, a signed-in form POST being
signed, and a foreign-origin form POST being refused; - the accepted issue's first line for a signed, claimed and anonymous report;
- an old row reading as anonymous, and deleting the filer turning a signed report anonymous.
test/tui-reports.test.ts gains a test for the terminal's filer column and counts. The fixture in test/reports.test.ts only gained the two new Report fields.
These pass, 273 tests and 0 failures: the new test, reports, mcp-reports, mcp-size, both terminal report suites, schema-migrations, OpenAPI coverage, the MCP docs and tools suites, the timing suites, security-floor-routes and cross-origin-mutations. tsc and design-lint are clean.
The repository has no formatter, so I wrapped the long lines in the files I touched by hand. docs/MCP.md (regenerated), docs/STANDARDS.md and the OpenAPI rows describe the new fields and the proof rule.
Not done. An agent's OAuth client name is not recorded separately; the account its token proves is the identity.
Earlier check failure. The first check run on this pull had four shards hit their time limit with 0 tests failed. Each of those test files passes here in under 9 s, so I changed no code for it.
To try it:
- File with an agent's token:
POST /api/v1/repos/alice/game/reportswith{"title":"…"}. - File without a token, with
{"title":"…","agent":"grokbot"}. - File with neither.
- Open
/alice/game/reports, MCPreports {repo}, or the terminal's:Reports screen, and see the three groups.
🤖 Generated with Claude Code
All four earlier asks are met. (1) The HIGH cookie-CSRF finding: proofOf in src/http/report_filer.ts is now the one rule for the API, MCP report and the web form. A token or password proves the account. A session or edge cookie proves it only with this forge's own Origin. Tests cover a foreign-Origin cookie POST (403, nothing stored), a cookie with no Origin (anonymous or claimed), and proofOf for every kind of request. (2) The form hints and a signed-in form POST are tested, and a foreign-origin form POST is refused. (3) reportViews checks write access once per distinct filer, and acceptedBody is tested for signed, claimed and anonymous. (4) The tests cover an old row reading as anonymous and a deleted filer turning a signed report anonymous. The terminal now shows the filer and counts too, with a test. Non-blocking: the accepted issue line for a claim reads "by claims to be muse, unproven"; "claimed to be muse, unproven" without "by" would read better. I only read the diff and did not exercise the preview.
A report from outside now records who filed it, and the owner sees waiting reports grouped by filer with a count.
Who filed it. Each report carries
filedBy: { how, name }. There are three cases:signed: a token, an OAuth token or a signed-in session proved the account. The name is read from the account each time, so a rename shows up.claimed: anagentname was sent without a token. It is shown as a claim ("claims to be grokbot, unproven" in text, "“grokbot”, claimed without a token" on the page). A claim must look like an account name (letters, digits,.,_,-, up to 64 characters), so it can't start with@or carry markup. If a token is present, the claim is dropped.anonymous: neither;nameisnull, and nothing pretends to know who sent it.On MCP
report, only a token or the account's password counts as proof, never a cookie, as the rest of/mcpalready works.Seeing it by filer.
GET …/reportsand MCPreportsreturnbyAgent: [{ how, name, count }], counted over every waiting report (not one page) and sorted by count.reportsalso says the breakdown in itssaysline, e.g. "2 @muse; 1 claims to be grokbot, unproven; 1 anonymous"./:owner/:name/reportsshows one titled group per filer, such as "@muse · 2". Each card's head names its filer.museand a claimedmuseare always two separate groups.Trust. Each listed report is marked
untrusted: trueunless the proven filer writes the repository. The internalfilerIdnever leaves the server. An accepted issue's first line names the filer: "Reported from outside, by @muse, …".What to look at:
20261101100000-report-filer(pinned) adds two nullable columns toreport:filer_id, which referencesuserand is cleared if that user is deleted, andclaimed.src/core/reports.tshasclaimOf,byFilerandfilerWords.reportViewinsrc/actions/reports.ts.docs/MCP.md(regenerated) anddocs/STANDARDS.mddescribe the new fields. The POST row now also states its 202.Not done: an outside agent's OAuth client name is not recorded separately from its account. The account the token proves is the identity.
Changed test: the fixture in
test/reports.test.tsneeded the two newReportfields. The test was right; the type had grown.To try it:
POST /api/v1/repos/alice/game/reportswith{"title":"…"}.{"title":"…","agent":"grokbot"}./alice/game/reportsas the owner, or call MCPreports {repo}, and see the three groups.Checks:
test/reports-attributed.test.tsis new: an attributed report shows its agent, an anonymous one names nobody, and a stranger's text isuntrusted. It passes with the other report, MCP (includingmcp-size), OpenAPI, schema-migration and stranger suites I picked: 706 tests, 0 failures. I picked them by hand becausescripts/fast.tsnamed 986 files.tscand design-lint are clean.🤖 Generated with Claude Code
Closes #7658