Found during review of PR #1352 (issue #1163, part of #1127). Pre-existing (from commit cc37703), out of scope there — filed so it is not lost.
Two related defects in how a throwing access rule behaves on the MCP read path.
1. The raw error message reaches the assistant
When a field's access rule throws, the resulting text is handed to the MCP client verbatim, e.g.:
Operation failed: Cannot read properties of null (reading 'role')
That is an internal implementation detail of the application's own access rule, surfaced to an external consumer. It can name fields, session properties and code structure that the schema deliberately withholds — and it is precisely the class of information the rest of #1163's work goes to some length to conceal.
2. One brittle field fails every fields-bearing query on its list
resolveFieldsProjection evaluates every field's rule up front. So a single rule that throws — for example ({ session }) => session.role === 'admin' on an anonymous request — fails the whole query, even when the caller never asked for that field.
The blast radius is the list, not the field. A field the caller did not name, and may not even be allowed to see, can deny them every row.
This is the read-path counterpart of the tools/list problem #1163 fixed: listing is now contained per field (a throwing rule hides that field and the listing survives), while the read path deliberately still propagates, per ADR-0030's fail-loud convention. That asymmetry is intentional — but "fail loud" arguably should not mean "one bad rule denies the entire list to everyone", nor "hand the raw TypeError to the caller".
Acceptance criteria
Notes
Deciding this needs a view on what "fail loud" means at a boundary with an untrusted consumer: loud to the operator (logs, telemetry) is clearly right; loud to the assistant is what leaks. Those can be separated.
Related: #1360 (write-surface refusal leak), and decideAdvertisement's silent fail-closed catch on the listing path, which #1352 is adding a console.warn to.
Generated by Claude Code
Found during review of PR #1352 (issue #1163, part of #1127). Pre-existing (from commit
cc37703), out of scope there — filed so it is not lost.Two related defects in how a throwing access rule behaves on the MCP read path.
1. The raw error message reaches the assistant
When a field's access rule throws, the resulting text is handed to the MCP client verbatim, e.g.:
That is an internal implementation detail of the application's own access rule, surfaced to an external consumer. It can name fields, session properties and code structure that the schema deliberately withholds — and it is precisely the class of information the rest of #1163's work goes to some length to conceal.
2. One brittle field fails every
fields-bearing query on its listresolveFieldsProjectionevaluates every field's rule up front. So a single rule that throws — for example({ session }) => session.role === 'admin'on an anonymous request — fails the whole query, even when the caller never asked for that field.The blast radius is the list, not the field. A field the caller did not name, and may not even be allowed to see, can deny them every row.
This is the read-path counterpart of the
tools/listproblem #1163 fixed: listing is now contained per field (a throwing rule hides that field and the listing survives), while the read path deliberately still propagates, per ADR-0030's fail-loud convention. That asymmetry is intentional — but "fail loud" arguably should not mean "one bad rule denies the entire list to everyone", nor "hand the rawTypeErrorto the caller".Acceptance criteria
Notes
Deciding this needs a view on what "fail loud" means at a boundary with an untrusted consumer: loud to the operator (logs, telemetry) is clearly right; loud to the assistant is what leaks. Those can be separated.
Related: #1360 (write-surface refusal leak), and
decideAdvertisement's silent fail-closed catch on the listing path, which #1352 is adding aconsole.warnto.Generated by Claude Code