mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -0,0 +1,120 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UserSkillMetadata represents a user skill without its raw Markdown content.
|
||||
type UserSkillMetadata struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
}
|
||||
|
||||
// UserSkill represents a user skill with its raw Markdown content.
|
||||
type UserSkill struct {
|
||||
UserSkillMetadata
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// CreateUserSkillRequest is the payload for creating a user skill.
|
||||
type CreateUserSkillRequest struct {
|
||||
// Content must be SKILL.md-format Markdown with YAML frontmatter. The
|
||||
// frontmatter must include name, may include description, and must be
|
||||
// followed by a non-empty body.
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// UpdateUserSkillRequest is the payload for updating a user skill.
|
||||
type UpdateUserSkillRequest struct {
|
||||
// Content must be SKILL.md-format Markdown with YAML frontmatter. The
|
||||
// frontmatter must include name, may include description, and must be
|
||||
// followed by a non-empty body.
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func userSkillsPath(user string) string {
|
||||
return fmt.Sprintf("/api/experimental/users/%s/skills", url.PathEscape(user))
|
||||
}
|
||||
|
||||
func userSkillPath(user string, name string) string {
|
||||
return fmt.Sprintf("%s/%s", userSkillsPath(user), url.PathEscape(name))
|
||||
}
|
||||
|
||||
// CreateUserSkill creates a user skill from raw Markdown content.
|
||||
func (c *ExperimentalClient) CreateUserSkill(ctx context.Context, user string, req CreateUserSkillRequest) (UserSkill, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, userSkillsPath(user), req)
|
||||
if err != nil {
|
||||
return UserSkill{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return UserSkill{}, ReadBodyAsError(res)
|
||||
}
|
||||
var skill UserSkill
|
||||
return skill, json.NewDecoder(res.Body).Decode(&skill)
|
||||
}
|
||||
|
||||
// UserSkills lists user skill metadata for the specified user.
|
||||
func (c *ExperimentalClient) UserSkills(ctx context.Context, user string) ([]UserSkillMetadata, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, userSkillsPath(user), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
var skills []UserSkillMetadata
|
||||
return skills, json.NewDecoder(res.Body).Decode(&skills)
|
||||
}
|
||||
|
||||
// UserSkillByName returns a user skill by name.
|
||||
func (c *ExperimentalClient) UserSkillByName(ctx context.Context, user string, name string) (UserSkill, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, userSkillPath(user, name), nil)
|
||||
if err != nil {
|
||||
return UserSkill{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return UserSkill{}, ReadBodyAsError(res)
|
||||
}
|
||||
var skill UserSkill
|
||||
return skill, json.NewDecoder(res.Body).Decode(&skill)
|
||||
}
|
||||
|
||||
// UpdateUserSkill replaces a user skill's raw Markdown content.
|
||||
func (c *ExperimentalClient) UpdateUserSkill(ctx context.Context, user string, name string, req UpdateUserSkillRequest) (UserSkill, error) {
|
||||
res, err := c.Request(ctx, http.MethodPatch, userSkillPath(user, name), req)
|
||||
if err != nil {
|
||||
return UserSkill{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return UserSkill{}, ReadBodyAsError(res)
|
||||
}
|
||||
var skill UserSkill
|
||||
return skill, json.NewDecoder(res.Body).Decode(&skill)
|
||||
}
|
||||
|
||||
// DeleteUserSkill deletes a user skill by name.
|
||||
func (c *ExperimentalClient) DeleteUserSkill(ctx context.Context, user string, name string) error {
|
||||
res, err := c.Request(ctx, http.MethodDelete, userSkillPath(user, name), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user