Documentation
¶
Overview ¶
Package skillpins implements TOFU content pins for registry skill documents, the document-scale sibling of pkg/pins' tool-schema pins. A pin records per-file SHA-256 digests over a skill's whole document set (canonicalized SKILL.md plus supporting files); any later mismatch is "pin drift" that persists until a human approves (re-pins) or resets. The deterministic hash is the gate; poisoning findings attached to a pin are advisory decoration and never block anything.
"Pin drift" is deliberately distinct from pkg/skills sync drift (local edits vs the last git import): the two are different facts about the same skill and can both be true at once.
Index ¶
- Constants
- Variables
- func CanonicalSkillHash(sk *registry.AgentSkill) (string, error)
- func CompositeHash(skillHash string, files []FileDigest) string
- type FileDigest
- type OriginRef
- type PinFile
- type SkillDiff
- type SkillPin
- type SkillSource
- type Store
- func (ps *Store) Approve(name string, src SkillSource, expectedHash, reason string) error
- func (ps *Store) CurrentCompositeHash(name string, src SkillSource) (string, error)
- func (ps *Store) Get(name string) (*SkillPin, bool)
- func (ps *Store) GetAll() map[string]*SkillPin
- func (ps *Store) Load() error
- func (ps *Store) Reset(name string) error
- func (ps *Store) SetScanConfig(enabled bool, ignore []string)
- func (ps *Store) Sync(src SkillSource) (*SyncResult, error)
- func (ps *Store) Verify(name string, src SkillSource) (*VerifyResult, error)
- type SyncResult
- type VerifyResult
Constants ¶
const ( StatusPinned = "pinned" StatusDrift = "drift" )
Status values for SkillPin, matching pkg/pins' vocabulary.
const ( SourceLocal = "local" SourceGit = "git" )
Source discriminants for SkillPin.Source. The schema deliberately leaves room for future sources (e.g. "upstream" for gateway-ingested skills): unknown values load fine and pass through untouched.
Variables ¶
var ( ErrCorrupt = errors.New("corrupt skill pin file") ErrNewerVersion = errors.New("skill pin file written by a newer gridctl") ErrNotPinned = errors.New("skill is not pinned") ErrHashMismatch = errors.New("skill content changed since the reviewed diff") ErrReasonRequired = errors.New("approving a skill with unresolved findings requires a reason") )
Sentinel errors. Load's ErrCorrupt/ErrNewerVersion follow the pkg/pins contract: a caller preferring availability (the daemon) may continue past ErrCorrupt with the empty store Load leaves behind; ErrNewerVersion must never be papered over. The rest are decision errors surfaced by Approve and the diff paths so the CLI and API can map them to exit codes and status codes without string matching.
ErrDigestUnavailable marks a digest pass that could not read the skill's content (an unreadable supporting file, e.g. a dangling symlink). It is deliberately distinct from registry.ErrNotFound: consumers must never confuse "cannot hash this skill" with "this skill does not exist" — the former is a fail-closed condition, the latter a reset hint.
Functions ¶
func CanonicalSkillHash ¶
func CanonicalSkillHash(sk *registry.AgentSkill) (string, error)
CanonicalSkillHash digests the canonical rendering of a skill's SKILL.md. Hashing the parse-rendered form (not raw bytes) is what keeps the hash stable across frontmatter normalization: import, editor save, and projection all round-trip through registry.ParseSkillMD/RenderSkillMD, so a semantically unchanged skill can never manufacture pin drift.
The gridctl-managed `state` field is excluded from the hash input: a draft/active/disabled toggle is an exposure decision made through gridctl itself, not a content change, and must not trip a gate built for out-of-band edits.
func CompositeHash ¶
func CompositeHash(skillHash string, files []FileDigest) string
CompositeHash folds a skill hash and its file digests into one fingerprint, the value approvals bind to (the pkg/pins HashTools precedent): capture it when rendering a diff, compare it at approve time, and reject on mismatch so content cannot change between review and approval.
Types ¶
type FileDigest ¶
FileDigest is one supporting file's content digest, path relative to the skill directory.
func ComputeDigests ¶
func ComputeDigests(src SkillSource, sk *registry.AgentSkill) (skillHash string, files []FileDigest, err error)
ComputeDigests hashes a skill's whole document set: the canonical SKILL.md plus every digested supporting file, sorted by path. File-read failures wrap ErrDigestUnavailable with the underlying cause flattened (%v, not %w): src wraps registry.ErrNotFound into per-file errors, and letting that chain leak would make an unreadable file indistinguishable from a deleted skill.
type OriginRef ¶
type OriginRef struct {
Repo string `json:"repo,omitempty"`
Ref string `json:"ref,omitempty"`
CommitSHA string `json:"commitSha,omitempty"`
}
OriginRef is the factual provenance of a git-imported skill, copied from its .origin.json sidecar at pin time. Display-only: origin answers "where did this come from", never "is this safe". The commitSha tag deliberately mirrors the sidecar's camelCase key (pkg/skills.Origin) rather than this file's snake_case convention, so the two records stay field-identical.
type PinFile ¶
type PinFile struct {
Version string `json:"version"`
Stack string `json:"stack"`
CreatedAt time.Time `json:"created_at"`
Skills map[string]*SkillPin `json:"skills"`
}
PinFile is the top-level JSON structure stored at ~/.gridctl/pins/{stackName}.skills.json.
type SkillDiff ¶
type SkillDiff struct {
Name string `json:"name"`
OldSkillHash string `json:"old_skill_hash"`
NewSkillHash string `json:"new_skill_hash"`
OldDocument string `json:"old_document,omitempty"`
NewDocument string `json:"new_document,omitempty"`
AddedFiles []string `json:"added_files,omitempty"`
RemovedFiles []string `json:"removed_files,omitempty"`
ModifiedFiles []string `json:"modified_files,omitempty"`
Findings []pins.Finding `json:"findings,omitempty"`
}
SkillDiff describes how a skill's document set moved since its pin. Findings are advisory results for the NEW content, computed at verify time so the reviewer sees them beside the diff they annotate.
func (*SkillDiff) DocumentChanged ¶
DocumentChanged reports whether the canonical SKILL.md moved.
func (*SkillDiff) FilesChanged ¶
FilesChanged reports whether any supporting file was added, removed, or modified.
type SkillPin ¶
type SkillPin struct {
// SkillHash is the digest of the canonical SKILL.md rendering.
SkillHash string `json:"skill_hash"`
// Files are the supporting-file digests, sorted by path.
Files []FileDigest `json:"files,omitempty"`
// Document is the canonical SKILL.md as pinned, kept so drift review can
// show a prose diff (the pkg/pins Description/schema-capture precedent).
Document string `json:"document,omitempty"`
// Source and Origin are provenance at pin time: "local" or "git".
Source string `json:"source,omitempty"`
Origin *OriginRef `json:"origin,omitempty"`
// ApprovedReason records the human justification when a pin carrying
// unresolved findings was approved. Empty for finding-free approvals.
ApprovedReason string `json:"approved_reason,omitempty"`
PinnedAt time.Time `json:"pinned_at"`
LastVerifiedAt time.Time `json:"last_verified_at"`
Status string `json:"status"`
// Findings are advisory poisoning-scan results for the pinned content.
Findings []pins.Finding `json:"findings,omitempty"`
}
SkillPin holds the pin state for a single skill. Document, Findings, Source, and Origin are derived data in the pkg/pins sense: an older gridctl rewriting the file drops them without a file-version bump, and only the digests are load-bearing for drift.
type SkillSource ¶
type SkillSource interface {
ListSkills() []*registry.AgentSkill
GetSkill(name string) (*registry.AgentSkill, error)
ListFiles(skillName string) ([]registry.SkillFile, error)
ReadFile(skillName, filePath string) ([]byte, error)
}
SkillSource is the read surface the pin store needs from the registry. *registry.Store satisfies it; defining it here keeps the dependency pointing from skillpins into registry, never back.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store manages TOFU content pins for a stack's skill registry. It is safe for concurrent use: in-memory access is guarded by a RWMutex, and disk writes are serialized across processes via state.WithLock. The pin file lives under ~/.gridctl/pins/, deliberately outside the watched registry tree, so pin writes can never re-trigger the registry watcher.
func New ¶
New creates a Store for the given stack name. The pin file lives at ~/.gridctl/pins/skills/{stackName}.json. Call Load() before performing verification or pinning operations.
func NewWithPath ¶
NewWithPath creates a Store that keeps pins in dir/{stackName}.skills.json. Intended for testing where the real state directory should not be used.
func (*Store) Approve ¶
func (ps *Store) Approve(name string, src SkillSource, expectedHash, reason string) error
Approve re-pins a skill's current content, clearing pin drift. expectedHash, when non-empty, must match the current composite hash or ErrHashMismatch is returned — binding the approval to the reviewed content. When the current content carries unresolved advisory findings, a non-empty reason is required (ErrReasonRequired otherwise) and is persisted on the record.
func (*Store) CurrentCompositeHash ¶
func (ps *Store) CurrentCompositeHash(name string, src SkillSource) (string, error)
CurrentCompositeHash computes the approval fingerprint for a skill's current content, the value a reviewed diff carries and Approve checks.
func (*Store) GetAll ¶
GetAll returns a deep-copied snapshot of every skill pin, keyed by skill name. Copies for the same reason pkg/pins copies: callers marshal outside the lock while Sync mutates records in place.
func (*Store) Load ¶
Load reads the pin file from disk into memory. A missing file starts the store empty (ready for first pin); a corrupt file resets to empty and wraps ErrCorrupt; a newer-version file resets to empty and wraps ErrNewerVersion.
func (*Store) SetScanConfig ¶
SetScanConfig configures the advisory poisoning scanner: enabled toggles it, ignore suppresses findings by code. Call before the store starts pinning or verifying.
func (*Store) Sync ¶
func (ps *Store) Sync(src SkillSource) (*SyncResult, error)
Sync is the primary entry point, called after every registry refresh. It TOFU-pins skills seen for the first time (silently), verifies the rest, and marks drift. It never auto-approves and never prunes records for skills missing from the registry — both wait for a human. One save per pass, only when something changed.
func (*Store) Verify ¶
func (ps *Store) Verify(name string, src SkillSource) (*VerifyResult, error)
Verify builds the diff for one skill against its pin without writing anything. Returns ErrNotPinned when the skill has no pin yet and registry.ErrNotFound (wrapped by src) when the skill does not exist.
type SyncResult ¶
type SyncResult struct {
// Pinned lists skills pinned for the first time this pass.
Pinned []string
// Drifted lists skills whose content no longer matches their pin.
Drifted []string
// Missing lists pinned skills absent from the registry. Their records
// are kept (reset guidance surfaces in the CLI), never auto-pruned.
Missing []string
}
SyncResult summarizes one TOFU/verify pass over the whole registry.
type VerifyResult ¶
type VerifyResult struct {
SkillName string
Status string // StatusPinned (first pin or clean) | StatusDrift
CompositeHash string
Diff *SkillDiff
}
VerifyResult is the outcome of verifying one skill against its pin. CompositeHash is the approval-binding fingerprint of the verified content, computed from the same digests the Diff describes.