Documentation
¶
Overview ¶
Package proposals stores staged changes to governed documents: a skill, a context fragment (or AGENTS.md when fragments mode is off), or a hook. A proposal carries a unified diff against the content its author read, the composite hash of that content, a classification, prevalence counts, opaque evidence references, and a reason. Nothing reaches a governed document until a human approves the proposal.
Approve is compare-and-swap on the base hash. On the apply path (a locally authored skill or local fragment whose pin shows no drift) the diff is applied, the target is re-pinned through its own pin store, and that store appends the approvals log entry. On the record path (any imported target, and every hook) only the verdict is written, to the proposal file: no target bytes, no pin store call, and no log entry.
The store is stateless between calls, like pkg/ctxpins: every public method reloads under the proposals file lock. It never fetches evidence, calls a model, or reads transcripts. Proposals are untrusted input until approved; findings from the poisoning scanner are advisory and gate only by requiring a reason.
Index ¶
- Constants
- Variables
- func Annotate(ctx context.Context, p *Proposal, targets Resolver)
- func ApplyStrict(current []byte, p FilePatch) ([]byte, error)
- type ApproveRequest
- type CreateRequest
- type FilePatch
- type Filter
- type Hunk
- type HunkLine
- type ListResult
- type Prevalence
- type Proposal
- type Provenance
- type RejectRequest
- type RepinRequest
- type Resolver
- type Skipped
- type Store
- func (s *Store) Approve(ctx context.Context, id string, req ApproveRequest, targets Resolver) (*Proposal, error)
- func (s *Store) Create(ctx context.Context, req CreateRequest, targets Resolver) (*Proposal, error)
- func (s *Store) Dir() (string, error)
- func (s *Store) Get(ctx context.Context, id string) (*Proposal, error)
- func (s *Store) List(ctx context.Context, f Filter) (ListResult, error)
- func (s *Store) Reject(ctx context.Context, id string, req RejectRequest) (*Proposal, error)
- func (s *Store) SetScanConfig(enabled bool, ignore []string)
- type Target
- type Targets
- type Verdict
- type WriteError
Constants ¶
const ( KindSkill = "skill" KindContext = "context" KindHook = "hook" KindAgent = "agent" )
Target kinds. KindAgent is a recognized token that Create refuses: agents have no pin store yet.
const ( ClassKnowledge = "knowledge" ClassGuarantee = "guarantee" )
Classifications.
const ( StatusPending = "pending" StatusApproved = "approved" StatusRejected = "rejected" )
Proposal status values.
const ( PathApply = "apply" PathRecord = "record" )
Approve paths. PathApply writes the diff and re-pins; PathRecord writes the verdict on the proposal only.
const ( ProvenanceLocal = "local" ProvenanceLocalImport = "local-import" ProvenanceGit = "git" ProvenancePack = "pack" )
Provenance kinds.
const ( PinStatusPinned = "pinned" PinStatusDrift = "drift" PinStatusUnpinned = "unpinned" PinStatusUnknown = "unknown" )
Pin status values computed for show and the REST get. They are never stored.
const ( // MaxDiffBytes caps the stored unified diff. MaxDiffBytes = 256 << 10 // MaxSourceBytes caps the free-form source label. MaxSourceBytes = 256 // MaxListFiles caps how many proposal files List reads. MaxListFiles = 10000 // MaxEvidence caps a proposal's evidence references, including any an // approver adds. The approvals log allows 16; one slot is held for // the synthetic proposal:<id> reference an apply-path approve adds. MaxEvidence = 15 )
Bounds.
const FileVersion = "1"
FileVersion is the proposal file version this binary writes and the highest version it accepts. Advertised by `gridctl info --json` as schemas.proposals.
Variables ¶
var ( ErrCorrupt = errors.New("corrupt proposal file") ErrNewerVersion = errors.New("proposal file written by a newer gridctl") ErrNotFound = errors.New("proposal not found") ErrNotPending = errors.New("proposal is not pending") ErrBaseStale = errors.New("target changed since the proposal's base") ErrTargetDrift = errors.New("target has unreviewed pin drift") ErrReasonRequired = errors.New("approving a proposal with advisory findings requires a reason") ErrKindUnsupported = errors.New("proposal kind is not supported") ErrInvalidInput = errors.New("invalid proposal") ErrDiffUnsupported = errors.New("unsupported diff") ErrTargetNotFound = errors.New("proposal target not found") ErrApplyFailed = errors.New("proposal did not apply") // ErrPinFailed marks an apply-path approve whose writes landed but // whose re-pin failed. The proposal is still marked approved with // verdict.pin_error set. ErrPinFailed = errors.New("proposal applied but re-pinning failed") // a pin store) was not supplied. ErrStoreUnavailable = errors.New("store unavailable") )
Sentinel errors. Callers map them with errors.Is; the wrapped text says what was refused and how to retry.
Functions ¶
func Annotate ¶
Annotate refreshes the computed fields of a loaded proposal without writing: base_current against the target's current hash, and pin_status. A target or pin store that cannot be read leaves base_current as stored and sets pin_status to unknown.
func ApplyStrict ¶
ApplyStrict applies one file patch to current content. current is nil for an added file. CRLF is normalized to LF before matching and the result is LF. Every context and removal line must match exactly at the hunk's stated offset; there is no fuzz and no offset search. A mismatch names the file and hunk index without quoting content.
Types ¶
type ApproveRequest ¶
type ApproveRequest struct {
Reason string
Evidence []string
ApprovedBy string
ApprovedBySource string
}
ApproveRequest is one approval. Reason, when non-empty, replaces the proposal's reason. Evidence is appended to the proposal's evidence. ApprovedBy and ApprovedBySource are already resolved attribution.
type CreateRequest ¶
type CreateRequest struct {
Kind string `json:"kind"`
Target string `json:"target"`
BaseHash string `json:"base_hash"`
Diff string `json:"diff"`
Classification string `json:"classification"`
Prevalence Prevalence `json:"prevalence"`
Evidence []string `json:"evidence"`
Reason string `json:"reason"`
Source string `json:"source"`
CreatedBy string `json:"created_by"`
Principal string `json:"-"`
}
CreateRequest is the JSON create document plus caller attribution. Principal is a verified identity (an OAuth subject) and wins over the declared CreatedBy; it is never read from the document.
type FilePatch ¶
FilePatch is one file section of a unified diff. Path is the cleaned path relative to the target root. New is true for an added file (--- /dev/null).
func ParseUnified ¶
ParseUnified parses a unified diff with one or more file sections. CRLF line endings are normalized to LF first. Git headers that describe a deletion, rename, copy, mode change, or binary patch are refused with ErrDiffUnsupported, as is a section whose new side is /dev/null or whose old and new paths differ. Each path is checked with cleanPatchPath. Text outside a file section other than "diff " and "index " lines is refused with ErrInvalidInput.
type Filter ¶
Filter narrows List. Empty Status or "all" matches every status; empty Kind matches every kind.
type Hunk ¶
Hunk is one @@ section. Starts are 1-based line numbers as written in the header; a zero count means the hunk inserts after OldStart.
type HunkLine ¶
HunkLine is one body line. Op is ' ', '-', or '+'. NoNewline is set when the line was followed by "\ No newline at end of file".
type ListResult ¶
ListResult is the outcome of List. Proposals are sorted pending first, then newest first. Truncated is true when the directory held more than MaxListFiles proposal files.
type Prevalence ¶
Prevalence counts how often the proposer saw the pattern behind the change. Both values are non-negative.
type Proposal ¶
type Proposal struct {
ID string `json:"id"`
Version string `json:"version"`
Kind string `json:"kind"`
Target string `json:"target"`
BaseHash string `json:"base_hash"`
BaseCurrent bool `json:"base_current"`
Diff string `json:"diff"`
Classification string `json:"classification"`
Prevalence Prevalence `json:"prevalence"`
Evidence []string `json:"evidence"`
Reason string `json:"reason,omitempty"`
Source string `json:"source,omitempty"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
CreatedBySource string `json:"created_by_source,omitempty"`
Status string `json:"status"`
Path string `json:"path"`
Provenance Provenance `json:"provenance"`
Findings []pins.Finding `json:"findings"`
Verdict *Verdict `json:"verdict"`
Files []string `json:"files"`
PinStatus string `json:"pin_status,omitempty"`
}
Proposal is one staged change, stored at ~/.gridctl/proposals/<id>.json. PinStatus is computed for display and is never written to the file.
type Provenance ¶
type Provenance struct {
Kind string `json:"kind"`
Pack string `json:"pack,omitempty"`
SourceDir string `json:"source_dir,omitempty"`
}
Provenance says where the target came from, which decides the path. Pack is set for ProvenancePack when the pack name is known. SourceDir is set for ProvenanceLocalImport.
type RejectRequest ¶
RejectRequest is one rejection. Reason is required.
type RepinRequest ¶
type RepinRequest struct {
ExpectedHash string
Reason string
Evidence []string
ApprovedBy string
ApprovedBySource string
}
RepinRequest carries the approval written to the pin store on the apply path. ExpectedHash is the post-apply composite hash.
type Skipped ¶
Skipped is a proposal file List could not load. Category is one of corrupt, newer_version, permission, or unreadable. Contents are never reported.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store holds proposals, one JSON file each. It keeps only its directory and scan settings; every public method reloads under state.WithLock.
func New ¶
func New() *Store
New creates a Store for ~/.gridctl/proposals. The directory is resolved on first use. Scan settings default to ctxpins.DeployedScanConfig so a deployed stack's scan_ignore applies.
func NewWithPath ¶
NewWithPath creates a Store rooted at dir with the scanner on and no ignores. Intended for tests; it never resolves the real home.
func (*Store) Approve ¶
func (s *Store) Approve(ctx context.Context, id string, req ApproveRequest, targets Resolver) (*Proposal, error)
Approve approves a pending proposal. It refuses ErrBaseStale when the target's current composite hash differs from the base, ErrReasonRequired when findings exist and no reason is available, and, on the apply path, ErrTargetDrift when the target's pin shows drift. The apply path writes the diff, re-pins through the target's pin store with the proposal's reason and evidence plus proposal:<id>, and marks the proposal approved. The record path writes only the verdict. The proposal file is rewritten only after target writes and the pin store call have returned. A re-pin failure after a successful write returns the approved proposal together with an error wrapping ErrPinFailed.
func (*Store) Create ¶
Create validates a create document, computes findings, path, and provenance against the current target, and stores the proposal. Every refusal happens before any file is written. A diff that does not apply to a current base is refused with ErrApplyFailed; a stale base is stored with base_current false.
func (*Store) List ¶
List reads up to MaxListFiles proposal files, applies f, and sorts pending first, then newest first. A file that fails to load is reported in Skipped by category and does not stop the listing.
func (*Store) Reject ¶
Reject records a rejection. The reason is required and validated as an approval reason. Nothing is written to the target or any approvals log.
func (*Store) SetScanConfig ¶
SetScanConfig configures the advisory scanner used for findings.
type Target ¶
type Target interface {
Kind() string
Name() string
// Stack is the skill pin stack, or empty for stackless kinds.
Stack() string
// CurrentHash is the composite hash of the current content, in the
// form the kind's pin store produces.
CurrentHash(ctx context.Context) (string, error)
// PinStatus is pinned, drift, or unpinned.
PinStatus(ctx context.Context) (string, error)
// ReadFiles validates each path against the kind's rules and returns
// the current bytes of those that exist. A missing key is an absent
// file. A refused path wraps ErrDiffUnsupported.
ReadFiles(ctx context.Context, paths []string) (map[string][]byte, error)
Provenance(ctx context.Context) (Provenance, error)
// Scan returns advisory findings for the post-apply content.
Scan(ctx context.Context, applied map[string][]byte, enabled bool, ignore []string) ([]pins.Finding, error)
// PostApplyHash parses the post-apply content and returns the
// composite hash the pin store will compute after Write, including
// any render normalization the write performs. A parse or validation
// failure wraps ErrApplyFailed.
PostApplyHash(ctx context.Context, applied map[string][]byte) (string, error)
// Write writes the applied files in order. On failure the returned
// restore puts back every file it touched and reports which files
// were restored and which are still changed.
Write(ctx context.Context, applied map[string][]byte) (restore func() (restored, stillChanged []string), err error)
// Repin re-pins the target and appends the approvals log entry
// through the kind's pin store.
Repin(ctx context.Context, req RepinRequest) error
}
Target is one governed document as the store sees it. Implementations keep the store kind-agnostic. Paths are slash-separated and relative to the target root (the skill directory, the fragment file's directory, or the hook directory).
type Targets ¶
type Targets struct {
Registry *registry.Store
SkillPins *skillpins.Store
Stack string
LockPath string
Context *contexts.Manager
ContextPins *ctxpins.Store
HookPins *hookpins.Store
}
Targets resolves targets from the stores the caller already built. The CLI and the API fill it; pkg/proposals never constructs a store that resolves the real home. Registry and SkillPins must be loaded. LockPath is skills.lock.yaml; empty skips lockfile provenance.
type Verdict ¶
type Verdict struct {
At time.Time `json:"at"`
By string `json:"by,omitempty"`
BySource string `json:"by_source,omitempty"`
Reason string `json:"reason,omitempty"`
Evidence []string `json:"evidence,omitempty"`
Applied bool `json:"applied"`
ApprovedHash string `json:"approved_hash,omitempty"`
NewHash string `json:"new_hash,omitempty"`
Stack string `json:"stack,omitempty"`
PinError string `json:"pin_error,omitempty"`
}
Verdict records an approve or reject. ApprovedHash is the base hash the verdict bound to. NewHash is the post-apply composite hash on the apply path. Stack is the skill pin stack an apply-path skill approval used. PinError is set when the target was written but re-pinning failed.
type WriteError ¶
WriteError is the ErrApplyFailed returned when a target write fails after the compare-and-swap. Restored lists files put back to their prior bytes; StillChanged lists files the restore could not put back.
func (*WriteError) Error ¶
func (e *WriteError) Error() string
func (*WriteError) RestoreFailed ¶
func (e *WriteError) RestoreFailed() bool
RestoreFailed reports whether any file is still changed.
func (*WriteError) Unwrap ¶
func (e *WriteError) Unwrap() []error
Unwrap lets errors.Is match ErrApplyFailed and the write's own cause, such as context.Canceled or fs.ErrPermission.