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,80 @@
|
||||
package skillpack
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func sampleTree() Tree {
|
||||
return Tree{
|
||||
ManifestName: []byte(goodManifest),
|
||||
"scripts/fill.py": []byte("print('hi')\n"),
|
||||
"references/spec.md": []byte("# spec\n"),
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeDigest_StableAndContentSensitive(t *testing.T) {
|
||||
a := sampleTree()
|
||||
b := sampleTree()
|
||||
if a.Digest() != b.Digest() {
|
||||
t.Fatal("identical trees must share a digest")
|
||||
}
|
||||
b["scripts/fill.py"] = []byte("print('bye')\n")
|
||||
if a.Digest() == b.Digest() {
|
||||
t.Fatal("content change must change the digest")
|
||||
}
|
||||
// Adding a file changes the digest.
|
||||
c := sampleTree()
|
||||
c["extra.txt"] = []byte("x")
|
||||
if a.Digest() == c.Digest() {
|
||||
t.Fatal("added file must change the digest")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPack(t *testing.T) {
|
||||
p, err := LoadPack(sampleTree())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Manifest.Name != "pdf-processing" {
|
||||
t.Errorf("name = %q", p.Manifest.Name)
|
||||
}
|
||||
if len(p.Bundled) != 2 || p.Bundled[0] != "references/spec.md" || p.Bundled[1] != "scripts/fill.py" {
|
||||
t.Errorf("bundled = %v (want sorted, sans SKILL.md)", p.Bundled)
|
||||
}
|
||||
if p.Digest == "" {
|
||||
t.Error("digest empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPack_NoManifest(t *testing.T) {
|
||||
if _, err := LoadPack(Tree{"readme.md": []byte("x")}); err != ErrNoManifest {
|
||||
t.Fatalf("want ErrNoManifest, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeWriteTo(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := sampleTree().WriteTo(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(dir, "scripts", "fill.py"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "print('hi')\n" {
|
||||
t.Errorf("staged content = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeWriteTo_RejectsTraversal(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
evil := Tree{"../escape.txt": []byte("nope")}
|
||||
if err := evil.WriteTo(dir); err == nil {
|
||||
t.Fatal("expected traversal rejection")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(filepath.Dir(dir), "escape.txt")); err == nil {
|
||||
t.Fatal("traversal file was written outside dir")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user