9bb5d143f7
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>
66 lines
2.5 KiB
Go
66 lines
2.5 KiB
Go
package skillpack
|
|
|
|
import "time"
|
|
|
|
// Subscription is a host's persisted "I track this pack, pinned here" record. It
|
|
// is metadata only — the pinned pack's bytes live in a PackCache keyed by
|
|
// PinnedDigest. A subscription is only ever advanced to new content by an
|
|
// explicit Apply (see Syncer): a sync records a PendingDigest, it never moves
|
|
// the pin. That is the supply-chain guard — a compromised or careless upstream
|
|
// cannot change what an agent runs without a human re-pin.
|
|
type Subscription struct {
|
|
// ID is a stable host-assigned identifier.
|
|
ID string
|
|
// Name is the pack's manifest name (unique per host); what an agent lists in
|
|
// its SkillPacks and what skill_use receives.
|
|
Name string
|
|
// Description is the pinned manifest's description, cached so the catalog
|
|
// renders without opening the PackCache.
|
|
Description string
|
|
|
|
// Source coordinates.
|
|
SourceKind string // "dir" | "git"
|
|
SourceURL string // dir path or git URL
|
|
Subpath string // git subpath, if any
|
|
// TrackRef is the git commit-ish the host follows (branch/tag/SHA); empty =
|
|
// default branch. Sync fetches THIS; the pin only moves on Apply.
|
|
TrackRef string
|
|
|
|
// Pinned* describe the currently-active content.
|
|
PinnedDigest string // content digest = PackCache key + change signal
|
|
PinnedSourceRef string // source's resolved ref (git SHA) — provenance
|
|
PinnedAt time.Time
|
|
PinnedBy string
|
|
|
|
// Pending* describe an update a sync found but has NOT applied. Empty
|
|
// PendingDigest = no pending update. A pending digest equal to the pinned
|
|
// one is impossible by construction (Syncer clears it).
|
|
PendingDigest string
|
|
PendingSourceRef string
|
|
PendingAt time.Time
|
|
|
|
// Enabled lets a host keep a subscription but deactivate it without
|
|
// deleting the pin/history.
|
|
Enabled bool
|
|
}
|
|
|
|
// HasPending reports whether a sync found an unapplied update.
|
|
func (s *Subscription) HasPending() bool {
|
|
return s.PendingDigest != "" && s.PendingDigest != s.PinnedDigest
|
|
}
|
|
|
|
// pinTo advances the active pin to a fetched pack and clears any pending state.
|
|
// Used by initial pin and by Apply. It does NOT set Name: a subscription's name
|
|
// is its stable host handle, fixed at Subscribe time — letting an upstream pack
|
|
// rename move it would silently collide with another subscription on Apply.
|
|
func (s *Subscription) pinTo(p *Pack, sourceRef, by string, now time.Time) {
|
|
s.Description = p.Manifest.Description
|
|
s.PinnedDigest = p.Digest
|
|
s.PinnedSourceRef = sourceRef
|
|
s.PinnedAt = now
|
|
s.PinnedBy = by
|
|
s.PendingDigest = ""
|
|
s.PendingSourceRef = ""
|
|
s.PendingAt = time.Time{}
|
|
}
|