fix(skillpack): address review — symlink read, git arg-injection, dup-subscribe, nil panics
executus CI / test (pull_request) Successful in 3m30s

Real issues from the PR review:
- security: readTree now skips symlinks (a pack with SKILL.md -> /etc/passwd
  or scripts/x -> ../../.ssh/id_rsa could read host files); covers file and
  dir symlinks, incl. within a git subpath
- security: GitSource rejects url/ref beginning with '-' (git arg injection)
  and clones with '--' separator; --filter=blob:none (blobless partial clone)
  instead of full-history clone
- correctness: Subscribe no longer swallows a non-ErrNotFound store error from
  GetByName (would create a duplicate subscription); handles *GitSource as well
  as GitSource in the URL/subpath extraction
- correctness: pinTo no longer renames a subscription, so Apply can't silently
  collide two subscriptions when an upstream pack changes its name
- validation: isKebab rejects leading/trailing/consecutive hyphens; BOM-
  prefixed SKILL.md now parses (matches the doc comment)
- robustness: Catalog/Activate/renderPackBody/Stage guard nil/malformed packs
- test cleanup: Syncer.Store field renamed Cache (collided with the Store
  interface); test NewID returns distinct ids
- tests: symlink-skip, BOM, strict-kebab, nil-pack-safety

Deferred (advisory perf, documented): PackCache stores raw trees so activation
re-parses; CheckAll is serial. Both fine at expected scale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 20:41:44 -04:00
parent bf0b67f9af
commit 9bb5d143f7
10 changed files with 150 additions and 24 deletions
+5 -4
View File
@@ -2,6 +2,7 @@ package skillpack
import (
"context"
"fmt"
"testing"
"time"
)
@@ -27,10 +28,10 @@ func packTree(name, body string) Tree {
func newTestSyncer(src *fakeSource) *Syncer {
n := 0
return &Syncer{
Store: NewMemoryPackCache(),
Cache: NewMemoryPackCache(),
Subs: NewMemory(),
Now: func() time.Time { return time.Unix(1000, 0) },
NewID: func() string { n++; return "id-1" },
NewID: func() string { n++; return fmt.Sprintf("id-%d", n) },
SourceFor: func(*Subscription) (Source, error) { return src, nil },
}
}
@@ -51,7 +52,7 @@ func TestSubscribeAndPin(t *testing.T) {
t.Fatal("fresh subscription should have no pending update")
}
// pinned tree is cached under its digest
if _, err := y.Store.Get(ctx, sub.PinnedDigest); err != nil {
if _, err := y.Cache.Get(ctx, sub.PinnedDigest); err != nil {
t.Fatalf("pinned tree not cached: %v", err)
}
}
@@ -93,7 +94,7 @@ func TestCheck_RecordsPendingButDoesNotMovePin(t *testing.T) {
t.Errorf("pending ref = %q", updated.PendingSourceRef)
}
// the pending tree is cached, ready for Apply
if _, err := y.Store.Get(ctx, updated.PendingDigest); err != nil {
if _, err := y.Cache.Get(ctx, updated.PendingDigest); err != nil {
t.Fatalf("pending tree not cached: %v", err)
}
}