feat(skillpack): SKILL.md-subscription battery
New additive, nil-safe battery for subscribing to skill packages in the Anthropic agent-skills format (SKILL.md manifest + bundled files): - Manifest/ParseManifest: SKILL.md frontmatter+body parse & validation (name/description required, allowed-tools passthrough, kebab/length limits) - Tree/Pack/LoadPack: self-contained file set, order-independent content digest (the pin identity + change signal), bundled-file listing, traversal- safe staging - Source (DirSource, GitSource): Fetch returns tree + resolved ref; git clones to temp, reads subpath into memory, cleans up (self-contained tree) - Subscription + Store + content-addressed PackCache, with Memory defaults - Syncer: Subscribe pins; Check records a PENDING update but never moves the pin; Apply is the only re-pin (supply-chain guard — upstream can't silently change what an agent runs) - Activate: resolved packs -> majordomo agent.Skill (catalog instructions + one skill_use tool) for progressive disclosure; Stage materializes files Third distinct 'skill' concept, deliberately separate from executus/skill (saved-agent noun) and majordomo/skill (eager capability bundle). Mort-side wiring (convars, .skillpack commands, Agent.SkillPacks, allowed-tools shim) is a later, separate step. Full unit + hermetic local-git tests; gofmt/vet clean; race-tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package skillpack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writePack(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "scripts"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, ManifestName), []byte(goodManifest), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "scripts", "fill.py"), []byte("print('hi')\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSource(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePack(t, dir)
|
||||
|
||||
tree, ref, err := DirSource{Path: dir}.Fetch(context.Background(), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ref != tree.Digest() {
|
||||
t.Errorf("dir resolved ref should be the content digest")
|
||||
}
|
||||
p, err := LoadPack(tree)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Manifest.Name != "pdf-processing" || len(p.Bundled) != 1 {
|
||||
t.Errorf("loaded pack wrong: name=%q bundled=%v", p.Manifest.Name, p.Bundled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSource_NotADir(t *testing.T) {
|
||||
f := filepath.Join(t.TempDir(), "file")
|
||||
os.WriteFile(f, []byte("x"), 0o644)
|
||||
if _, _, err := (DirSource{Path: f}).Fetch(context.Background(), ""); err == nil {
|
||||
t.Fatal("expected error for non-directory source")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitSource drives a real local git repo (no network) to exercise clone +
|
||||
// checkout + subpath + SHA resolution. Skipped when git is unavailable.
|
||||
func TestGitSource(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not installed")
|
||||
}
|
||||
repo := t.TempDir()
|
||||
git := func(args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = repo
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v: %v: %s", args, err, out)
|
||||
}
|
||||
}
|
||||
git("init", "-q", "-b", "main")
|
||||
// pack lives under packs/pdf/
|
||||
sub := filepath.Join(repo, "packs", "pdf")
|
||||
writePack(t, sub)
|
||||
git("add", "-A")
|
||||
git("commit", "-q", "-m", "add pack")
|
||||
|
||||
src := GitSource{URL: repo, Subpath: "packs/pdf"}
|
||||
tree, sha, err := src.Fetch(context.Background(), "main")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sha) != 40 {
|
||||
t.Errorf("resolved ref should be a full SHA, got %q", sha)
|
||||
}
|
||||
if _, ok := tree[ManifestName]; !ok {
|
||||
t.Errorf("subpath tree missing SKILL.md; got %v", tree.Paths())
|
||||
}
|
||||
if _, ok := tree[".git"]; ok {
|
||||
t.Error(".git leaked into the tree")
|
||||
}
|
||||
p, err := LoadPack(tree)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Manifest.Name != "pdf-processing" {
|
||||
t.Errorf("name = %q", p.Manifest.Name)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user