Found while implementing objectui#6515. Filed unassigned, recording only — not graded, no type.
Measured on the objectui#6515 branch, which is origin/main @ 28ed9ce8f plus that card's fix. The defect is older than that branch and unaffected by it: it lives in the normaliser's own anonymous branch, which objectui#6515 does not touch.
What was measured
buildExpressionUser has two branches, and only one of them carries id:
if(!u){return{name: 'Anonymous',email: '',role: 'guest',isPlatformAdmin: false,positions: []};}return{id: u.id,name: u.name,email: u.email,role: …,isPlatformAdmin: …,positions: …};So 'id' in buildExpressionUser(null) === false. A CEL predicate naming ctx.user.id /
current_user.id / os.user.id therefore hits an unbound key for a signed-out visitor,
FAULTS, and evaluateVisibility fails OPEN — the gate renders its field or action for
exactly the principal it was written to exclude, with nothing on screen to say so.
This is the same mechanism objectui#6515 fixed for RecordFormPage, but one level down: it
is in the shared normaliser, so it reaches every mount site — AppContent and the
console's InternalFormRoute included, both of which have always called the normaliser
correctly.
Measured directly, as a test on the #6515 branch
(packages/app-shell/src/providers/expressionUser.mountParity.test.tsx, the case named
RECORDS: a ctx.user.id gate STILL fails open for a signed-out visitor): with
authState.user = null and a field gated on ctx.user.id == 'u_admin', the field is
PRESENT in the schema handed to ObjectForm. The signed-IN case beside it is green — there
id is present and the gate bites.
Why it was not fixed in objectui#6515
That card's face is "make the mount site publish the normaliser's output unchanged". This
would change what the normaliser's output is, which is a different decision and has a
pin standing on it: AppContent.expressionUserShape.test.ts asserts the anonymous branch
toStrictEqual the five-key shape above.
The decision this needs
Three candidate shapes, and the choice is not mechanical:
id: null — the key exists and answers. ctx.user.id == '…' evaluates FALSE for a
signed-out visitor, which is the semantically right answer. Costs: null is a value CEL
authors then have to know about.id: undefined — rejected on precedent. objectui#5424 removed roles from this exact
object precisely because present-and-always-undefined "is the shape that teaches the
wrong thing": the context answers rather than being plainly absent, and the answer is
silently wrong.- Leave it absent — the status quo, i.e. accept fail-open for id-shaped gates on
anonymous sessions. Defensible only if no anonymous surface evaluates one; that is a
measurement nobody has taken.
Whichever is chosen, the two branches should end up agreeing on one key set — that
symmetry is what objectui#5424 was closing when it removed roles, and this is the last
place the two branches still disagree.
Related
Generated by Claude Code
Found while implementing objectui#6515. Filed unassigned, recording only — not graded, no type.
Measured on the objectui#6515 branch, which is
origin/main@28ed9ce8fplus that card's fix. The defect is older than that branch and unaffected by it: it lives in the normaliser's own anonymous branch, which objectui#6515 does not touch.What was measured
buildExpressionUserhas two branches, and only one of them carriesid:So
'id' in buildExpressionUser(null) === false. A CEL predicate namingctx.user.id/current_user.id/os.user.idtherefore hits an unbound key for a signed-out visitor,FAULTS, and
evaluateVisibilityfails OPEN — the gate renders its field or action forexactly the principal it was written to exclude, with nothing on screen to say so.
This is the same mechanism objectui#6515 fixed for
RecordFormPage, but one level down: itis in the shared normaliser, so it reaches every mount site —
AppContentand theconsole's
InternalFormRouteincluded, both of which have always called the normalisercorrectly.
Measured directly, as a test on the #6515 branch
(
packages/app-shell/src/providers/expressionUser.mountParity.test.tsx, the case namedRECORDS: a ctx.user.id gate STILL fails open for a signed-out visitor): withauthState.user = nulland a field gated onctx.user.id == 'u_admin', the field isPRESENT in the schema handed to
ObjectForm. The signed-IN case beside it is green — thereidis present and the gate bites.Why it was not fixed in objectui#6515
That card's face is "make the mount site publish the normaliser's output unchanged". This
would change what the normaliser's output is, which is a different decision and has a
pin standing on it:
AppContent.expressionUserShape.test.tsasserts the anonymous branchtoStrictEqualthe five-key shape above.The decision this needs
Three candidate shapes, and the choice is not mechanical:
id: null— the key exists and answers.ctx.user.id == '…'evaluates FALSE for asigned-out visitor, which is the semantically right answer. Costs:
nullis a value CELauthors then have to know about.
id: undefined— rejected on precedent. objectui#5424 removedrolesfrom this exactobject precisely because present-and-always-
undefined"is the shape that teaches thewrong thing": the context answers rather than being plainly absent, and the answer is
silently wrong.
anonymous sessions. Defensible only if no anonymous surface evaluates one; that is a
measurement nobody has taken.
Whichever is chosen, the two branches should end up agreeing on one key set — that
symmetry is what objectui#5424 was closing when it removed
roles, and this is the lastplace the two branches still disagree.
Related
idandisPlatformAdminby hand; thisfinding is the residue its shape pin surfaced.
precedent.
reportUnresolvableVisibilityPredicate's advice paragraph names node-tier roots (record,page.<var>) that no app-shellvisiblepredicate binds #6487 / Thedisabled/disabledOnnode gate has NO fault diagnostic in either build — and its fail-soft polarity greys a control out rather than showing it #6445 — fail-open on a faulting predicate is deliberate and isNOT what this card would change; the point is that the predicate should not fault.
Generated by Claude Code