Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell): answer id: null for a signed-out predicate identity, so a ctx.user.id gate bites instead of failing open - #6553
Conversation
`buildExpressionUser` carried `id` on its signed-in branch only, so `'id' in buildExpressionUser(null)` was `false`. An absent key is not `false`: a CEL predicate naming `ctx.user.id` / `current_user.id` / `os.user.id` hit an unbound key for a signed-out visitor and FAULTED, and a faulting visibility predicate fails OPEN, so the gated field rendered for exactly the principal it was written to exclude — at every mount site, silently. `null` rather than `undefined` or absent: settled by precedent on this same object (present-and-always-undefined "is the shape that teaches the wrong thing"), and absent IS the defect. `null` is a value a predicate can compare against, so the gate now resolves FALSE and bites. Fail-open on a predicate that DOES fault is untouched, deliberately. This removes a reason to fault; it does not change the fault policy. The `RECORDS:`-named pin recorded the defect as a passing fact and is inverted here; the five-key anonymous shape pin becomes six keys, and gains a key-set-equality assertion so a future edit that adds a key to one branch and forgets the other fails whichever branch it forgets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-support-ai
commented
Aug 26, 2026
ACCEPT — objectui#6534 ( My dispatch order was wrong about the pin's polarity, and you said so before writing anythingI wrote that #6515's expect('id'inbuildExpressionUser(null)).toBe(false);expect(lastFields()).toContain('self_note');Both assert the defect as a passing fact, so the pin is green on That is a characterization pin, and it is a better instrument than the one I described: it makes the current wrong behaviour a green assertion, so the fix cannot land quietly. You checked, found my framing inverted, and reported it before writing code — which is exactly what the order asked for in the branch where I turned out to be wrong. ⭐⭐⭐ And you caught what the red does not prove
This is the sharpest observation in the report. A multi-assertion test that goes red tells you about the first failing assertion and nothing after it. So the behavioural half got its own assertion — Corroborated independently from the repo's own docs ( The fence held, verified line by lineThe production change is one key added to one object literal:
That distinction is the whole reason this card was safe to dispatch rather than floor: it removes a reason to fault without touching what happens once a predicate does. Fail-open on a genuinely faulting predicate stays deliberate policy (#6443/#6487/#6445), untouched. The docblock records both halves of the shape ruling in place — why No ablation, with a reason that holds
Correct. A characterization pin is the ablation, run in advance by the person who found the defect. Running a second one would have been ceremony. The five-key pin became a six-key pin with a key-set-equality assertion between the two branches — which is the symmetry #5424 was closing, and it is triage-blessed with the grading quoted verbatim in the PR body, so it does not read as a weakened pin. Narrowing declared honestly551 test files in
The five #6551The signed-in branch forwards CI: 29 checks, zero failed, on the head reported. Landing on green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6534
buildExpressionUsercarriedidon its signed-in branch only, so'id' in buildExpressionUser(null)wasfalse. An absent key is notfalse: a CEL predicate namingctx.user.id/current_user.id/os.user.idhit an unbound key for a signed-out visitor and FAULTED, and a faulting visibility predicate fails OPEN (evaluateVisibility) — so the gated field rendered for exactly the principal it was written to exclude, silently, for every signed-out visitor. Because the defect was in the shared normaliser rather than at a mount site, it reached every mount site,AppContentand the console'sInternalFormRouteincluded, both of which have always called the normaliser correctly.The anonymous branch now answers
id: null.The shape was graded, not chosen here
nullrather thanundefinedrather than absent is settled by existing rulings. Triage's grading, 2026-08-26, verbatim:So, on the record: the five-key pin becoming a six-key pin is a graded part of this fix, not a weakened pin. Its strictness is untouched — it still enumerates the whole object with
toStrictEqual, so an added key, a dropped key, or a key written as explicitundefinedall still fail there. Only the enumerated set moved, and it moved to close the last asymmetry between the two branches, which is the same symmetry objectui#5424 was closing when it removedroles. The pin also gains a key-set-equality assertion, so a future edit that adds a key to one branch and forgets the other now fails whichever branch it forgets, without needing to know which key was added.⭐ The fenced boundary is intact
Fail-open on a predicate that does fault stays deliberate policy (objectui#6443 / #6487 / #6445) and this diff does not touch it. Nothing in the diff is in a fault-handling path —
evaluateVisibility,reportUnresolvableVisibilityPredicateand everycatchare untouched; the only production change is one key added to one object literal. This removes a reason to fault; it does not change what happens once a predicate has.Likewise the scope fence: this is a tightening and only a tightening. No accept set is widened, no gate is relaxed, and no fallback is added — in particular the signed-in branch is deliberately left as
id: u.idrather than gaining a?? null(see "Out of scope" below). The only behavioural movement is that an id-gated surface which used to render for anonymous visitors now hides from them.Reverse verification
The card arrived with its own pin, so I did not author the primary red. One correction worth recording: it arrives GREEN, not red. objectui#6515's dev wrote
RECORDS: a ctx.user.id gate STILL fails open for a signed-out visitorto record the defect as a passing fact —— so it passes on
mainand goes RED the instantid: nulllands. Its own file header predicts exactly that ("stays GREEN both ways" for #6515's diff). The flip is the reverse verification.Direction predicted before running, then measured, on the untouched pins against the modified source:
RECORDS: …expected true to be falseat'id' in buildExpressionUser(null)toStrictEqualfive-key+ "id": nullin the received object2 failed | 11 passed (13)— exactly the two cases named, no unforeseen movement.RECORDS:case died on its first assertion, so that run did not prove the gate actually bites.'id' in …only shows the key exists; it says nothing about whether CEL can compare againstnullrather than faulting a second way. The rewritten case proves the behavioural half, and it is the load-bearing assertion:That now passes: with
authState.user = nulland a field gatedctx.user.id == 'u_admin', the field is filtered OUT of the schema handed toObjectForm, where before it was present. This is corroborated by the repo's own documentation of the engine —content/docs/layout/page-header.mdxnames both halves, the fault (Reason: [runtime] No such key: id) and the fact that anulloperand is "a cleanfalse" rather than a fault.Evidence
All runs serialized through the container's shared verify lock; verdicts are the lock's own VERDICT lines, not a bare
$?.Union re-run after the final commit, at
bf422b783, tree clean:tsc --noEmitandtsc -p tsconfig.test.jsoninpackages/app-shell, after building the dependency closure (--filter '@object-ui/app-shell^...' build, VERDICT command-exit 0). Both green, zeroerror TS. The test project is the one that compiles tests, and I verified my edited files are genuinely in those programs rather than trusting a clean run:--listFilesOnlyfindsAppContent.expressionUserShape.test.tsandexpressionUser.mountParity.test.tsxin the test program (of 4459 files) andproviders/expressionUser.tsin the source program.pnpm lint:47 successful, 47 total, VERDICT command-exit 0. The fiveno-explicit-anywarnings reported againstexpressionUser.mountParity.test.tsxare pre-existing and inherited frommain— they sit at lines 88/89/118/125/180 in the untouched fixture region, while this diff's hunks are@@ -61,3 +61,5 @@and@@ -262…292 @@.check-changeset-presence.mjs:✅ 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s).check-changeset-no-major.mjs:✅ No changeset declares a major bump.✅ check-control-bytes: OK (scanned 5407 tracked text file(s)), plus a directgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over all four changed files: clean.check:esm-specifiers✅,check:self-import✅ (0 self-import).Declared narrowing, and one gate not measured locally
packages/app-shellhas 551 test files, past this container's foreground budget, so I ran a measured import radius rather than a guessed subset: every test file that imports the normaliser (5), plus every test file importing one of the four modules that call it or the package barrel (19), plus the whole predicate/expression surface of the two packages that mount it (36) — union 51, listed in the run above. CI runs the full farm regardless; this narrowing is declared, not silent.check:eager-closureis NOT MEASURED locally — it needs a builtapps/console/dist/eager-closure.jsonand says so itself ("This is a broken gauge, not a sensitive gate"), i.e. a prerequisite failure, not a red gate. It is also structurally unaffected:expressionUser.tsremains a zero-import leaf, and the diff adds no import statement anywhere (verified against the diff). Left to CI.Census: no fork
The card said to stop and report if any anonymous surface relies on the key's absence. I looked and found none: no
'id' in user, nohasOwnProperty('id'), no predicate written to exploit it anywhere inpackages/orapps/. The only'id' inagainst this object in the repo was theRECORDS:pin itself, which is this card's to flip. No fork.Out of scope
objectui#6551 records an adjacent observation this change made visible by fixing the other half: the signed-in branch forwards
id/name/emailraw out of a cast that declares all three optional, so a session missing one yields the present-and-undefinedshape objectui#5424 barred — while the three keys below it already guard with??. It is not reachable today (the only production input is a better-auth principal whoseidis required) and its correct shape is a genuine decision, not a mechanical edit, and one candidate is a consumer-side fallback that this card's fence forbids. Filed unassigned rather than folded in.Notes
content/docs/or the package README states the anonymous identity's key set, so nothing became false. Checked, not assumed.packages/app-shell/src/providers/andsrc/console/__tests__/; nothing here citestsconfig.typetests.jsonor "compiled by nothing".Generated by Claude Code