Skip to content

feat(organizations): add admin role with owner-equivalent authority - #5007

Merged
jrf0110 merged 5 commits into
mainfrom
admin-capabilities
Aug 6, 2026
Merged

jrf0110 merged 5 commits into
mainfrom
admin-capabilities

Conversation

@jrf0110

@jrf0110 jrf0110 commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds an admin organization role that matches owner everywhere except owner management: an admin cannot grant the owner role, and cannot act on an existing owner's membership.

Context on the original ask

The request was "Admins can do everything owners can but can't delete the org." Two findings changed the shape of this:

  1. There was no admin org role. Roles were owner | member | billing_manager, so this is a new role rather than a permission tweak.
  2. No org role can delete an organization. Deletion is Kilo-staff-only (organizations.admin.delete, an adminProcedure). Owners are not getting self-serve deletion, so the "can't delete the org" carve-out is a no-op.

The meaningful boundary landed elsewhere: owner management.

Permission model

Capability owner admin billing_manager member
Org settings, groups, modes, SSO, members yes yes no no
Billing yes yes yes no
Invite member yes yes yes no
Invite/grant admin yes yes no no
Invite/grant owner yes no no no
Change or remove an existing owner yes no no no
Delete the organization no (Kilo staff only) no no no

Both owner-management restrictions are enforced together on purpose. Blocking only the grant would let an admin demote or remove every owner while being unable to appoint a replacement, leaving the org with no owner.

Changes

Role definition

  • admin added to the canonical union in packages/db/src/schema-types.ts and @kilocode/app-shared/organizations.
  • No migration: organization_memberships.role is plain text() with a TS-level $type and no DB check constraint.

Shared role sets (replacing literal arrays duplicated across ~25 files)

  • ORGANIZATION_MANAGE_ROLES = owner, admin
  • ORGANIZATION_BILLING_ROLES = owner, admin, billing_manager

Authorization

  • organizationOwnerProcedure / organizationOwnerMutationProcedure renamed to organizationAdminProcedure / organizationAdminMutationProcedure and widened to owner+admin. The rename forces every call site to be reviewed rather than silently changing semantics.
  • All 25 previously owner-gated org routes now accept admins: members, settings, groups, modes, SSO, usage recommendations.
  • assertOwnerAuthority reserves owner management for owners, applied to the role-update, member-removal, invite, and invite-revocation paths. Kilo staff bypass it because ensureOrganizationAccess resolves them to owner.
  • admin added to role priority and to parent-organization inheritance (kept as its own list so billing-scope changes can't silently widen child-org inheritance).

Frontend

  • Owner-only UI gates widened via canManageOrganization so admins aren't shown a read-only UI for routes the API now permits.
  • Role dropdown and invite dialog mirror the server rules; an owner's membership renders read-only for admins.
  • admin surfaced in role labels and the role-testing switcher.
  • Fixed getRoleLabel also rendering billing_manager as "Member".

Testing

  • apps/web typecheck: clean (tsgo --noEmit, exit 0)
  • oxlint over apps/web/src, packages/app-shared/src, packages/db/src: 0 warnings, 0 errors
  • Jest, 50 suites over routers/organizations, lib/organizations, components/organizations: 932 passed, 1 failed
    • The single failure is organization-kiloclaw-router.test.ts "clears organization subscription destruction lifecycle" — a pre-existing timezone assertion (2026-04-12 vs 2026-04-11 19:00:00-05), confirmed failing on a clean stashed tree.
  • packages/app-shared vitest role tests: 5 passed
  • New coverage in the members router: admin can update a member role, grant admin, invite an admin, and remove a non-owner; admin is rejected when granting owner, inviting an owner, changing an existing owner's role, or removing an owner. Existing assertions that billing managers and members are rejected still pass.

Notes for review

  • Admins are now included in the billing-capable recipient query for Kilo Pass notifications, so they will receive those emails. Say the word if billing authority should be decoupled from the notifications.
  • Rebased onto main after chore(cost-insights): remove frontend pages, components, and tRPC routers #4993/chore(cost-insights): stop all writes to the cost_insight_* tables #4996 removed the cost-insights frontend and repository. Both rebase conflicts were modify/delete on files those PRs deleted; the deletions were accepted, so this branch no longer touches cost-insights.
  • Revoking a pending owner invitation is treated as owner management and blocked for admins, since it is a pending grant of owner authority.
  • Scope is web + shared packages. apps/mobile still has local role string-unions without admin; mobile does not yet render or assign the role.

@kilo-code-bot

kilo-code-bot Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental pass over d00404e2..3ec61121: the branch was rebased onto current main (2385fd18) with zero PR-authored content changes — all 60 PR files are byte-identical to the previously approved state except packages/db/src/schema-types.ts, whose delta is entirely main-sourced (cost-insights enum removal, custom-LLM Google service account, code-review abandoned reason) and leaves this PR's OrganizationRole union change intact; all prior findings remain resolved at HEAD.

Files Reviewed (3 files this pass)
  • packages/db/src/schema-types.ts - only file with drift since d00404e2; verified the drift is rebase-carried main changes and the PR's own one-line role-union change (owner | admin | member | billing_manager) is unchanged
  • apps/web/src/components/organizations/OrganizationMembersCard.tsx - re-verified at HEAD: canRemoveMember receives member.role at both call sites (lines 143, 596), and canActOnMemberRole gates the row's canEditRole, invitation revoke, and canDelete paths (lines 94-98, 184, 585, 598)
  • packages/app-shared/src/organizations/roles.ts - unchanged since prior approval; canManageOrganizationOwners still imported and used by the member card
Previous Review Summaries (2 snapshots, latest commit d00404e)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit d00404e)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental pass over ff40951b..d00404e2 (fix commits c563d9ea and d00404e2c): the previously flagged TS2554 compile break is fixed — all canRemoveMember call sites now pass member.role — and the new canActOnMemberRole gate faithfully mirrors the server-side assertOwnerAuthority boundary for role edits, member removals, and pending-owner-invitation revocations; all prior findings are resolved.

