skillpins

package
v1.0.0-rc.5 Latest Latest
Warning

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

Go to latest
Published: Nov 8, 2026 License: Apache-2.0 Imports: 17 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.

View Source
const (

	// FileVersion is the skill pin file version this binary writes and the
	// highest version it accepts. Advertised by `gridctl info --json`.
	FileVersion = "1"
)

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 (for example, a supporting file removed during hashing). 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.

func ListStacks

func ListStacks() ([]string, error)

ListStacks returns the stack names of existing skill pin files (<pins>/skills/*.json), sorted. A missing directory is an empty list, not an error. Temporary files are ignored. A name that is not a single .json basename is ignored so a stray file never becomes a stack name.

func ResetOnDisk

func ResetOnDisk(ctx context.Context, name string, skip func(stack string) bool) (reset []string, skipped []string, err error)

ResetOnDisk resets name in every on-disk skill pin file except those skip returns true for. A nil skip excludes nothing. Each remaining file is loaded on a throwaway New(stack); files that fail to load are recorded in skipped and logged, not rewritten. Stacks excluded by skip are omitted from both lists. err is reserved for ListStacks and context errors.

Types

type ApproveRequest

type ApproveRequest struct {
	ExpectedHash     string
	Reason           string
	Evidence         []string
	ApprovedBy       string
	ApprovedBySource string
}

ApproveRequest is one explicit skill approval. ExpectedHash, when non-empty, must match the current composite hash. Evidence is stored as returned by pinapprovals.ValidateReferences, not as the caller supplied it.

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 Manifest

type Manifest struct {
	SkillMD []byte
	Files   []ManifestFile
}

Manifest contains canonical SKILL.md bytes and the complete served file set. Files starts with SKILL.md, followed by supporting files sorted by path.

func ComputeManifest

func ComputeManifest(ctx context.Context, src SkillSource, sk *registry.AgentSkill) (*Manifest, error)

ComputeManifest hashes all content without publication limits. Cancellation is checked between reads; legacy pin callers use ComputeDigests.

type ManifestFile

type ManifestFile struct {
	Path   string
	Digest string
	Size   int64
}

ManifestFile describes the raw bytes hashed, with an unprefixed SHA-256 digest.

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"`
	// Approvals is the projected history, newest first. The log under
	// pins/approvals/skills/ is the source of truth.
	Approvals []pinapprovals.Entry `json:"approvals,omitempty"`
	// CompositeHash is the approval-binding fingerprint of the pinned digests.
	CompositeHash string `json:"composite_hash,omitempty"`
}

SkillPin holds the pin state for a single skill. Document, Findings, Source, Origin, Approvals, and CompositeHash 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. Approvals is rebuilt from the approvals log on every load. CompositeHash is recomputed from the digests. Dropping either loses nothing.

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(ctx context.Context, name string, src SkillSource, req ApproveRequest) error

Approve re-pins a skill's current content, clearing pin drift, and appends an approval entry. Reason and references are validated before any file write. ExpectedHash, when non-empty, must match the current composite hash or ErrHashMismatch is returned. When the current content carries unresolved advisory findings, a non-empty reason is required (ErrReasonRequired otherwise) and is persisted on the record as ApprovedReason. The approvals log is appended inside the file lock before the pin file is rewritten; a log error leaves the pin file unchanged.

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. History in the approvals log is left untouched, so the fresh pin still shows the prior entries. Reset does not reload from disk: calling it on a store that was never loaded writes an empty file. The daemon's live store uses Reset. Disk resets of a throwaway store use ResetPersisted.

func (*Store) ResetPersisted

func (ps *Store) ResetPersisted(ctx context.Context, name string) (bool, error)

ResetPersisted deletes name from the on-disk pin file and reports whether the record was present. It is for a freshly constructed New(stack) that the caller discards. loadPinFileLocked empties the in-memory map before returning ErrCorrupt or ErrNewerVersion, so this must never be called on the daemon's live store: the next dirty Sync would persist that empty map. The approvals log is not read or written. A missing record returns (false, nil) without saving. A file that fails to load is returned without saving.

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