Files
coder/site/permissions.json
Michael Suchacz 6498c5776d feat(site): add organization skill settings with a shared skills UI (#30612)
## Stack

Part of
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. #30609 data model: one migration, queries, RBAC and dbauthz
2. #30610 organization skill API routes and the scope-neutral SDK
3. #30611 sharing ACL routes
4. **#30612** settings UI: shared skills table, admin page, member
section (this PR)
5. #30613 chats: chatd loading, `/` menu, docs

## What changes

This PR turns the personal skills page into a skills table that works
for any owner, then uses it for an organization admin page and a
read-only member section. It changes only `site/`.

**Shared skills table.** The personal page's files move to owner-neutral
paths (kept as git renames, so the diff shows edits):
`AgentSettingsPersonalSkillsPageView.tsx` becomes
`components/SkillsTableView.tsx`, `AgentSettingsPersonalSkillsPage.tsx`
becomes `components/SkillsTable.tsx`, `PersonalSkillEditor` becomes
`SkillEditor`, and `utils/personalSkills` becomes `utils/skills`.
`queries/userSkills.ts` keeps main's query names and keys for personal
skills, and the new `queries/organizationSkills.ts` holds the
organization and ACL queries. The table takes a `SkillsScope` (list,
detail, create, update and delete queries), built by `userSkillsScope`
or `organizationSkillsScope` in `utils/skills.ts`, plus an `access`
object (`create`, `update`, `delete`) and its copy as props.

- **Enabled toggle:** `SkillEnabledSwitch` runs one update mutation per
row from the table's scope, is disabled while it is pending, and
invalidates the list when it settles, which also drops a skill deleted
elsewhere.
- **Read-only status:** a table without update access (the member
section, and auditors on the admin page) shows a green check ("Enabled")
or a gray cross ("Disabled") in the Enabled column instead of a disabled
switch. Editable tables keep the switch.
- **Read-only view:** when the caller can't update a skill, the row
opens `SkillEditor` in read-only mode with a Close button, replacing the
old View dialog. It omits the naming hint and starts focus on Close.
- Dialogs return focus to the control that opened them
(`useDialogFocusReturn`).
- Create, update and delete invalidate the skill list instead of
patching it by hand. Update also writes the PATCH response to the
skill's detail, so a reopened editor shows the new content.
- Saving an ACL refreshes the organization skill list and the skill's
detail. Deleting a skill removes its detail and the ACL queries cached
under it, so a skill recreated with the same name doesn't inherit them.

**Admin page** at `/ai/settings/skills?org=<name>`
(`AISettingsPage/SkillsPage`): lists, creates, edits, toggles and
deletes organization skills with the caller's per-organization
permissions, and opens `OrganizationSkillSharingDialog` for users with
share. A requested organization the caller can't read shows the
permission error instead of falling back to another organization, so a
skill is never added to an organization the URL didn't name. The new
`viewAnyOrganizationSkills` permission adds Skills to the AI settings
sidebar, shows the navbar's AI settings link, and lets the AI settings
index redirect to the skills page. The sidebar's Skills link keeps the
current `?org=` only when the user can read that organization's skills,
using the page's own filter (`readableSkillsOrganizations`), so
following the link never lands on the permission error.

**Member section.** The agent settings page becomes
`/agents/settings/skills` ("Skills" in the sidebar; the old
`/agents/settings/personal-skills` URL redirects). Below personal
skills, "From your organizations" shows one organization at a time,
read-only. Its picker lists the organizations the user is a member of
that have enabled skills for them (an organization still loading or
whose list failed stays listed), starts on the default organization, and
is hidden when only one organization qualifies. Export all downloads
only the selected organization's skills. If the selected organization's
list fails, a retry button (labeled "Retry loading _name_ skills")
replaces the table.

**Shared sharing helpers.** `ChatModelPrincipalAutocomplete` becomes
`components/ACLPrincipalAutocomplete`, used by the model, MCP server and
skill sharing dialogs; `MCPServerPrincipalAutocomplete` is deleted and
the common ACL diff logic moves to `aclSharing.ts`. A new
`ChatModelSharingDialog.test.tsx` guards the model dialog through the
swap.

Declined on purpose (Mike's call): extra toggle cache hardening, namely
a pending-toggle map, reconciling a View that 404s, and guarding a stale
refetch against a retried toggle. Invalidating on settle already
refetches the list after every toggle.

## Commits

This PR reworks the old site layers rather than reproducing them. The
commits go in review order: shared helpers, renames, the shared table,
the two pages, then review fixes.

1. `6463992b717` refactor: share the ACL principal autocomplete and
sharing helpers.
2. `e18599f5049` refactor: move the personal skills files to shared
skills paths (file moves plus import path updates only).
3. `3c71e6c2168` refactor: drop the personal prefix from shared skill
symbols.
4. `3be35bcde68` refactor: key the skills table and queries by owner.
5. `d0c1dfcfdd1` fix: return focus to the opener when a skills dialog
closes.
6. `92ef6351eb4` feat: add an Enabled toggle to the skills table.
7. `31c103e5396` refactor: move the skill edit and delete dialogs to
their own file.
8. `cff0bef4576` feat: view a skill read-only when the user cannot edit
it.
9. `a8b54a22981` feat: add the organization skills admin page with
sharing.
10. `391401b62b7` feat: show organization skills on the personal skills
page.
11. `2cf6e250a60` refactor: keep the skill dialogs in the table view and
extract focus return. This undoes the file split from commit 7 so
`SkillsTableView.tsx` still diffs as a rename (497 lines then, 507 at
the head).
12. `0af93452c7d` test: keep only the role wiring test for the skills
page.
13. `8032c6708a8` fix: refetch the skill list after a create when no
list is cached (superseded by item 22).
14. `a52f9ba659c` refactor: tidy skills copy, editor prop naming, and
story fixtures.
15. `f10bc316305` fix: refresh the organization skill list after saving
its ACL. Saving an ACL bumps the skill's `updated_at`, so the save now
also invalidates the organization skill list and the skill's detail; a
new test in `api/queries/skills.test.ts` (now
`organizationSkills.test.ts`) covers it.
16. `698a6d0b55f` Merge the updated sharing layer into settings-ui,
bringing in #30610's PATCH fix.
17. `aaa356f0fd5` fix: drop the editing hint and field focus from the
read-only skill view. From the remote UAT: read-only View no longer
shows the Name field's naming hint, and the dialog opens with focus on
Close instead of a field (`onOpenAutoFocus`, like `ChatSearchDialog`).
The new `SkillEditor.test.tsx` case "starts focus on Close when
read-only" fails without the fix.
18. `698dd83946d` fix: drop a deleted skill's ACL caches with its
detail. The delete now removes every query under the skill's key, not
only the exact detail; a new case in `api/queries/skills.test.ts` (now
`organizationSkills.test.ts`) covers it.
19. `f82573cd1c2` fix: carry only a skill-readable organization on the
Skills link, with a table-driven `AISettingsSidebar.test.tsx` case.
20. `0e55137de7b` fix: share the skill-readable organization filter
(`SkillsPage/readableSkillsOrganizations.ts`, used by the sidebar and
`SkillsPage`) and cover an organization that is listed but unreadable.
21. `97d42fd3cd8` refactor: split skill queries by owner.
`queries/skills.ts` goes back to main's `userSkills.ts` (main's names
and keys) plus a new `organizationSkills.ts` for the organization and
ACL queries; the table takes a `SkillsScope` instead of an owner union.
22. `4ddfb80ac54` refactor: invalidate skill lists after writes, instead
of patching the cached list; update still writes the detail from the
PATCH response.
23. `14d5b755087` refactor: pass skill organizations to the sidebar as
`Organization` objects instead of names.
24. `463df6a5e69` feat: pick one organization on the member skills page.
25. `9c802557b9c` refactor: tighten the skill query comments and tests.
26. `9002dc60382` test: cover the failed sharing save in Vitest
(`OrganizationSkillSharingDialog.test.tsx`).
27. `a9df756b976` and `5ebf7b8984a` merge the updated sharing layer,
which brings in main. `a9df756b976` moves the `toast` imports in
`OrganizationSkillSharingDialog.tsx` and `SkillEnabledSwitch.tsx` to
`#/components/Toaster/toast`, as main's Biome import rule now requires,
and resolves conflicts with main in the model and MCP server sharing
dialogs.
28. `ac3e62c83b7` test: assert the member page's organization switch
through requests. After a switch, the test checks that the list and the
viewed skill are requested from the selected organization (a Codex
review finding).
29. `63bb42754e3` fix: show read-only skill status instead of a disabled
switch (from jaaydenh's review). `SkillEnabledSwitch` loses its
`disabled` prop; a new `ReadOnly` story in `SkillsTableView.stories.tsx`
shows the icons, and the read-only case in `SkillsTable.test.tsx`
asserted the status icon and no switch (removed again in item 30).
30. `82b2487ad17` test: address Codex review on skills tests. The
`SkillsPage.test.tsx` update-only case now edits the description, saves,
and asserts the `updateOrganizationSkill` call. The read-only status
case in `SkillsTable.test.tsx` is removed because the `ReadOnly` story
covers that visual state, and `renderTable` loses its `access` argument.
`AISettingsSidebar.test.tsx`'s hoisted permissions mock drops its cast
for an explicit return type.
31. `658be8b8df5` merges the updated sharing layer (main at
`e7b76872098`); no changes of its own.

## Size

Production +1465/-617, tests and stories +1104/-141 (tests +690 net,
stories +273 net), generated +4/-0; total +2573/-758. The site changes
in this PR and #30613 net +2263 lines, against +3058 for the same scope
in the old stack. This is above the 500-line reviewer-friendly limit on
purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this
stack replaces.

## Replaces

#30590 (site rename), #30595 (site API), #30596 (skills table), #30597
(Enabled toggle), #30598 (organization page), #30599 (ACL autocomplete),
#30600 (permissions), #30603 (member page).

## Validation

- `pnpm run lint` clean (tsc, oxlint, knip, React Compiler) and Biome
clean; `pnpm run lint` reran clean on `13f03d095ff`, which contains this
PR up to `0e55137de7b`.
- Vitest: 9 files, 49 tests. Storybook Vitest: 7 files, 101 tests. These
full runs were on `a52f9ba659c`; on `0e55137de7b`, the three test files
the later fixes change (`api/queries/skills.test.ts`,
`AISettingsSidebar.test.tsx`, `SkillEditor.test.tsx`) and the two
`SkillsPage` tests that cover the changed `SkillsPage.tsx` pass, 20
tests.
- On `a6f8f63ad13`, which contains this PR up to `5ebf7b8984a` and both
main merges: 38 Vitest files (the skill queries, the sidebar, the skill
pages and components, the slash menu, and `pages/AgentsPage/utils`)
pass, 542 tests with 2 skipped.
- On `63bb42754e3`: `pnpm check` and the full `pnpm run lint` pass;
Vitest (`SkillsTable`, `SkillsPage`, `AgentSettingsSkillsPage`: 6 tests)
and Storybook Vitest (the `SkillsTableView`, `AgentSettingsSkillsPage`
and `SkillsPage` stories: 34 tests) pass; `coder-frontend-review`
passed.
- On `82b2487ad17`: `pnpm check`, the full `pnpm run lint`, and Vitest
on the three changed test files (7 tests) pass.
- On `658be8b8df5`: `go build`, `pnpm check` and the full `pnpm lint`
pass; 6 Vitest files with 39 tests pass.
- `frontend-review` (FE1 to FE10) and `coder-frontend-review` (8
reviewer roles); findings fixed or declined as noted above.
- Remote UAT round 7 through Coder Agents passed on the whole stack,
last on `13f03d095ff`, which contains this PR up to `0e55137de7b`; see
#30613's Validation for scope and findings.
- Remote UAT round 8 passed on `b03c14c86de` (one item graded on intent;
see #30613), which contains this PR up to `9c802557b9c`: the member page
organization picker, list refresh after writes on the personal and admin
tables, no stale ACL after deleting and recreating a skill, and the
sidebar Skills `?org=`. See #30613 for details. `9002dc60382`,
`ac3e62c83b7`, `63bb42754e3`, `82b2487ad17` and the main merges have
tests but no UAT run.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 16:57:41 +00:00

187 lines
4.8 KiB
JSON

{
"viewAllUsers": {
"object": { "resource_type": "user" },
"action": "read"
},
"updateUsers": {
"object": { "resource_type": "user" },
"action": "update"
},
"createUser": {
"object": { "resource_type": "user" },
"action": "create"
},
"createTemplates": {
"object": { "resource_type": "template", "any_org": true },
"action": "create"
},
"createWorkspace": {
"object": {
"resource_type": "workspace",
"any_org": true,
"owner_id": "me"
},
"action": "create"
},
"updateTemplates": {
"object": { "resource_type": "template" },
"action": "update"
},
"updateAnyTemplate": {
"object": { "resource_type": "template", "any_org": true },
"action": "update"
},
"deleteTemplates": {
"object": { "resource_type": "template" },
"action": "delete"
},
"viewDeploymentConfig": {
"object": { "resource_type": "deployment_config" },
"action": "read"
},
"editDeploymentConfig": {
"object": { "resource_type": "deployment_config" },
"action": "update"
},
"viewDeploymentStats": {
"object": { "resource_type": "deployment_stats" },
"action": "read"
},
"readWorkspaceProxies": {
"object": { "resource_type": "workspace_proxy" },
"action": "read"
},
"editWorkspaceProxies": {
"object": { "resource_type": "workspace_proxy" },
"action": "create"
},
"createOrganization": {
"object": { "resource_type": "organization" },
"action": "create"
},
"viewAnyGroup": {
"object": { "resource_type": "group" },
"action": "read"
},
"createGroup": {
"object": { "resource_type": "group" },
"action": "create"
},
"viewAllLicenses": {
"object": { "resource_type": "license" },
"action": "read"
},
"viewNotificationTemplate": {
"object": { "resource_type": "notification_template" },
"action": "read"
},
"viewOrganizationIDPSyncSettings": {
"object": { "resource_type": "idpsync_settings" },
"action": "read"
},
"viewAnyMembers": {
"object": { "resource_type": "organization_member", "any_org": true },
"action": "read"
},
"editAnyGroups": {
"object": { "resource_type": "group", "any_org": true },
"action": "update"
},
"assignAnyRoles": {
"object": { "resource_type": "assign_org_role", "any_org": true },
"action": "assign"
},
"viewAnyIdpSyncSettings": {
"object": { "resource_type": "idpsync_settings", "any_org": true },
"action": "read"
},
"viewAnyMCPServerConfigs": {
"object": { "resource_type": "mcp_server_config", "any_org": true },
"action": "read"
},
"viewAnyOrganizationSkills": {
"object": { "resource_type": "organization_skill", "any_org": true },
"action": "read"
},
"createAnyMCPServerConfig": {
"object": { "resource_type": "mcp_server_config", "any_org": true },
"action": "create"
},
"updateAnyMCPServerConfig": {
"object": { "resource_type": "mcp_server_config", "any_org": true },
"action": "update"
},
"deleteAnyMCPServerConfig": {
"object": { "resource_type": "mcp_server_config", "any_org": true },
"action": "delete"
},
"editAnySettings": {
"object": { "resource_type": "organization", "any_org": true },
"action": "update"
},
"viewAnyAuditLog": {
"object": { "resource_type": "audit_log", "any_org": true },
"action": "read"
},
"viewAnyConnectionLog": {
"object": { "resource_type": "connection_log", "any_org": true },
"action": "read"
},
"viewDebugInfo": {
"object": { "resource_type": "debug_info" },
"action": "read"
},
"viewAnyAIBridgeInterception": {
"object": { "resource_type": "aibridge_interception", "any_org": true },
"action": "read"
},
"viewAnyAIProvider": {
"object": { "resource_type": "ai_provider" },
"action": "read"
},
"viewAIGatewayKeys": {
"object": { "resource_type": "ai_gateway_key" },
"action": "read"
},
"createOAuth2App": {
"object": { "resource_type": "oauth2_app" },
"action": "create"
},
"editOAuth2App": {
"object": { "resource_type": "oauth2_app" },
"action": "update"
},
"deleteOAuth2App": {
"object": { "resource_type": "oauth2_app" },
"action": "delete"
},
"viewOAuth2AppSecrets": {
"object": { "resource_type": "oauth2_app_secret" },
"action": "read"
},
"createChat": {
"object": { "resource_type": "chat", "any_org": true, "owner_id": "me" },
"action": "create"
},
"viewAnyChatModelConfig": {
"object": { "resource_type": "chat_model_config", "any_org": true },
"action": "read"
},
"createAnyChatModelConfig": {
"object": { "resource_type": "chat_model_config", "any_org": true },
"action": "create"
},
"editAnyChatModelConfig": {
"object": { "resource_type": "chat_model_config", "any_org": true },
"action": "update"
},
"deleteAnyChatModelConfig": {
"object": { "resource_type": "chat_model_config", "any_org": true },
"action": "delete"
},
"shareAnyChatModelConfig": {
"object": { "resource_type": "chat_model_config", "any_org": true },
"action": "share"
}
}