Files Reviewed (4 files this pass)
  • apps/web/src/components/organizations/OrganizationMembersCard.tsx - previous CRITICAL resolved: DeleteMemberButton (line 143) and the members-list canDelete (line 596) both pass member.role; new owner-target gates on canEditRole, invitation deletion, and row canDelete match the server's FORBIDDEN paths; usage-limit comment verified accurate (limit updates are not owner-gated server-side)
  • packages/app-shared/src/organizations/roles.ts - previous SUGGESTION resolved: docstring now documents the owner-management carve-out; new canManageOrganizationOwners matches assertOwnerAuthority
  • packages/app-shared/src/organizations/roles.test.ts - new coverage asserts canManageOrganizationOwners is true only for owner
  • packages/app-shared/src/security-agent/settings.test.ts - previous SUGGESTION resolved: test name now includes admin

Previous review (commit ff40951)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The latest fix commit (c563d9ea) leaves OrganizationMembersCard.tsx failing to compile: canRemoveMember gained a required fourth parameter but the members-list canDelete call site at line 592 still passes three arguments (TS2554).

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/web/src/components/organizations/OrganizationMembersCard.tsx 109 Adding required targetRole parameter broke the missed canRemoveMember call site at line 592 — file no longer type-checks (TS2554: Expected 4 arguments, but got 3); pass member.role there as DeleteMemberButton does. (Inline comment posted.)
Files Reviewed (4 files this pass)

Incremental pass over ff40951b..c563d9ea (single commit c563d9ea fix(web): stop offering owner-only member actions to admins):

  • apps/web/src/components/organizations/OrganizationMembersCard.tsx - 1 issue
  • packages/app-shared/src/organizations/roles.ts - no issues; updated docstring and new canManageOrganizationOwners match the server-side assertOwnerAuthority boundary
  • packages/app-shared/src/organizations/roles.test.ts - no issues; new coverage asserts canManageOrganizationOwners is true only for owner
  • packages/app-shared/src/security-agent/settings.test.ts - no issues; stale test name corrected

Fix these issues in Kilo Cloud


Reviewed by kimi-k3 · Input: 59.6K · Output: 11.1K · Cached: 792.6K

Review guidance: REVIEW.md from base branch main

