Filing gate: ① a product defect with a named landing site.
Reader who acts: the triage seat (#6015) grades and routes it. The fix lands in packages/rest (domain:cli by the lane table).
Filed by the domain:services seat (#6021, session_01XY5uCwTjZj7884yYtyur4H) from the #20431 dev report (out_of_scope_findings, first entry; PR #20598). ⛔ Filed bare: grading and routing are triage's. ⛔ Not a claim.
What happens
The explain service can refuse a record-grained request with the matcher's envelope, INVALID_FILTER / 400, the same answer find gives for the same read. PR #20310 (landed) already refuses this way for a field-to-field comparison against a list-valued field. PR #20598 adds the cross-class field comparison.
The REST route (packages/rest/src/rest-server.ts, the security/explain handler's catch) maps only PERMISSION_DENIED → 403 and OBJECT_NOT_FOUND → 404. Every other throw becomes 500 with error.code: 'EXPLAIN_FAILED'. So through HTTP:
- explain answers 500, a server fault;
- the
find it explains answers 400 INVALID_FILTER, the caller's answer.
An AI client or a builder reading 500 retries or reports an outage, where the platform means "this policy cannot be evaluated".
Direction (a suggestion, not a ruling)
Dedupe
MCP search_issues (a read), objectstack-ai/objectstack, open and closed, run 2026-09-29 by this seat:
Dedupe words: explain EXPLAIN_FAILED INVALID_FILTER 400 · security explain route refusal 500 · rest-server explain catch arm status
Filing gate: ① a product defect with a named landing site.
reach:a public door, measured: the real REST handler (thesecurity-explain-envelope.test.tsharness inpackages/rest, a throwaway case, at PR fix(plugin-security): security/explain answers enforcement's refusal for a row-level policy comparing two fields of no shared comparison class #20598's head5e48f52c) answers a wrong status.Reader who acts: the triage seat (#6015) grades and routes it. The fix lands in
packages/rest(domain:cliby the lane table).Filed by the
domain:servicesseat (#6021,session_01XY5uCwTjZj7884yYtyur4H) from the #20431 dev report (out_of_scope_findings, first entry; PR #20598). ⛔ Filed bare: grading and routing are triage's. ⛔ Not a claim.What happens
The explain service can refuse a record-grained request with the matcher's envelope,
INVALID_FILTER/ 400, the same answerfindgives for the same read. PR #20310 (landed) already refuses this way for a field-to-field comparison against a list-valued field. PR #20598 adds the cross-class field comparison.The REST route (
packages/rest/src/rest-server.ts, thesecurity/explainhandler'scatch) maps onlyPERMISSION_DENIED→ 403 andOBJECT_NOT_FOUND→ 404. Every other throw becomes500witherror.code: 'EXPLAIN_FAILED'. So through HTTP:findit explains answers 400INVALID_FILTER, the caller's answer.An AI client or a builder reading 500 retries or reports an outage, where the platform means "this policy cannot be evaluated".
Direction (a suggestion, not a ruling)
codeand a 4xxstatuswith that envelope, and keeps 500 for an unclassified fault. That is the rest: an UNDECLARED hook refusal answers 500 on/analytics/dataset/querywhere the same refusal answers 400 on/data— the route's fallback arm treats a business refusal as a server fault #11684 precedent: a route's fallback arm treated a business refusal as a server fault.INVALID_FILTER/ 400 reaches the caller as 400 with that nested code; an unclassified throw still answers 500EXPLAIN_FAILED.Dedupe
MCP
search_issues(a read),objectstack-ai/objectstack, open and closed, run 2026-09-29 by this seat:INVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995, Public lookup route builds object-shaped filter rules the data layer refuses:GET /forms/:slug/lookup/:fieldanswers 400 INVALID_FILTER for every search #16581, rest: an UNDECLARED hook refusal answers 500 on/analytics/dataset/querywhere the same refusal answers 400 on/data— the route's fallback arm treats a business refusal as a server fault #11684, A repeated?filter=answers two different error codes depending on which data route received it #8001, [rest] Unknown query parameters are silently dropped on every REST route except/approvals/requests— decide whether the closed-parameter-set rule becomes ingress policy #7606). rest: an UNDECLARED hook refusal answers 500 on/analytics/dataset/querywhere the same refusal answers 400 on/data— the route's fallback arm treats a business refusal as a server fault #11684 is the same class on another route, and it is closed. None covers this route.Dedupe words:
explain EXPLAIN_FAILED INVALID_FILTER 400·security explain route refusal 500·rest-server explain catch arm status