Repository navigation
fix(app-shell): a navigation visible gate withholds its entry when it cannot be evaluated (objectui#12700) - #12728
Merged
objectstack-fleet[bot] merged 5 commits intoOct 16, 2026
Conversation
…it cannot be evaluated (objectui#12700) A `current_user.can(object, verb)` navigation predicate answers from the `/auth/me/permissions` payload `ExpressionProvider` binds. Where no payload is bound (a frame with no permission provider: Home, Studio) the engine refuses, and `evaluateVisibility` answered the refusal fail-open, so the gated entry was drawn for everyone. The navigation surfaces (`useNavMemberGates`: sidebar, landing, activity roster; `nav:menu`; the command palette's navigation list) now evaluate through `evaluateNavigationVisibility`, which withholds an entry on a fault and reports it once with a new `withholding` consequence line. A blank gate is still drawn. `evaluateVisibility` (the object-field gate and the published export) is unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
…ion gate (objectui#12700) The sidebar's member gates now import `evaluateNavigationVisibility`; every layout test that stubs the provider module answers it with the stub it already used for `evaluateVisibility`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Contributor
✅ 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
|
`PredicateGateKind` gains `'withholding'` (the same class of change as `'concealment'`, objectui#6503, which shipped as minor), and the navigation surfaces move a shipped fault answer from fail-open to fail-closed. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Contributor
✅ 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
|
Contributor
Author
Review: ACCEPT (part 1 of objectui#12700) — objectui PR #12728 ·
|
objectstack-fleet
Bot
deleted the
claude/issue-12700-nav-can-permissions
branch
October 16, 2026 16:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #12700
Clause-②: yes
The
yesis measured:@object-ui/reactwidens its publishedPredicateGateKindunion by one member,'withholding'. No runtime signature moves;@object-ui/app-shell's publishedevaluateVisibilityis unchanged.Root cause (measured)
The card read the console warning as "the nav predicate reaches the engine without
permissions". Two measurements, on objectui mainf8267df93:MePermissionsProvideraround/apps/:appName/*only (apps/console/src/AppContent.tsx), and it renders its loading fallback, not the shell, until/auth/me/permissionsanswers. Under it,ExpressionProviderbinds theobjectsmap forcurrent_user.can. A probe throughuseNavMemberGateswith the served CEL envelope answered: denied → hidden, granted → shown, no warning. So the A1 assumption ("the nav predicate is evaluated before the payload loads") is false for the in-app sidebar: there is no loading window there./home(and the Studio frames) mount neitherMePermissionsProvidernorExpressionProvider. On/home,AppHeader's bell (useSharedActivityFeed) and Home's activity card (useHumanActivityFeed) derive their activity roster from every app's navigation throughuseNavMemberGates, so HotCRM'scurrent_user.can("crm_opportunity", "read")is evaluated by the no-provider default evaluator: the engine refuses ("this evaluation context carries no permission data"), andevaluateVisibilityanswered the refusal fail-open, i.e. "shown". That is the warning the card quotes, and it fires on every console load because the console lands on/home.The A1 half that holds: the safe default for
app-shell:visiblewas "show" (evaluateVisibilityis fail-open).What this PR changes (ruling Target 2)
evaluateNavigationVisibilityinExpressionProvider.tsx(package-internal, not on the entry). Same verdict asevaluateVisibilityfor every predicate that evaluates; on a fault it withholds the entry and reports once per predicate with the engine's reason. A blank gate stays "no gate" and is drawn: a fault reported by the fail-soft call is confirmed on the evaluator's own fail-closed seam (throwOnError) before the entry is withheld, so a predicate that evaluates never pays a second call.useNavMemberGates(sidebar, app-root landing,SidebarItemMenu, activity roster), thenav:menublock, and the command palette's navigation list.evaluateVisibilitykeeps its fail-open answer for the object-field gate (isObjectFieldVisible) and the published export.visibilityDiagnostic.ts: one new consequence row,withholding, because the existingvisibilityrow ("the gate did NOT bite … reads like one that said yes") would now be false for a navigation entry. Same opening line, so filters onUNRESOLVABLE_VISIBILITY_PREFIXstill catch it. The comment above the re-export inpackages/react/src/index.tsstated the member count, so it now points at the union instead.What this PR does NOT do (why
Part of)Target 1 for the
/homeand Studio frames is not reachable inside the claimed file surface: the only permission source the console has is theMePermissionsProviderthatapps/console/src/AppContent.tsxmounts for/apps/:appName/*. With this PR those frames withhold acan()-gated entry from the activity roster (the safe direction), and the refusal is still reported once there, so the card's "console warning is gone on a fresh HotCRM load" acceptance item is not met yet. Putting the same provider above every protected frame is a decision the report hands back (one fetch for the whole shell, but Home then waits on/auth/me/permissions). #12700 remains open for it.Pins
packages/app-shell/src/layout/__tests__/UnifiedSidebar.navCanPermissions-12700.test.tsxmounts the realUnifiedSidebarunder the realExpressionProvider,MePermissionsProvider(only itsfetcher, the network, is mocked) andMetadataProvider. The gated entry is adashboardentry, so the object-read prune (objectui#12109) cannot be what hides it:/auth/me/permissionsis pending the provider withholds the whole shell (its existing loading posture, not one entry), the entry is absent, and it appears once the answer grants read; one request;app-shell:visibleline naming the missing permission data and the withholding consequence.packages/app-shell/src/providers/__tests__/navigationVisibility-12700.test.ts: granted/denied answer silently; unbound withholds and reports once; another fault kind (unknown root) withholds; literals, an absent gate and a blank gate keep their answers; CONTROL:evaluateVisibilityandisObjectFieldVisiblestay fail-open on the same fault.(e) CONTROL, the record page's own
cangate:currentUserCan-4421.render.test.tsxpasses unchanged.Fifteen layout tests that stub the
ExpressionProvidermodule now also stubevaluateNavigationVisibilitywith the stub they already used forevaluateVisibility.Ablation (one-shot, on the committed fix)
useNavMemberGatesswitched back toevaluateVisibility(mutation confirmed on disk: mutant marker 1, fix marker 0) →UnifiedSidebar.navCanPermissions-12700.test.tsx: 1 failed, 4 passed; the red one is the no-payload pin. (a)–(d) stay green under the ablation, which is the root-cause measurement above: inside an app the payload is bound and the gate never faulted. Restored withgit checkout HEAD --, blob hash equal to HEAD's.Verification (head
3e7dfc0c7)turbo run build --filter='@object-ui/app-shell^...': 28/28.type-checkfor@object-ui/reactand@object-ui/app-shell(bothtsconfigandtsconfig.test.json): green after typing the spy.--maxWorkers=1, one area at a time:app-shell/src/providers+hooks+chrome108 files / 918 tests passed;app-shell/src/layout84 / 593 passed;app-shell/src/console/__tests__+ two react diagnostic suites 33 / 277 passed;react/src/utils+ the concealment diagnostic suite 7 / 82 passed; the rest ofapp-shell/src/consoleplusviews/__tests__/nav-*: 676 tests passed, 0 failed, but the run hit its 560 s cap before printing a summary, so that slice is partly NOT MEASURED. Final targeted run on3e7dfc0c7(both new files, 4421, 12109 sidebar, 12426 landing): 5 files / 35 tests passed. Not run locally: the rest ofapp-shell/src/viewsand the full suite (CI).eslint --no-inline-configon the 23 changed.ts/.tsxfiles: 0 errors (--format json: 23 files). The config enables no type-aware linting, so this diff cannot move the verdict on an untouched file.check:phantom-deps,self-import,unreferenced-sources,doc-example-readers,handler-key-reads,side-effects-array,element-data-source-declaration,spec-symbols,action-forward-parity,designer-field-key-parity,icon-record-names,i18n-keys,i18n-drift,i18n-designer-parity,control-bytes,test-path-roots,new-line-citations,check-hand-rolled-comment-mask.mjs,check-changeset-fixed.mjs,check-changeset-presence.mjs,check-changeset-no-major.mjs.Acceptance notes
visible(not the served CEL envelope) takes the evaluator's legacy path, which answeredfalseeven for a granted member in the probe. Served navigation carries the envelope (the spec normalizes the authored string), so it is outside this card's surface. Noted, not filed.Generated by Claude Code