Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
309 lines
10 KiB
Go
309 lines
10 KiB
Go
package runtimeprep
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const stableSkillBundleSchemaVersion = 1
|
|
|
|
type stableSkillBundleManifest struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
Digest string `json:"digest"`
|
|
Provider string `json:"provider"`
|
|
SkillCount int `json:"skillCount"`
|
|
}
|
|
|
|
type canonicalStableSkillBundle struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
Provider string `json:"provider"`
|
|
Skills []canonicalStableSkill `json:"skills"`
|
|
}
|
|
|
|
type canonicalStableSkill struct {
|
|
SkillID string `json:"skillId"`
|
|
Slug string `json:"slug"`
|
|
Directory string `json:"directory"`
|
|
Files []canonicalStableSkillFile `json:"files"`
|
|
}
|
|
|
|
type canonicalStableSkillFile struct {
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type materializedStableSkill struct {
|
|
spec providerSkillSpec
|
|
directory string
|
|
}
|
|
|
|
func materializeStableProviderSkills(
|
|
storeRoot string,
|
|
input PrepareInput,
|
|
) (string, error) {
|
|
if input.SkipSkills {
|
|
return "", nil
|
|
}
|
|
storeRoot = filepath.Clean(strings.TrimSpace(storeRoot))
|
|
if storeRoot == "." || !filepath.IsAbs(storeRoot) {
|
|
return "", fmt.Errorf("stable provider skill bundle root must be absolute")
|
|
}
|
|
specs, err := providerSkills(input)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
canonical, materialized, err := canonicalizeStableProviderSkills(input.Provider, specs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
canonicalJSON, err := json.Marshal(canonical)
|
|
if err != nil {
|
|
return "", fmt.Errorf("encode stable provider skill bundle: %w", err)
|
|
}
|
|
digestBytes := sha256.Sum256(canonicalJSON)
|
|
digest := hex.EncodeToString(digestBytes[:])
|
|
manifest := stableSkillBundleManifest{
|
|
SchemaVersion: stableSkillBundleSchemaVersion,
|
|
Digest: digest,
|
|
Provider: strings.TrimSpace(input.Provider),
|
|
SkillCount: len(materialized),
|
|
}
|
|
versionRoot := filepath.Join(storeRoot, fmt.Sprintf("v%d", stableSkillBundleSchemaVersion))
|
|
bundleRoot := filepath.Join(versionRoot, digest)
|
|
skillsRoot := filepath.Join(bundleRoot, "skills")
|
|
if err := validateStableSkillBundle(bundleRoot, manifest, materialized); err == nil {
|
|
return skillsRoot, nil
|
|
} else if !os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
if err := os.MkdirAll(versionRoot, 0o755); err != nil {
|
|
return "", fmt.Errorf("create stable provider skill bundle store: %w", err)
|
|
}
|
|
temporaryRoot, err := os.MkdirTemp(versionRoot, ".tmp-"+digest+"-")
|
|
if err != nil {
|
|
return "", fmt.Errorf("create stable provider skill bundle temporary directory: %w", err)
|
|
}
|
|
defer func() { _ = os.RemoveAll(temporaryRoot) }()
|
|
temporarySkillsRoot := filepath.Join(temporaryRoot, "skills")
|
|
if err := os.MkdirAll(temporarySkillsRoot, 0o755); err != nil {
|
|
return "", fmt.Errorf("create stable provider skill root: %w", err)
|
|
}
|
|
for _, skill := range materialized {
|
|
if err := installProviderSkillFiles(
|
|
filepath.Join(temporarySkillsRoot, skill.directory),
|
|
skill.spec,
|
|
); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
manifestJSON, err := stableSkillBundleManifestJSON(manifest)
|
|
if err != nil {
|
|
return "", fmt.Errorf("encode stable provider skill bundle manifest: %w", err)
|
|
}
|
|
if err := os.WriteFile(
|
|
filepath.Join(temporaryRoot, "bundle.json"),
|
|
manifestJSON,
|
|
0o644,
|
|
); err != nil {
|
|
return "", fmt.Errorf("write stable provider skill bundle manifest: %w", err)
|
|
}
|
|
if err := validateStableSkillBundle(temporaryRoot, manifest, materialized); err != nil {
|
|
return "", fmt.Errorf("validate staged provider skill bundle: %w", err)
|
|
}
|
|
if err := os.Rename(temporaryRoot, bundleRoot); err != nil {
|
|
if validationErr := validateStableSkillBundle(bundleRoot, manifest, materialized); validationErr == nil {
|
|
return skillsRoot, nil
|
|
}
|
|
return "", fmt.Errorf("commit stable provider skill bundle: %w", err)
|
|
}
|
|
return skillsRoot, nil
|
|
}
|
|
|
|
func canonicalizeStableProviderSkills(
|
|
provider string,
|
|
specs []providerSkillSpec,
|
|
) (canonicalStableSkillBundle, []materializedStableSkill, error) {
|
|
sorted := append([]providerSkillSpec(nil), specs...)
|
|
sort.Slice(sorted, func(left, right int) bool {
|
|
leftID, rightID := strings.TrimSpace(sorted[left].skillID), strings.TrimSpace(sorted[right].skillID)
|
|
if leftID != rightID {
|
|
return leftID < rightID
|
|
}
|
|
return strings.TrimSpace(sorted[left].baseName) < strings.TrimSpace(sorted[right].baseName)
|
|
})
|
|
canonical := canonicalStableSkillBundle{
|
|
SchemaVersion: stableSkillBundleSchemaVersion,
|
|
Provider: strings.TrimSpace(provider),
|
|
Skills: make([]canonicalStableSkill, 0, len(sorted)),
|
|
}
|
|
materialized := make([]materializedStableSkill, 0, len(sorted))
|
|
usedDirectories := make(map[string]struct{}, len(sorted))
|
|
for _, spec := range sorted {
|
|
directory, err := allocateStableBundleSkillDirectory(spec.baseName, usedDirectories)
|
|
if err != nil {
|
|
return canonicalStableSkillBundle{}, nil, err
|
|
}
|
|
normalizedFiles := make(map[string]string, len(spec.files))
|
|
paths := make([]string, 0, len(spec.files))
|
|
for filePath, content := range spec.files {
|
|
cleanPath, err := cleanProviderSkillFilePath(filePath)
|
|
if err != nil {
|
|
return canonicalStableSkillBundle{}, nil, err
|
|
}
|
|
if _, exists := normalizedFiles[cleanPath]; exists {
|
|
return canonicalStableSkillBundle{}, nil, fmt.Errorf(
|
|
"provider skill %s has duplicate normalized file path %q",
|
|
spec.skillID,
|
|
cleanPath,
|
|
)
|
|
}
|
|
normalizedFiles[cleanPath] = content
|
|
paths = append(paths, cleanPath)
|
|
}
|
|
sort.Strings(paths)
|
|
files := make([]canonicalStableSkillFile, 0, len(paths))
|
|
for _, filePath := range paths {
|
|
files = append(files, canonicalStableSkillFile{
|
|
Path: filePath,
|
|
Content: normalizedFiles[filePath],
|
|
})
|
|
}
|
|
canonical.Skills = append(canonical.Skills, canonicalStableSkill{
|
|
SkillID: strings.TrimSpace(spec.skillID),
|
|
Slug: strings.TrimSpace(spec.baseName),
|
|
Directory: directory,
|
|
Files: files,
|
|
})
|
|
materialized = append(materialized, materializedStableSkill{
|
|
spec: spec,
|
|
directory: directory,
|
|
})
|
|
}
|
|
return canonical, materialized, nil
|
|
}
|
|
|
|
func allocateStableBundleSkillDirectory(
|
|
baseName string,
|
|
used map[string]struct{},
|
|
) (string, error) {
|
|
baseName = strings.TrimSpace(baseName)
|
|
if baseName == "" || baseName == "." || baseName == ".." ||
|
|
filepath.Base(baseName) != baseName || strings.ContainsAny(baseName, `/\\`) {
|
|
return "", fmt.Errorf("provider skill name is invalid: %q", baseName)
|
|
}
|
|
candidates := []string{baseName, baseName + "-tutti"}
|
|
for index := 2; index <= 99; index++ {
|
|
candidates = append(candidates, fmt.Sprintf("%s-tutti-%d", baseName, index))
|
|
}
|
|
for _, candidate := range candidates {
|
|
if _, exists := used[candidate]; exists {
|
|
continue
|
|
}
|
|
used[candidate] = struct{}{}
|
|
return candidate, nil
|
|
}
|
|
return "", fmt.Errorf("allocate stable provider skill directory: exhausted names for %s", baseName)
|
|
}
|
|
|
|
func validateStableSkillBundle(
|
|
bundleRoot string,
|
|
wantManifest stableSkillBundleManifest,
|
|
skills []materializedStableSkill,
|
|
) error {
|
|
info, err := os.Stat(bundleRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("stable provider skill bundle is not a directory: %s", bundleRoot)
|
|
}
|
|
manifestBytes, err := os.ReadFile(filepath.Join(bundleRoot, "bundle.json"))
|
|
if err != nil {
|
|
return fmt.Errorf("read stable provider skill bundle manifest: %w", err)
|
|
}
|
|
var manifest stableSkillBundleManifest
|
|
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
|
|
return fmt.Errorf("decode stable provider skill bundle manifest: %w", err)
|
|
}
|
|
if manifest != wantManifest {
|
|
return fmt.Errorf("stable provider skill bundle manifest does not match digest")
|
|
}
|
|
wantManifestBytes, err := stableSkillBundleManifestJSON(wantManifest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wantFiles := map[string]string{"bundle.json": string(wantManifestBytes)}
|
|
wantDirectories := map[string]struct{}{".": {}, "skills": {}}
|
|
for _, skill := range skills {
|
|
directory := filepath.ToSlash(filepath.Join("skills", skill.directory))
|
|
wantDirectories[directory] = struct{}{}
|
|
wantFiles[filepath.ToSlash(filepath.Join(directory, ".tutti-managed-skill"))] =
|
|
skill.spec.skillID + "\n"
|
|
for filePath, content := range skill.spec.files {
|
|
cleanPath, err := cleanProviderSkillFilePath(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relative := filepath.ToSlash(filepath.Join(directory, filepath.FromSlash(cleanPath)))
|
|
wantFiles[relative] = content
|
|
for parent := filepath.ToSlash(filepath.Dir(relative)); parent != "."; parent = filepath.ToSlash(filepath.Dir(parent)) {
|
|
wantDirectories[parent] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
seenFiles := make(map[string]struct{}, len(wantFiles))
|
|
err = filepath.WalkDir(bundleRoot, func(path string, entry os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
relative, err := filepath.Rel(bundleRoot, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relative = filepath.ToSlash(relative)
|
|
if entry.Type()&os.ModeSymlink != 0 {
|
|
return fmt.Errorf("stable provider skill bundle contains symlink: %s", relative)
|
|
}
|
|
if entry.IsDir() {
|
|
if _, exists := wantDirectories[relative]; !exists {
|
|
return fmt.Errorf("stable provider skill bundle contains unexpected directory: %s", relative)
|
|
}
|
|
return nil
|
|
}
|
|
want, exists := wantFiles[relative]
|
|
if !exists {
|
|
return fmt.Errorf("stable provider skill bundle contains unexpected file: %s", relative)
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if string(content) != want {
|
|
return fmt.Errorf("stable provider skill bundle file does not match digest: %s", relative)
|
|
}
|
|
seenFiles[relative] = struct{}{}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(seenFiles) != len(wantFiles) {
|
|
return fmt.Errorf("stable provider skill bundle is incomplete")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func stableSkillBundleManifestJSON(manifest stableSkillBundleManifest) ([]byte, error) {
|
|
encoded, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode stable provider skill bundle manifest: %w", err)
|
|
}
|
|
return append(encoded, '\n'), nil
|
|
}
|