feat: add personal skill storage, API, and SDK (#25363)

> Mux updated this PR on behalf of Mike.

## Stack Context

This PR is the storage, permissions, API, and SDK layer for experimental
personal skills. #25362 has landed on `main`, so this branch is
restacked directly on `main`.

Stack order:
1. #25363 storage, permissions, API, and SDK
2. #25365 API test coverage
3. #25366 chattool and chatd integration
4. #25066 settings UI and docs
5. #25386 personal skills slash menu

## What?

Adds the `user_skills` database table, generated queries, RBAC resources
and scopes, audit resource handling, experimental user-scoped CRUD
endpoints, SDK types, and generated API/site types.

Follow-up review and restack fixes:
- Enforce a bounded personal skill description in parser and database
constraints.
- Return `403 Forbidden` for unauthorized create and update attempts.
- Return explicit conflict responses when soft-deleted users are
targeted.
- Keep user admins out of personal skills, while site owners can read
and delete but not create or update.
- Document trigger-raised constraint names and keep schema constants
covered by tests.
- Reuse `UserSkillMetadata` in the full `UserSkill` SDK response type.
- Generate user skill IDs in Go instead of relying on a database
default.
- Rebase on latest `main` and renumber the user skills migration to
`000502_user_skills`.

## Why?

Personal skills need durable user-owned storage with owner
authorization, limited site-owner moderation, and a hidden API surface
before chatd can consume them.

## Validation

- `make gen`
- `go test ./coderd/database -run '^TestUserSkillSchemaConstants$'
-count=1`
- `go test ./coderd/database/dbauthz -run
'^TestMethodTestSuite/TestUserSkills$' -count=1`
- `go test ./coderd -run '^TestPatchUserSkill$' -count=1`
- `go test ./codersdk ./coderd/database/db2sdk`
- `make lint`
- pre-commit hook on `97fd58108d`
This commit is contained in:
Michael Suchacz
2026-05-20 00:09:09 +02:00
committed by GitHub
parent 3c9c8c708d
commit 5a8d0016a5
46 changed files with 2315 additions and 70 deletions
+304 -2
View File
@@ -877,6 +877,227 @@ const docTemplate = `{
]
}
},
"/api/experimental/users/{user}/skills": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "List user skills",
"operationId": "list-user-skills",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.UserSkillMetadata"
}
}
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Create a user skill",
"operationId": "create-a-user-skill",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
},
{
"description": "Create user skill request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.CreateUserSkillRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/codersdk.UserSkill"
}
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
}
},
"/api/experimental/users/{user}/skills/{skillName}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get a user skill by name",
"operationId": "get-a-user-skill-by-name",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Skill name",
"name": "skillName",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.UserSkill"
}
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
},
"delete": {
"tags": [
"Users"
],
"summary": "Delete a user skill",
"operationId": "delete-a-user-skill",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Skill name",
"name": "skillName",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Update a user skill",
"operationId": "update-a-user-skill",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Skill name",
"name": "skillName",
"in": "path",
"required": true
},
{
"description": "Update user skill request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.UpdateUserSkillRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.UserSkill"
}
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
}
},
"/api/experimental/watch-all-workspacebuilds": {
"get": {
"produces": [
@@ -14802,6 +15023,11 @@ const docTemplate = `{
"user_secret:delete",
"user_secret:read",
"user_secret:update",
"user_skill:*",
"user_skill:create",
"user_skill:delete",
"user_skill:read",
"user_skill:update",
"webpush_subscription:*",
"webpush_subscription:create",
"webpush_subscription:delete",
@@ -15023,6 +15249,11 @@ const docTemplate = `{
"APIKeyScopeUserSecretDelete",
"APIKeyScopeUserSecretRead",
"APIKeyScopeUserSecretUpdate",
"APIKeyScopeUserSkillAll",
"APIKeyScopeUserSkillCreate",
"APIKeyScopeUserSkillDelete",
"APIKeyScopeUserSkillRead",
"APIKeyScopeUserSkillUpdate",
"APIKeyScopeWebpushSubscriptionAll",
"APIKeyScopeWebpushSubscriptionCreate",
"APIKeyScopeWebpushSubscriptionDelete",
@@ -17400,6 +17631,15 @@ const docTemplate = `{
}
}
},
"codersdk.CreateUserSkillRequest": {
"type": "object",
"properties": {
"content": {
"description": "Content must be SKILL.md-format Markdown with YAML frontmatter. The\nfrontmatter must include name, may include description, and must be\nfollowed by a non-empty body.",
"type": "string"
}
}
},
"codersdk.CreateWorkspaceBuildReason": {
"type": "string",
"enum": [
@@ -21537,6 +21777,7 @@ const docTemplate = `{
"usage_event",
"user",
"user_secret",
"user_skill",
"webpush_subscription",
"workspace",
"workspace_agent_devcontainers",
@@ -21586,6 +21827,7 @@ const docTemplate = `{
"ResourceUsageEvent",
"ResourceUser",
"ResourceUserSecret",
"ResourceUserSkill",
"ResourceWebpushSubscription",
"ResourceWorkspace",
"ResourceWorkspaceAgentDevcontainers",
@@ -21811,7 +22053,8 @@ const docTemplate = `{
"ai_provider_key",
"group_ai_budget",
"chat",
"user_secret"
"user_secret",
"user_skill"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -21845,7 +22088,8 @@ const docTemplate = `{
"ResourceTypeAIProviderKey",
"ResourceTypeGroupAIBudget",
"ResourceTypeChat",
"ResourceTypeUserSecret"
"ResourceTypeUserSecret",
"ResourceTypeUserSkill"
]
},
"codersdk.Response": {
@@ -23781,6 +24025,15 @@ const docTemplate = `{
}
}
},
"codersdk.UpdateUserSkillRequest": {
"type": "object",
"properties": {
"content": {
"description": "Content must be SKILL.md-format Markdown with YAML frontmatter. The\nfrontmatter must include name, may include description, and must be\nfollowed by a non-empty body.",
"type": "string"
}
}
},
"codersdk.UpdateWorkspaceACL": {
"type": "object",
"properties": {
@@ -24307,6 +24560,55 @@ const docTemplate = `{
}
}
},
"codersdk.UserSkill": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"
}
}
},
"codersdk.UserSkillMetadata": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"
}
}
},
"codersdk.UserStatus": {
"type": "string",
"enum": [
+282 -2
View File
@@ -774,6 +774,205 @@
]
}
},
"/api/experimental/users/{user}/skills": {
"get": {
"produces": ["application/json"],
"tags": ["Users"],
"summary": "List user skills",
"operationId": "list-user-skills",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.UserSkillMetadata"
}
}
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
},
"post": {
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Users"],
"summary": "Create a user skill",
"operationId": "create-a-user-skill",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
},
{
"description": "Create user skill request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.CreateUserSkillRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/codersdk.UserSkill"
}
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
}
},
"/api/experimental/users/{user}/skills/{skillName}": {
"get": {
"produces": ["application/json"],
"tags": ["Users"],
"summary": "Get a user skill by name",
"operationId": "get-a-user-skill-by-name",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Skill name",
"name": "skillName",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.UserSkill"
}
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
},
"delete": {
"tags": ["Users"],
"summary": "Delete a user skill",
"operationId": "delete-a-user-skill",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Skill name",
"name": "skillName",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
},
"patch": {
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Users"],
"summary": "Update a user skill",
"operationId": "update-a-user-skill",
"parameters": [
{
"type": "string",
"description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Skill name",
"name": "skillName",
"in": "path",
"required": true
},
{
"description": "Update user skill request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.UpdateUserSkillRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.UserSkill"
}
}
},
"security": [
{
"CoderSessionToken": []
}
],
"x-apidocgen": {
"skip": true
}
}
},
"/api/experimental/watch-all-workspacebuilds": {
"get": {
"produces": ["application/json"],
@@ -13254,6 +13453,11 @@
"user_secret:delete",
"user_secret:read",
"user_secret:update",
"user_skill:*",
"user_skill:create",
"user_skill:delete",
"user_skill:read",
"user_skill:update",
"webpush_subscription:*",
"webpush_subscription:create",
"webpush_subscription:delete",
@@ -13475,6 +13679,11 @@
"APIKeyScopeUserSecretDelete",
"APIKeyScopeUserSecretRead",
"APIKeyScopeUserSecretUpdate",
"APIKeyScopeUserSkillAll",
"APIKeyScopeUserSkillCreate",
"APIKeyScopeUserSkillDelete",
"APIKeyScopeUserSkillRead",
"APIKeyScopeUserSkillUpdate",
"APIKeyScopeWebpushSubscriptionAll",
"APIKeyScopeWebpushSubscriptionCreate",
"APIKeyScopeWebpushSubscriptionDelete",
@@ -15752,6 +15961,15 @@
}
}
},
"codersdk.CreateUserSkillRequest": {
"type": "object",
"properties": {
"content": {
"description": "Content must be SKILL.md-format Markdown with YAML frontmatter. The\nfrontmatter must include name, may include description, and must be\nfollowed by a non-empty body.",
"type": "string"
}
}
},
"codersdk.CreateWorkspaceBuildReason": {
"type": "string",
"enum": [
@@ -19749,6 +19967,7 @@
"usage_event",
"user",
"user_secret",
"user_skill",
"webpush_subscription",
"workspace",
"workspace_agent_devcontainers",
@@ -19798,6 +20017,7 @@
"ResourceUsageEvent",
"ResourceUser",
"ResourceUserSecret",
"ResourceUserSkill",
"ResourceWebpushSubscription",
"ResourceWorkspace",
"ResourceWorkspaceAgentDevcontainers",
@@ -20013,7 +20233,8 @@
"ai_provider_key",
"group_ai_budget",
"chat",
"user_secret"
"user_secret",
"user_skill"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -20047,7 +20268,8 @@
"ResourceTypeAIProviderKey",
"ResourceTypeGroupAIBudget",
"ResourceTypeChat",
"ResourceTypeUserSecret"
"ResourceTypeUserSecret",
"ResourceTypeUserSkill"
]
},
"codersdk.Response": {
@@ -21887,6 +22109,15 @@
}
}
},
"codersdk.UpdateUserSkillRequest": {
"type": "object",
"properties": {
"content": {
"description": "Content must be SKILL.md-format Markdown with YAML frontmatter. The\nfrontmatter must include name, may include description, and must be\nfollowed by a non-empty body.",
"type": "string"
}
}
},
"codersdk.UpdateWorkspaceACL": {
"type": "object",
"properties": {
@@ -22388,6 +22619,55 @@
}
}
},
"codersdk.UserSkill": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"
}
}
},
"codersdk.UserSkillMetadata": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"
}
}
},
"codersdk.UserStatus": {
"type": "string",
"enum": ["active", "dormant", "suspended"],