Comment thread apps/web/src/components/organizations/OrganizationMembersCard.tsx
Comment thread packages/app-shared/src/organizations/roles.ts
@jrf0110
jrf0110 force-pushed the admin-capabilities branch from a04704b to 2cd2eaa Compare August 4, 2026 19:31
Comment thread packages/app-shared/src/security-agent/settings.test.ts Outdated
Comment thread apps/web/src/components/organizations/OrganizationMembersCard.tsx
jrf0110 added 5 commits August 6, 2026 08:54
Introduces an `admin` organization role that holds the same authority as
`owner`. There is no action an owner can take that an admin cannot.

- Add `admin` to the canonical role union (`packages/db` schema-types and
  `@kilocode/app-shared/organizations`). The `role` column is plain `text`
  with a TS-level `$type`, so no migration is required.
- Add shared `ORGANIZATION_MANAGE_ROLES` (owner, admin) and
  `ORGANIZATION_BILLING_ROLES` (owner, admin, billing_manager) constants and
  replace the literal role arrays that previously duplicated them.
- Rename `organizationOwnerProcedure`/`organizationOwnerMutationProcedure` to
  `organizationAdminProcedure`/`organizationAdminMutationProcedure` and widen
  them to owner+admin, so every previously owner-gated org route (members,
  settings, groups, modes, SSO, usage recommendations) now accepts admins.
- Include `admin` in role priority and in parent-organization inheritance.
- Widen the matching frontend gates via `canManageOrganization` so admins are
  not shown a read-only UI for routes the API now permits, and surface the
  role in the label maps, invite dialog, and role dropdown.

Adds admin-parity coverage to the members router tests and points the role
array assertions at the shared constant so they cannot drift.
Admins match owners everywhere except owner management: an admin may not
grant the owner role, nor act on an existing owner's membership.

Both halves are enforced together on purpose. Blocking only the grant would
let an admin demote or remove every owner while being unable to appoint a
replacement, leaving the organization with no owner.

Enforced in `assertOwnerAuthority` and applied to the role update, member
removal, invite, and invite-revocation paths. Kilo staff are unaffected
because `ensureOrganizationAccess` resolves them to `owner`.

The role dropdown and invite dialog mirror the server rules: admins can
assign admin, member, and billing_manager, and an owner's membership renders
read-only for them.
`apps/mobile` shares `OrganizationRole` with `packages/db`, so adding `admin`
broke its typecheck in two places.

- `canManageSecurityAgent` took a hand-written narrow role union. It now takes
  `OrganizationRole` and delegates to `canManageOrganizationBilling`, which
  already encodes the same owner/billing_manager rule and includes admins.
- Mobile's `ROLE_LABEL` is a `Record<OrgRole, string>`, so it needs an `admin`
  entry; the role is also added to the assignable options an owner sees.
Addresses PR review on the member card and stale role docs.

`canManageMembers` was widened to admins, which also widened the remove and
revoke-invitation gates. The server rejects both for admins when the target is
an owner, so an admin saw enabled controls that always failed with FORBIDDEN.

- Add `canManageOrganizationOwners` to the shared role module as the single
  statement of the owner-management rule, mirroring `assertOwnerAuthority`.
- Gate `DeleteMemberButton` and `DeleteInvitationButton` on the target's role
  so admins are not offered actions against an owner or a pending owner invite.
- Refresh the member-removal business-rule comment, which predated the role.
- Correct the `ORGANIZATION_MANAGE_ROLES` docstring, which still claimed admin
  was an exact peer of owner, and a stale security-agent test name.
Completes the previous commit. `canRemoveMember` gained a target-role
parameter but the member row's own `canDelete` still called it with three
arguments, which broke the build, and two sibling gates on that row had the
same owner-management gap.

- Pass the target role at the row call site.
- Gate the row's `canEditRole` and the invited-member `canDelete` on
  `canActOnMemberRole`, so admins are not shown edit mode or a revoke control
  for an owner or a pending owner invitation.
- Leave `canEditLimit` ungated and say why: the server scopes
  `assertOwnerAuthority` to role changes, so an admin may still set an
  owner's daily usage limit.
@jrf0110
jrf0110 force-pushed the admin-capabilities branch from d00404e to 3ec6112 Compare August 6, 2026 13:54
@jrf0110
jrf0110 merged commit 57f4327 into main Aug 6, 2026
92 of 93 checks passed
@jrf0110
jrf0110 deleted the admin-capabilities branch August 6, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants