Uh oh!
There was an error while loading. Please reload this page.
Approvals Inbox: the raw payload panel is a platform-operator affordance, not the approver's read path - #5563
Merged
Conversation
…access Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
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
|
os-sales
marked this pull request as ready for review
August 21, 2026 12:53
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 21, 2026
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 freeto 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.
Fixes#5553
Verified at
d96c3727f— every gate verdict below was read from a run on that exact tree, after the final commit.The defect
The detail drawer's "Raw data (JSON)" panel rendered on
payload != nullalone — no principal check of any kind — so every business approver could expand (and one-click copy) the submitted record's complete raw row:id,created_by,updated_by,owner_id,organization_id, bare lookup ids, and the fields the object's metadata declareshidden: true. Reported from a live EHR deployment on 17.1.0 (objectstack-ai/objectstack#10734, @baozhoutao), where that declaration is a patient-data control. The app author had no legitimate lever to remove it — fieldhidden, view columns, app navigation, permission sets and env vars are all ineffective — so the remedies available in the field were patching the shipped bundle or injecting CSS.Premise re-verified against
origin/mainat0935a43bebefore editing, not relayed: the panel was still live and still unconditional. PR #5509 (9b9af8d, the row-remount fix) had already landed in this file and did not touch the panel.The fix
The panel is gated on
holdsStudioAccess, reused verbatim from this app'sstudioEntrymodule.studio.accessis a declared platform-scope capability a tenant org owner does not hold by design (one of the framework'sPLATFORM_ADMIN_ONLY_CAPABILITIES), and it already reaches the browser insystemPermissions[]from/api/v1/auth/me/permissions— the payloadMePermissionsProvidermounts around every route this page renders under. Nothing new is served, computed, or made authorable: no new config key (the card forbids minting one, and none was needed), no new i18n copy, and the panel is byte-for-byte unchanged for the platform operator it was written for. A business approver keeps the structured record summary, the approval chain, the activity feed and the decision actions; only the raw snapshot is gone.Reusing
studioEntry's predicate rather than mirroring the framework's four-capability platform-admin probe into the renderer is deliberate: one definition of "is this principal a platform operator rather than a business user", so the two surfaces cannot drift into two spellings of one fact (AGENTS.md #0.1). The narrower gate is also the safer one here —manage_usersis in the server's probe, and a user administrator has no business reading raw clinical payloads.Fail CLOSED — inverted from
hasCapabilitiesThe gate reads the raw
systemPermissionssignal, notusePermissions().hasCapabilities. That hook fails open on purpose, and that is right for an action button: the server still refuses the write, and hiding a holder's button is the worse outcome. This panel has the opposite stake — the measured defect is a non-holder seeing it — so every not-a-reported-grant answer denies: no provider mounted, a backend predating ADR-0066 that omits the field, the resolver'scatchpath that answers200with nosystemPermissionsat all, and a reported empty array. A deployment whose permission layer just failed must not be the one that leaks the snapshot. This is the same inversionstudioEntryalready documents for the/studio/*route gate, and it is why theundefined-vs-[]distinction objectui#4656 preserved is load-bearing here.Tests, and how the vacuum is guarded
The acceptance condition is that something does not render — which an empty render reproduces perfectly. So in
ApprovalsInboxPage.rawPayloadGate.test.tsxevery denial case also asserts the drawer it is denying inside (process label + business summary row), and thestudio.accesscase drives the same fixture through the same helper and finds the panel. An empty render fails the counter-probe; a gate stuck open fails the denials; neither can pass alone.created_byandorganization_idare the witnesses rather than proxies: both are in the page'sPAYLOAD_SYSTEM_KEYS, so the summary card already drops them and their values can reach the DOM only through the raw panel.Ablation (pure source — the root Vitest config aliases every
@object-uispecifier at that package's source, so no build artifact sits between the edit and the run). The mutation was proven on disk before the measurement, not inferred from an editor exit code: the gated formmaySeeRawPayload && selected.payload != nullwent 1 → 0 occurrences and the bareselected.payload != null && (went 0 → 1, withgit diff --statshowing the single-line change. Restoring the bare condition turns 3 of 4 cases red — the three denial cases, each on theRaw data (JSON)assertion, with the failure naming the found summary element it had just found in the DOM — and leaves thestudio.accesscase green, which is exactly the predicted direction. The mutation script carried atrap … EXIT INT TERMrestore, and the tree came back byte-identical (git status --shortandgit diff --statboth empty).The
rawDatalocale key staysDeliberate, not incidental: the fix gates the panel rather than removing it, so
tr('rawData', 'Raw data (JSON)')remains a live call site and the label still renders for a holder.check:i18n-dead-keysis green and no locale table is touched — removing the key would have meant editing ~10 locale files, outside this card's file fence, to delete copy that is still reachable.Gates run locally at
d96c3727fAll verdicts quoted from each gate's own output line, with exit codes captured before any pipe.
apps/consolevitest (whole project)Test Files 63 passed (63)·Tests 704 passed (704)apps/console/src/pages/system/(final commit)Test Files 10 passed (10)·Tests 64 passed (64)@object-ui/console type-checktsc --noEmit && tsc -b tsconfig.node.json --forceechoed — not a zero-match pass)@object-ui/console lint✖ 201 problems (0 errors, 201 warnings)— all pre-existingcheck-changeset-fixed/-no-major/-presencecheck-control-bytescheck:i18n-keys/i18n-drift/i18n-dead-keyscheck:eager-closure✅ Console eager closure is 3784.8 KB gzipped across 52 of 508 chunks (budget: 3867.2 KB, headroom: 82.4 KB)check:phantom-deps/check:self-import/check:esm-specifierscheck-lint-coverage/check-type-check-coverageLint scope, stated so it is checkable:
eslint .inapps/consolelinted 165 files (count read from--format json, population from ESLint's own config resolution, not a hand-picked list) with 0 errors and 201 pre-existing warnings. My new test file reports 0 errors / 0 warnings;ApprovalsInboxPage.tsxreports 19 warnings, all on pre-existing lines (the lowest is line 522, thebuildApproverIdentities(user as any)line that predates this change; the added block starts at 524). Type-aware linting is not enabled in the rooteslint.config.js— noprojectService, noparserOptions.project— so this diff cannot move the verdict of any file it does not touch. The repo-widepnpm lintandpnpm type-checkfarms are CI's run and are not duplicated here.check:eager-closurewas first read as a broken gauge (No eager-closure report … the console was not built), which is the state of any fresh worktree, not a finding about this diff. It was resolved by actually building the console rather than by argument. The structural expectation held:studioEntryis already in the eager closure viaApp.tsx→StudioRoute, andApprovalsInboxPagestays lazily loaded, so the import adds nothing eager.Scope
apps/console/src/pages/system/ApprovalsInboxPage.tsx, its new test, and a changeset — the dispatched file fence, not breached.Out of scope, deliberately and not silently: trimming the summary card by object metadata, and the server-side residual that sends the unfiltered snapshot to the client at all (tracked separately in the objectstack repo). Neither is asserted here, because asserting them would pin behaviour this change does not deliver. objectstack-ai/objectstack#10734 remains open as the originating report.
Generated by Claude Code