fix(skillpack): address review — symlink read, git arg-injection, dup-subscribe, nil panics
executus CI / test (pull_request) Successful in 3m30s
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:
+20
-8
@@ -2,6 +2,7 @@ package skillpack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
// — the only call that changes the bytes an agent runs is Apply, always with an
|
||||
// actor recorded.
|
||||
type Syncer struct {
|
||||
Store PackCache // content store for pinned trees
|
||||
Cache PackCache // content store for pinned trees
|
||||
Subs Store // subscription metadata store
|
||||
|
||||
// SourceFor builds the Source for a stored subscription. A host overrides
|
||||
@@ -73,7 +74,7 @@ func (y *Syncer) fetchPack(ctx context.Context, src Source, ref string) (*Pack,
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := y.Store.Put(ctx, pack.Digest, pack.Tree); err != nil {
|
||||
if err := y.Cache.Put(ctx, pack.Digest, pack.Tree); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return pack, sourceRef, nil
|
||||
@@ -87,21 +88,32 @@ func (y *Syncer) Subscribe(ctx context.Context, src Source, trackRef, by string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing, err := y.Subs.GetByName(ctx, pack.Manifest.Name); err == nil {
|
||||
existing, err := y.Subs.GetByName(ctx, pack.Manifest.Name)
|
||||
if err == nil {
|
||||
return nil, fmt.Errorf("skillpack: already subscribed to %q (id %s)", pack.Manifest.Name, existing.ID)
|
||||
}
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
// A transient store error must NOT fall through to creating a row — that
|
||||
// would produce a duplicate subscription the uniqueness check missed.
|
||||
return nil, fmt.Errorf("skillpack: checking for existing subscription %q: %w", pack.Manifest.Name, err)
|
||||
}
|
||||
|
||||
sub := &Subscription{
|
||||
ID: y.newID(),
|
||||
Name: pack.Manifest.Name,
|
||||
SourceKind: src.Kind(),
|
||||
SourceURL: src.String(),
|
||||
TrackRef: trackRef,
|
||||
Enabled: true,
|
||||
}
|
||||
// Store the raw URL/subpath (String() may combine them for display).
|
||||
if gs, ok := src.(GitSource); ok {
|
||||
sub.SourceURL = gs.URL
|
||||
sub.Subpath = gs.Subpath
|
||||
// Store the raw URL + subpath separately (String() may combine them for
|
||||
// display). GitSource methods have value receivers, so a caller may pass
|
||||
// either GitSource or *GitSource — handle both.
|
||||
switch gs := src.(type) {
|
||||
case GitSource:
|
||||
sub.SourceURL, sub.Subpath = gs.URL, gs.Subpath
|
||||
case *GitSource:
|
||||
sub.SourceURL, sub.Subpath = gs.URL, gs.Subpath
|
||||
}
|
||||
sub.pinTo(pack, sourceRef, by, y.now())
|
||||
if err := y.Subs.Save(ctx, sub); err != nil {
|
||||
@@ -175,7 +187,7 @@ func (y *Syncer) Apply(ctx context.Context, id, by string) (*Subscription, error
|
||||
if !sub.HasPending() {
|
||||
return nil, fmt.Errorf("skillpack: %q has no pending update to apply", sub.Name)
|
||||
}
|
||||
tree, err := y.Store.Get(ctx, sub.PendingDigest)
|
||||
tree, err := y.Cache.Get(ctx, sub.PendingDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("skillpack: pending tree for %q missing from cache: %w", sub.Name, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user