Documentation
¶
Overview ¶
Package ctxpins implements TOFU content pins for global context: one record per rule fragment, or the single canonical AGENTS.md when fragments mode is off. A pin records a content digest; 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 projection.
The store is the stackless sibling of pkg/skillpins. It borrows that package's sentinel errors, lock, atomic save, and re-pin conventions, and reuses pkg/pins scanner types. It does not keep a Load-once copy of the pin file: every public method reloads under the file lock, so a CLI write is visible to the API server on its next call. Only hash and status are load-bearing. Document, source, pack, approved reason, findings, and the projected approvals list are derived; an older gridctl rewriting the file drops them without a file-version bump.
Hashes use the same canonical normalization as the projection lockfile (CRLF to LF, trailing newlines trimmed) with a c1: prefix. The lockfile stores sha256:-prefixed digests, so parity is the unprefixed digest. Pin drift is a fact about the source, distinct from client-side stale and drifted projection state.
Index ¶
- Constants
- Variables
- func CompositeHash(hash string) string
- func DeployedScanConfig() (bool, []string)
- func Hash(raw []byte) string
- func PinStatus(pin *FragmentPin, doc Document) string
- type ApproveRequest
- type Diff
- type DocSource
- type Document
- type FragmentPin
- type PinFile
- type Source
- type Store
- func (s *Store) Approve(ctx context.Context, name string, src Source, req ApproveRequest) error
- func (s *Store) Get(ctx context.Context, name string) (*FragmentPin, bool, error)
- func (s *Store) GetAll(ctx context.Context) (map[string]*FragmentPin, error)
- func (s *Store) Rename(ctx context.Context, from, to string) error
- func (s *Store) Reset(ctx context.Context, name string) error
- func (s *Store) SetScanConfig(enabled bool, ignore []string)
- func (s *Store) Sync(ctx context.Context, src Source) (*SyncResult, error)
- func (s *Store) Verify(ctx context.Context, name string, src Source) (*VerifyResult, error)
- type Summary
- type SyncResult
- type VerifyResult
Constants ¶
const ( StatusPinned = "pinned" StatusDrift = "drift" )
Status values for FragmentPin, matching pkg/pins' vocabulary.
const ( SourceLocal = "local" SourcePack = "pack" )
Source discriminants for FragmentPin.Source. Unknown values load fine and pass through untouched.
const CanonicalName = "AGENTS.md"
CanonicalName is the reserved record key for the single-file store.
const FileVersion = "1"
FileVersion is the context pin file version this binary writes and the highest version it accepts. Advertised by `gridctl info --json` as schemas.context_pins.
Variables ¶
var ( ErrCorrupt = errors.New("corrupt context pin file") ErrNewerVersion = errors.New("context pin file written by a newer gridctl") ErrNotPinned = errors.New("fragment is not pinned") ErrNoDocument = errors.New("fragment does not exist") ErrHashMismatch = errors.New("fragment content changed since the reviewed diff") ErrReasonRequired = errors.New("approving a fragment with unresolved findings requires a reason") )
Sentinel errors. Load's ErrCorrupt and ErrNewerVersion follow the pkg/skillpins contract: a corrupt file is reported and not rewritten; ErrNewerVersion must never be papered over. The rest are decision errors so the CLI and API can map them without string matching.
Functions ¶
func CompositeHash ¶
CompositeHash folds one fragment hash into the approval-binding fingerprint shared with skill pins. --expect values are comparable across kinds because both call skillpins.CompositeHash.
func DeployedScanConfig ¶
DeployedScanConfig reads gateway.security.schema_pinning scan and scan_ignore from the single deployed stack. Zero stacks, more than one stack, or a stack that does not load all mean the defaults: scan on, no ignores. A nil Scan pointer is on. This never returns an error; stack resolution failure must not fail a ctx command.
func Hash ¶
Hash returns the pin digest of raw fragment bytes. Normalization matches the projection lockfile's canonical content hash (CRLF to LF, trailing newlines trimmed) so stale and pin drift agree on whether content changed.
func PinStatus ¶
func PinStatus(pin *FragmentPin, doc Document) string
PinStatus reports pinned, drift, or unpinned for one document against an optional stored record. It does not write. An unreadable pinned document is drift.
Types ¶
type ApproveRequest ¶
type ApproveRequest struct {
ExpectedHash string
Reason string
Evidence []string
ApprovedBy string
ApprovedBySource string
}
ApproveRequest is one explicit fragment 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 Diff ¶
type Diff struct {
Name string `json:"name"`
OldHash string `json:"old_hash"`
NewHash string `json:"new_hash"`
OldDocument string `json:"old_document,omitempty"`
NewDocument string `json:"new_document,omitempty"`
Findings []pins.Finding `json:"findings"`
}
Diff describes how a fragment moved since its pin. Findings are advisory results for the new content, computed at verify time. Findings is never nil when Diff itself is non-nil.
func (*Diff) DocumentChanged ¶
DocumentChanged reports whether the pinned digest moved.
type DocSource ¶
type DocSource struct {
Docs []Document
}
DocSource is a Source backed by an already-loaded document list.
func (DocSource) GetDocument ¶
GetDocument returns the named document. A missing name wraps ErrNoDocument. A document with ReadErr set is returned as-is so the store can fail closed instead of treating an unreadable file as absent.
type Document ¶
type Document struct {
Name string
Description string
Body string
Raw []byte
Pack string
ReadErr error
}
Document is one fragment (or the canonical AGENTS.md) as the pin store sees it. Pack is filled by the CLI and API adapters from the skills lockfile; PinDocuments leaves it empty. ReadErr is a per-document read or parse failure. Callers must not turn that into a whole-list error: an unreadable fragment fails closed for itself and must not skip pinning for every other fragment.
func AnnotatePack ¶
AnnotatePack sets Document.Pack when the skills lockfile lists the fragment name under a pack source's Rules or RuleFiles. A lockfile read error leaves provenance empty and logs at DEBUG. The first pack name wins when more than one source lists the same fragment.
type FragmentPin ¶
type FragmentPin struct {
Hash string `json:"hash"`
Document string `json:"document,omitempty"`
Source string `json:"source,omitempty"`
Pack string `json:"pack,omitempty"`
ApprovedReason string `json:"approved_reason,omitempty"`
PinnedAt time.Time `json:"pinned_at"`
LastVerifiedAt time.Time `json:"last_verified_at"`
Status string `json:"status"`
Findings []pins.Finding `json:"findings,omitempty"`
Approvals []pinapprovals.Entry `json:"approvals,omitempty"`
}
FragmentPin holds the pin state for one fragment, or for AGENTS.md in single-file mode. Document, Source, Pack, ApprovedReason, Findings, and Approvals are derived. Approvals is rebuilt from the approvals log on every load. Only Hash and Status decide drift.
type PinFile ¶
type PinFile struct {
Version string `json:"version"`
Fragments map[string]*FragmentPin `json:"fragments"`
}
PinFile is the top-level JSON structure stored at ~/.gridctl/pins/context/global.json.
type Source ¶
type Source interface {
ListDocuments(ctx context.Context) ([]Document, error)
GetDocument(ctx context.Context, name string) (Document, error)
}
Source is the read surface Sync, Verify, and Approve need. Defining it here keeps pkg/ctxpins free of pkg/contexts.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store manages TOFU content pins for the global context. It holds only its path and scan config. Every public method reloads the pin file under state.WithLock, so two Store values on the same path never disagree.
func New ¶
func New() *Store
New creates a Store for ~/.gridctl/pins/context/global.json. The path is resolved on first use so construction stays error-free; resolution fails only when no home directory is available.
func NewWithPath ¶
NewWithPath creates a Store that keeps pins in dir/global.json. Intended for tests; it never resolves the real home.
func (*Store) Approve ¶
Approve re-pins the current content and appends an approval entry. A present fragment with no record is pinned by this approval, matching skill pins: the call is a review of current content, not a check that a record already exists. Absent fragments return ErrNoDocument from the source. The log is written inside the file lock before the pin file. A log error leaves the pin file unchanged. Reset does not touch the log.
func (*Store) Get ¶
Get returns a deep copy of one pin. The bool is false when the name is not pinned. A corrupt or newer pin file is returned as an error and is not rewritten.
func (*Store) GetAll ¶
GetAll returns a deep copy of every pin, keyed by fragment name. The map is empty when the file is absent. A corrupt or newer file is an error.
func (*Store) Rename ¶
Rename moves a record from one fragment name to another when from exists and to does not. Any other condition is a no-op that returns nil. Used by the fragments-mode migration, which preserves the digest because the written bytes normalize to the same hash.
func (*Store) Reset ¶
Reset deletes the pin record. The approvals log is left untouched, so a later re-pin still shows the prior entries.
func (*Store) SetScanConfig ¶
SetScanConfig configures the advisory poisoning scanner. enabled toggles it; ignore suppresses findings by code. Scanning never affects the hash or status. Nil Scan on a deployed stack means on; callers pass that resolved bool.
func (*Store) Sync ¶
Sync pins unseen documents, verifies the rest, flips status on change, and lists missing pinned names without pruning. It saves once, and only when something changed. A document with ReadErr set is drift when pinned and skipped when unpinned.
type Summary ¶
Summary is the read-only pin picture computed from stored records and the current documents. Arrays are non-nil.
func Summarize ¶
func Summarize(records map[string]*FragmentPin, docs []Document) Summary
Summarize compares stored pins with the current documents. It does not write. An unreadable pinned document is drift. An unreadable unpinned document is omitted: there is nothing trustworthy to call unpinned or missing. Slices are non-nil and sorted.
type SyncResult ¶
SyncResult summarizes one TOFU/verify pass. Slices are sorted.