feat(roles): surface operation-level grants (permission.operations) in the roles/users UI - #1628
Conversation
…n the roles/users UI A role's permission.operations allowlist (Harper 5.0+) gets a structured editor in the role modal: group toggles (read_only/admin_read/standard_user/agent), a categorized operation picker with super_user-delegation badges and a custom-name escape hatch for component-registered operations, plus an effective-operations summary. The roles table gains an Operations column, and user forms note when the chosen role is restricted. The static catalog is extracted from the Harper source per version (5.0/5.1/5.2) since core has no discovery operation; unknown names are preserved, non-delegable ones (secrets store, managed backups, get_deployment_payload) are never offered. Fixes #1627 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a structured operations allowlist editor for managing role-level API restrictions in Harper 5.0+, along with associated catalog utilities, tests, and user assignment summaries. Key feedback includes refactoring a ref mutation out of useMemo in EditRoleModal, avoiding parameter mutation in preparePermissionForSave, using a native checkbox input instead of the custom text input in OperationsAllowlistEditor, correcting Tailwind CSS v4 syntax for dynamic variables, and ensuring the Badge component supports the warning variant.
Stabilize the picker's array identity with a serialization-keyed useMemo instead of a ref written during render, and make preparePermissionForSave return a copy instead of mutating its argument. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Treat a record-shaped operations key as a real database on pre-5.0 instances (v4 reserved no such field), by shape in the narrowing helper and by version in the default calculator. - Skip the per-keystroke permission parse entirely when the instance doesn't support allowlists. - Widen the custom-grant pattern: component operation names are exact strings (deploy-v2, acme.deploy), not identifiers. - Correct the non-delegable chip: on a restricted super_user role the entry is load-bearing, not a no-op. - Cover the full lens wiring with an EditRoleModal test (picker -> JSON ordering -> preparePermissionForSave -> alter_role). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…save parse From the Gemini delta review: getDatabasePermissionRecord ran once per table instead of once per database, the save path parsed without guarding against Monaco's stale validity markers, and RoleOperationsSummary had no tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… roles
Harper's verifyPerms returns early for super users ('admins can do (almost)
anything') and for structure users on DDL, both before the operations gate is
reached — so an allowlist on such a role is stored and validated but never
enforced. The editor, the roles table, and the user-form summary all claimed
the opposite, which is the worst failure mode a permissions UI has: an admin
believing they revoked access they still hold.
Also from the deep review:
- Offer only the canonical spelling of an alias pair; both spellings dispatch
to one handler whose authorization entry carries the canonical api_name, so
granting the alias was inert. Alias chips now say so.
- Surface a malformed operations value instead of rendering it as no
restriction; Harper treats any present value as active.
- Leave database-scoped structure_user roles (structure_user: ['db']) alone at
save time — they still rely on their explicit table permissions.
- Skip a database named __proto__ in the template and require own-property
lookups, so it can't hit the prototype setter and silently disappear.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tructure_user, inert grants Chris's review caught that my previous 'elevated roles' handling was wrong in both directions, verified against harper v5.2.2: - super_user/cluster_user: validateNoSUPerms rejects any multi-key permission setting those flags, so keeping the allowlist made alter_role 400 outright. It is dropped again at save time and the conflict is stated in the editor, the roles table, and the user-form summary before the author saves. - structure_user: only DDL short-circuits the gate (create/drop table and attribute, plus create/drop database when true) — the allowlist IS enforced for everything else, so the UI now names the carve-out instead of calling the whole list inert and inviting its deletion. cluster_user never reaches the gate code at all. - The 22 operations whose authorization entry omits api_name are badged 'not yet enforced' rather than promising a super_user delegation they cannot receive (harper#2175); they stay offered so roles are ready when that lands. - sql is authorized against table permissions rather than the allowlist, so the editor copy and the catalog entry say so. - The custom-grant fallback no longer re-offers alias spellings or case variants of known names, both of which the server rejects or ignores. - expandEffectiveOperations folds alias spellings into their canonical name, so the count agrees with the chips beside it (read_only is 13 handlers, not 15). - A transient JSON error no longer makes the section claim the role is unrestricted; it describes the last good parse. - The operation list gets an sr-only path, and the unrestricted cell an aria-label, so title is not the only affordance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kriszyp
left a comment
There was a problem hiding this comment.
This is going to be awesome!
Anyway, some issues to address, mostly minor.
🤖 Reviewed with Codex
- Raise the version floor to v5.0.0-alpha.8, the first tagged build with both the authorization gate and the role validation; earlier 5.0 alphas would take the key and fail the save. - Add registration_info to the gate-inert set: its entry omits api_name like the other 22, and a role carrying any allowlist is denied it even when listed. - Model the operations key by shape everywhere: a pre-5.0 role granting a database named 'operations' is no longer reported as a malformed allowlist in the roles table or the user-form summary. - Keep a database named __proto__ instead of dropping it — define the property rather than assigning through the prototype setter. - Distinguish the structure_user forms: the array shape reaches DDL only on its listed databases and never create/drop database, so the copy says which. - Stop promising a super_user delegation for gate-inert grants, and exempt the structure_user DDL carve-out from the cannot-run-anything warning. - Use sr-only text for the unrestricted cell; aria-label is not exposed on a roleless span. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79d89b9 to
dc2c6a5
Compare
…_proto__ path Chris caught a regression I introduced last round: classifying a record-shaped operations value as a pre-5.0 database was version-blind, and on a 5.x instance role_validation rejects a non-array outright (OPERATIONS_MUST_BE_ARRAY). The editor therefore rendered "No operation-level restriction" over real table permissions, and one click on the switch would have overwritten them. - classifyOperationsValue/hasMalformedOperations now require the instance's allowlist support, so a record reads as a broken allowlist at or above the floor (left untouched) and as a database below it. - The roles table and the user-form summary read that support through a shared hook rather than assuming, so neither can go version-blind again. - The array-shaped structure_user copy no longer claims the DDL ops stay gated: that branch is terminal, so listing them cannot reach another database. The boolean form's create/drop database reach is stated in the summary too. - Restore the space the array branch lost, which rendered "drop_attributeon dev". - Add defaultCalculator tests: a database named __proto__ survives as an own, serializable key through the editor's round trip, plus the reserved-key behavior on both sides of the floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oles The summary appended the DDL carve-out after an absolute 'cannot run any operation.', so the two sentences contradicted each other. The note now qualifies the clause instead of following it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he version loads - getDatabasePermissionRecord now takes the same allowlistSupported flag as classifyOperationsValue, which delegates to it instead of repeating a shape test. The version alone decides whether the operations key is the allowlist or an ordinary database, so the two can no longer disagree: a record-shaped value is a table grant below the floor and a broken allowlist above it, and a record without a tables key is judged the same way as any other database. A test asserts the two agree across both worlds for four value shapes. - useOperationsAllowlistSupported returns boolean | undefined so "still loading" is no longer indistinguishable from "unsupported". The roles table and the user-form summary render nothing until it resolves rather than briefly calling a restricted role unrestricted; the permission hooks resolve it fail-closed, withholding a grant on a database named operations rather than inventing one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d8dd7ad to
61d068b
Compare
…pending verdict Harper's permissionsTranslator hands a role the translated table permissions under an `operations` key whenever a database of that name exists, so an upgraded v4 role still holds a real grant there. Threading the version into checkSchemaTablePermission made the browse UI stricter than the server for exactly that role, so those two call sites go back to reading the key by shape — which also drops the hook call that ignored usePermissions' entityId. The parameter is renamed to operationsIsAllowlist, since the right answer depends on the question being asked rather than on the version alone, and both readings are documented next to it. Adds tests for the upgraded-role grant, for a real allowlist never reading as one, and for the three verdicts the summary gives while the version is loading, below the floor, and above it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hook's tri-state The malformed branch told anyone holding an upgraded v4 role that granted a database named `operations` to fix the value in the role editor. Harper still honors those table grants (permissionsTranslator's schema loop), and replacing the record with an array makes perms.operations.tables[t] throw — failing permission translation for every request that user makes. So the advice destroyed a live grant and could lock the user out. - classifyOperationsValue gains `database-collision` for a record under the reserved key on a supporting instance. The summary describes it and points at renaming the database; the roles table marks it "database grant"; the editor refuses to touch it via isUneditableOperationsValue (renamed from hasMalformedOperations, which no longer covers every hands-off case). - Rewrite the module header to state the two questions the `operations` key answers and why the access checks deliberately answer differently, so the next reader doesn't re-unify them for a third time. - Add a test for useOperationsAllowlistSupported itself: pending resolves to undefined rather than false, then false below the floor and true above it. Collapsing the tri-state now fails a test instead of passing the suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tructive advice The previous commit corrected the wording in the user-assignment summary but left the same value hitting the old copy in the role editor — the surface an admin is most likely to act from. isUneditableOperationsValue collapsed `malformed` and `database-collision` into one flag, and both landed on "edit it in the JSON below". Following that replaces a live, server-honored grant with an array, which makes perms.operations.tables[t] throw and fails permission translation for every request that user makes. - Extract OperationsCollisionNotice and use it from both surfaces, so the two cannot drift again; the editor branches on the classification rather than a boolean that had stopped describing its contents. - Name the remedy Harper actually supports: there is no rename-database operation, and the role's permission key still reads `operations` afterwards, so the copy asks for the grants to be moved and re-keyed in the role JSON. - Fix the JSDoc pointer left dangling by the rename, and make the agreement test assert its own contract — it compared the record reading against `supported`, which made the above-floor half compare false to false. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chris traced this one further than either of us had: a role whose `operations` is not an array never reaches permissionsTranslator at all. listUsers calls cacheExpandedOperationsPerms for every user, its guard is truthiness-only, and expandOperationsPerms' for…of throws on a record — rejecting the user-cache load for the whole instance. Authentication then fails for every user, not just the holder of that role, on exactly the version range where the allowlist exists (the throw landed in the same v5.0.0-alpha.8). So "those table grants still apply" was the opposite of true, and presented an instance-wide outage as a benign coexistence problem. The notice now says what happens, names assignment as the trigger (only roles held by a user are expanded), and asks for the key to be removed rather than edited. Both non-array verdicts share it, since they fail identically, which puts isUneditableOperationsValue back in front of both surfaces instead of leaving it as test-only API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lid ones I collapsed both non-allowlist verdicts onto one notice claiming an instance-wide auth outage, having tested only the shapes that throw. Against harper's compiled expandOperationsPerms, only NON-ITERABLE values do: a record, `true`, a number. A bare string iterates per character and a mixed array iterates fine, so both expand without error and merely fail write-time validation — and a mixed array is reachable by ordinary editing, so the fatal wording was firing on a routine typo. - classifyOperationsValue now returns `breaks-auth` for the non-iterables and keeps `malformed` for the iterable-but-invalid ones; the notice carries the matching severity and remedy. - The remedy for the fatal case was also incomplete: moving the grants leaves the `operations` database in place, and while one exists any role carrying an allowlist array hits perms[s].tables[t] in permissionsTranslator and 500s. It now says to drop that database and states the instance-wide consequence. - The `assigning` surfaces (a role picker in the user forms) no longer point at "the JSON below", which does not exist there — they say to pick another role and repair this one in the role editor. - Drop the retracted "permissionsTranslator still grants those tables" narrative from the module docs; auth fails before any request reaches it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…or gate - The drop-the-database step was inside the "to keep the table grants" branch, but it is required on both: removing the key from one role leaves the database in place, and translateRolePermissions then throws for any role that carries an allowlist array — including the one the admin is about to create. Hoisted out of the conditional. - "Harper rejects it as an allowlist" was only true at write time. The gate enters on operations !== undefined and enforces whatever the value expands to, so a bare string denies every operation rather than being inert. The copy and the JSDoc now say what is enforced. - breaks-auth is floor-gated again, matching its own documentation: below the floor there is no expansion to throw, so a non-iterable value is merely invalid there rather than an instance-wide outage. - Add OperationsValueNotice.test.tsx. Every previous assertion reached the component through RoleOperationsSummary, which always passes `assigning`, so the editor wording — the half that carries the remedy — had no coverage; deleting the drop-the-database clause left the suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each of these cost a review round on #1628: the allowlist does not restrict super users (and cannot be saved alongside that flag), structure_user is gated except for DDL, ~23 operations plus every alias spelling are inert when granted, sql bypasses the gate, and a non-iterable value fails the user-cache load for the whole instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rning Two of these are fallout from last round's floor gate, which I bolted onto the return without re-reading what it now covered. - A well-formed allowlist BELOW the floor was being called malformed, so a pre-5.0 instance was told a role is locked down when the key is inert there — backwards, on the surface whose job is to warn. Sub-floor values now classify as `inert` (or `database` for a real record) and the surfaces stay quiet, since no gate, expansion or validation reads the key on those versions. - `breaks-auth` claimed the instance-wide outage for falsy values too, but cacheExpandedOperationsPerms returns early on `!operations`, so `false`, `null` and `0` never reach the expansion that rejects listUsers. They still enter the gate, which tests `!== undefined`, so they throw per request instead — bad, but scoped to the role. The verdict now requires truthiness and the malformed copy covers what those values actually do. - Restate the `malformed` JSDoc, which still described the retracted "expand without error, only fail write-time validation" behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From the pre-push Codex review of the current head:
- Saving a role whose `operations` value the editor refuses to touch still ran
the normalizer, which drops a non-array value — so an unrelated edit to
`{structure_user: true, operations: "read_only"}` silently removed a
restriction Harper was enforcing. Save is now disabled until the value is
repaired in the JSON.
- The user forms warned that assigning a role would break authentication
instance-wide and then let the admin do it. Both now refuse the submit.
- "cannot run any operation" contradicted our own sql caveat: SQL is authorized
against table permissions, so an empty allowlist does not stop it. Both
surfaces now say "Operations API call" and name the exception.
- The effective count folded a directly granted alias to its canonical name and
counted non-delegable entries, crediting the role with access the chips beside
it call inert. Group members still fold; direct entries no longer do.
- The drop-the-database remedy is shown only for a record-shaped value, not for
`true` or a number, where no such database is implied.
- Parse the draft with a quiet parser: safeParse console.errors, and a
half-typed document is invalid on most keystrokes.
- Resolve the instance version once for the roles table instead of once per row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adicting the chips - `databaseCollision` was added to the props type but never destructured, so the edit silently did nothing and the drop-the-database remedy stayed unconditional — shown even for `operations: true`, where no such database exists. The prop is read now, with a test for each branch. I did not assert the anchor on that replacement, which is why it passed review as done. - `delete_records_before` shares delete_files_before's handler, whose entry carries no api_name, so it is gate-inert like its twin. Added to the set. - `isInertGrant` is now the single definition of "listing this grants nothing", used by both the chip and the effective count. The count previously used two of the chip's three terms, so a role holding only gate-inert names was reported as allowing them while every chip called them inert. - That widened the zero-effect case, so the copy no longer says "Nothing selected" / "allowlist is empty" under visible chips: an all-inert list says so instead. Both surfaces branch on the list, not the expansion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hat survived mutation - `catchup` is not gate-inert and never was: its entry is keyed by the bare string 'catchup' (operation_authorization.ts:82,374), which is also its wire name, so the gate's `?? op` fallback matches and the grant works. My own harper#2175 write-up said exactly that and the catalog contradicted it — the set now documents the distinction (handler-name key vs bare-string key) so the next addition has to answer it. - Apply isInertGrant to expanded group members too. Latent today since no group lists an inert name, but it was the one seam where the "one predicate" claim didn't hold. delegatesSu's gate-inert term is now redundant and gone. - Add the guards three of these findings identified as missing: the new `delete_records_before` entry (deleting it previously left the suite green), and both "every entry is inert" branches, which survived mutation to their constants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kriszyp
left a comment
There was a problem hiding this comment.
Looks good, a few more things that could be cleaned up.
🤖 Reviewed with Codex
…overned sql
From Kris's review:
- The recovery order could have cost someone production data. It said to drop
the `operations` database and only then mentioned moving its grants, and it
spoke of "table grants" where the database holds records. The role repair now
stands alone and is stated as sufficient ("that alone restores
authentication"), and retiring the database is a clearly separate, optional
migration that leads with migrating and verifying the data and says plainly
that dropping it destroys what it holds.
- `sql` is authorized on its own path, so listing it neither grants nor
restricts anything — it is now marked out-of-gate and excluded from the
effective count, which no longer credits the allowlist for access it does not
control. read_only therefore reports 12.
- The malformed copy names each shape's outcome (a string denies everything, a
mixed array enforces what it expands to, a falsy value fails the requests)
instead of one lumped claim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| ) | ||
| : ( | ||
| <p className="text-xs text-muted-foreground" title={summarizeOperations(effective)}> | ||
| Effectively allows {pluralize(effective.length, 'operation', 'operations')} |
There was a problem hiding this comment.
Medium: dropping sql from the effective count leaves nothing on the non-empty path saying SQL is still reachable
The new outOfGate: true on the sql catalog entry feeds isInertGrant, so expandEffectiveOperations strips it. Rendered, operations: ['read_only'] now reads "restricted to 12 operations: describe_all, describe_metric, describe_schema, describe_table, get_analytics, ..." with sql nowhere in the list or the title. But serverHelpers/serverUtilities.ts:230-249 routes sql to verifyPermsAST, which contains zero references to operations/_expandedOperations -- so that role still runs arbitrary SQL SELECT/INSERT/UPDATE/DELETE against whatever its table CRUD permissions allow.
The empty branch immediately above carries (SQL is gated by table permissions, not this list). This branch does not, nor does RoleOperationsSummary.tsx:66 or tableDefinition.tsx:121, where a role with operations: ['sql'] now renders 0 allowed with an empty tooltip. The premise of the change is right (listing sql grants nothing), but the count is the only place a reader saw SQL at all, and removing it converts an over-report into an under-report of the role's real reach.
This is the unapplied half of the earlier finding on line 119, which suggested a matching caveat on read_only/standard_user since both include sql.
Suggested fix: qualify the non-empty branch unconditionally -- SQL is reachable on every restricted role, not only ones that list it -- e.g. Effectively allows {N} operations{delegatesSu ? ', ...' : ''}, plus SQL, which this list neither grants nor restricts. Mirror it in RoleOperationsSummary's summary sentence and in the roles-table title.
Generated by Barber AI
| : info?.nonDelegable | ||
| ? 'Always requires an actual super_user role; listing it cannot delegate the operation to a ' | ||
| + 'non-super_user role.' | ||
| : info?.outOfGate |
There was a problem hiding this comment.
Low: this outOfGate branch is a strict no-op, and the chip's inert styling is still unguarded
info?.outOfGate ? info.caveat : info?.caveat ? info.caveat : ... returns exactly what the next branch already returns, so it changes nothing today -- sql is the only outOfGate entry and it carries a caveat. For a future outOfGate entry without one it is actively wrong: it yields undefined and skips the info?.su / unknown-name fallbacks that would otherwise supply a title.
Separately, const inert = isInertGrant(name) on line 428 still survives mutation to const inert = false (71/71 tests pass), so nothing asserts the destructive chip variant -- including the new destructive styling sql picks up from this commit's isInertGrant change. Of the four behaviors that survived mutation last round, delete_records_before and both copy branches are now killed; this one is not.
Suggested fix: drop lines 439-440; info?.caveat already covers sql. If the branch is meant to stay, give it distinct copy rather than the same info.caveat, and add a chip test that pins the variant.
Generated by Barber AI
Closes #1627.
What this adds
Harper 5.0+ roles accept a
permission.operationsallowlist (deny-unlisted, with explicit listing acting as a scoped super_user delegation). Studio's role editor now surfaces it:EditRoleModal): a "Restrict operations" section above the JSON editor — predefined group toggles (read_only/admin_read/standard_user/agent), a categorized operation picker withsuper_userbadges on delegating grants, chips for the selected list, and an effective-operations summary (groups expanded). The section is a lens over the JSON text — the JSON stays the single source of truth, and structured writes floatoperationsto the top of the document (right after thesuper_user-family flags) so it's visible without scrolling.server.registerOperation): an exact-miss filter that looks like an operation name can be granted as a custom entry, case preserved.addedInfloors (5.1 deployments/agent ops, 5.2 secrets/env/backups) filter the picker; theagentgroup needs ≥ 5.2.0-alpha.1.The catalog (
operationsCatalog.ts) is static, extracted from Harper source at tags v5.0.0 / v5.1.0 / v5.2.2, because core has no discovery operation for grantable names (the issue's open question). Unknown names in existing role JSON are preserved and rendered, never dropped. Non-delegable ops (secrets store, managed backups,get_deployment_payload— handlers self-enforce super_user) are never offered.Where to look
src/integrations/api/localRolePermission.ts— reserved-key model. Key identity (not value shape) decides what's a database record vs a reserved key, which is what protects a database literally namedoperationsfrom clobbering (or being clobbered by) the allowlist indefaultCalculator/preparePermissionForSave.EditRoleModal.tsx— the JSON↔picker sync. Monaco applies programmaticvalueupdates without firingonChange(see AGENTS.md), which is what makes the lens loop-free. The save-path strip loop round-trips an array-shapedoperationsfor unconditionally-elevated roles rather than deleting what the author typed, while a non-array value is still stripped like any table permission.validateNoSUPermsrejects any multi-key permission that setssuper_user: trueorcluster_user: true, so an allowlist cannot coexist with those flags — it is dropped at save time and the conflict is stated in the editor, the roles table, and the user-form summary.structure_useris different: only DDL short-circuits the gate, so the allowlist is enforced there and the UI names the carve-out rather than calling the list inert.operationsvalue. A v4 role granting a database namedoperationssurvives the upgrade asoperations: {tables: …}, and from v5.0.0-alpha.8 that throws inexpandOperationsPermsduring the user-cache load — failing authentication for every user on the instance, triggered by assigning the role. The editor and the user-assignment form both say so and ask for the key to be removed. Filed against core as Non-array permission.operations throws during user-cache load, breaking authentication instance-wide (reachable via v4→v5 upgrade) harper#2194.api_name(role operations allowlist: grants are gate-inert for ops registered without api_name (deploy_component, get_status, …); sql bypasses the allowlist harper#2175) badge as "not yet enforced" instead of promising a delegation;sqlsays it is authorized against table permissions rather than this list. Both stay offered so roles are ready when core lands the fix — say the word if you'd rather withhold them.preparePermissionForSave.ts— extracted from the modal so the strip behavior is unit-tested.LocalRolePermissionindex signature widened to admitoperations; the three consumers that indexed it by database name now route throughgetDatabasePermissionRecord(with null/array guards that also fix a couple of latent crash shapes:{"data": {}},attribute_permissions: nullwith no legacy key).Deliberate tradeoffs / open items for review
useInstanceManagePermissionetc. still short-circuit onsuper_user), so a user holding a restricted-SU role sees admin UI that will 403 server-side. Deliberately out of scope here — it needs a UI-surface→operation mapping; flagged as a follow-up.OPERATION_PERMISSION_GROUPSas of 5.2.2, and legacy wire aliases (describe_database/describe_schemaetc.) count as distinct names, matching allowlist semantics rather than distinct handlers. If core grows alist_grantable_operationsop, the static catalog should defer to it.requiredPermissionswithout anapi_name(e.g.deployComponent,getStatus,getBackup), and the gate resolvesrequiredPermissions.get(op)?.api_name ?? opagainst the handler's camelCase name — which can never match a snake_case grant. Unless I've misread the dispatch, grantingdeploy_component/get_status(the issue's own example) may be gate-inert in core today. Now filed as role operations allowlist: grants are gate-inert for ops registered without api_name (deploy_component, get_status, …); sql bypasses the allowlist harper#2175 (which also coverssqlbypassing the allowlist gate entirely); the UI offers everythingvalidateOperationsaccepts, so it's correct either way once core is.Docs
Companion PR: HarperFast/documentation#631 — the reference page's two-gate description reads as universal, which is what led me to expect a "restricted super user". It now states the three limits (SU/CU rejected at write time, the structure_user DDL carve-out,
sqlauthorized on its own path). Studio's own docs are a single overview page with no per-feature UI reference, so nothing there needs updating.Review coverage
Authored by an LLM (Claude Fable 5). Cross-model coverage, all via the bundled pre-push CLI:
silently widened a
structure_userrole, user forms that warned about an instance-wide authoutage and then permitted it, an empty-allowlist claim that contradicted our own
sqlcaveat, aneffective count that credited grants the chips call inert, plus a per-keystroke
console.errorflood and a React Query observer per table row.
The three High ones each corrected a claim about Harper I had inferred rather than traced; the
most consequential established that a non-iterable
operationsvalue fails the user-cache loadand takes authentication down instance-wide (Non-array permission.operations throws during user-cache load, breaking authentication instance-wide (reachable via v4→v5 upgrade) harper#2194).
Cursor is unavailable on this machine.
🤖 Generated with Claude Code
Human-Review-Need: 4 @ bcfb362