skillpins

package
v0.1.0-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

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

View Source
const (
	StatusPinned = "pinned"
	StatusDrift  = "drift"
)

Status values for SkillPin, matching pkg/pins' vocabulary.

View Source
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

View Source
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.

View Source
var ErrDigestUnavailable = errors.New("skill content could not be hashed")

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

type FileDigest struct {
	Path   string `json:"path"`
	Digest string `json:"digest"`
}

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

func (d *SkillDiff) DocumentChanged() bool

DocumentChanged reports whether the canonical SKILL.md moved.

func (*SkillDiff) FilesChanged

func (d *SkillDiff) FilesChanged() bool

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

func New(stackName string) *Store

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

func NewWithPath(dir, stackName string) *Store

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) Get

func (ps *Store) Get(name string) (*SkillPin, bool)

Get returns a deep-copied pin for one skill.

func (*Store) GetAll

func (ps *Store) GetAll() map[string]*SkillPin

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

func (ps *Store) Load() error

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) Reset

func (ps *Store) Reset(name string) error

Reset deletes the pin record for a skill. The next Sync re-pins it fresh.

func (*Store) SetScanConfig

func (ps *Store) SetScanConfig(enabled bool, ignore []string)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL