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:
@@ -26,7 +26,7 @@
|
||||
// and workspace/<name> for the workspace skill. One source must not silently
|
||||
// override the other.
|
||||
//
|
||||
// Site admins can read and modify personal skill content. Personal skills are
|
||||
// Site admins can read and delete personal skill content. Personal skills are
|
||||
// user-authored instructions, not secret material. Audit records can include
|
||||
// raw Markdown content diffs alongside the actor, target user, and relevant
|
||||
// metadata.
|
||||
|
||||
@@ -18,6 +18,14 @@ const MaxPersonalSkillSizeBytes = workspacesdk.MaxSkillMetaBytes
|
||||
// personal skill upload. Skill names are also used in URL paths.
|
||||
const MaxPersonalSkillNameBytes = 256
|
||||
|
||||
// MaxPersonalSkillDescriptionBytes is the maximum frontmatter description size
|
||||
// accepted for a personal skill upload.
|
||||
const MaxPersonalSkillDescriptionBytes = 4096
|
||||
|
||||
// MaxPersonalSkillsPerUser is the maximum number of personal skills a user may
|
||||
// create.
|
||||
const MaxPersonalSkillsPerUser = 100
|
||||
|
||||
// Source identifies where a skill came from.
|
||||
type Source string
|
||||
|
||||
@@ -36,6 +44,8 @@ var (
|
||||
ErrSkillBodyRequired = xerrors.New("skill body is required")
|
||||
// ErrSkillTooLarge indicates that the raw skill Markdown is too large.
|
||||
ErrSkillTooLarge = xerrors.New("skill is too large")
|
||||
// ErrSkillDescriptionTooLarge indicates that the description is too large.
|
||||
ErrSkillDescriptionTooLarge = xerrors.New("skill description is too large")
|
||||
// ErrSkillNotFound indicates that a skill lookup did not match any alias.
|
||||
ErrSkillNotFound = xerrors.New("skill not found")
|
||||
// ErrSkillAmbiguous indicates that a skill lookup matched multiple sources.
|
||||
@@ -65,8 +75,9 @@ type ResolvedSkill struct {
|
||||
// ParsePersonalSkillMarkdown parses raw personal skill Markdown and enforces
|
||||
// the personal skill contract. The raw size must not exceed
|
||||
// MaxPersonalSkillSizeBytes, frontmatter must contain a valid kebab-case name,
|
||||
// the skill name must not exceed MaxPersonalSkillNameBytes, and the body after
|
||||
// frontmatter must be non-empty.
|
||||
// the skill name must not exceed MaxPersonalSkillNameBytes, the description must
|
||||
// not exceed MaxPersonalSkillDescriptionBytes, and the body after frontmatter
|
||||
// must be non-empty.
|
||||
func ParsePersonalSkillMarkdown(raw []byte) (ParsedSkill, error) {
|
||||
if len(raw) > MaxPersonalSkillSizeBytes {
|
||||
return ParsedSkill{}, xerrors.Errorf(
|
||||
@@ -102,6 +113,15 @@ func ParsePersonalSkillMarkdown(raw []byte) (ParsedSkill, error) {
|
||||
MaxPersonalSkillNameBytes,
|
||||
)
|
||||
}
|
||||
descriptionBytes := len(description)
|
||||
if descriptionBytes > MaxPersonalSkillDescriptionBytes {
|
||||
return ParsedSkill{}, xerrors.Errorf(
|
||||
"%w: got %d bytes, maximum is %d bytes",
|
||||
ErrSkillDescriptionTooLarge,
|
||||
descriptionBytes,
|
||||
MaxPersonalSkillDescriptionBytes,
|
||||
)
|
||||
}
|
||||
if strings.TrimSpace(body) == "" {
|
||||
return ParsedSkill{}, xerrors.Errorf(
|
||||
"%w: skill %q has no content after frontmatter",
|
||||
|
||||
@@ -91,6 +91,19 @@ func TestParsePersonalSkillMarkdown(t *testing.T) {
|
||||
require.ErrorContains(t, err, "maximum is 256 bytes")
|
||||
})
|
||||
|
||||
t.Run("DescriptionTooLong", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := skills.ParsePersonalSkillMarkdown([]byte(personalSkillMarkdownForTest(
|
||||
"my-skill",
|
||||
strings.Repeat("a", skills.MaxPersonalSkillDescriptionBytes+1),
|
||||
"Body.",
|
||||
)))
|
||||
|
||||
require.ErrorIs(t, err, skills.ErrSkillDescriptionTooLarge)
|
||||
require.ErrorContains(t, err, "maximum is 4096 bytes")
|
||||
})
|
||||
|
||||
t.Run("EmptyBody", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user