proposals

package
v1.0.0-rc.6 Latest Latest
Warning

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

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

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

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

View Source
const (
	ClassKnowledge = "knowledge"
	ClassGuarantee = "guarantee"
)

Classifications.

View Source
const (
	StatusPending  = "pending"
	StatusApproved = "approved"
	StatusRejected = "rejected"
)

Proposal status values.

View Source
const (
	PathApply  = "apply"
	PathRecord = "record"
)

Approve paths. PathApply writes the diff and re-pins; PathRecord writes the verdict on the proposal only.

View Source
const (
	ProvenanceLocal       = "local"
	ProvenanceLocalImport = "local-import"
	ProvenanceGit         = "git"
	ProvenancePack        = "pack"
)

Provenance kinds.

View Source
const (
	PinStatusPinned   = "pinned"
	PinStatusDrift    = "drift"
	PinStatusUnpinned = "unpinned"
	PinStatusUnknown  = "unknown"
)

Pin status values computed for show and the REST get. They are never stored.

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

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

View Source
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")
	// ErrStoreUnavailable means a store the target needs (the registry or
	// 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

func Annotate(ctx context.Context, p *Proposal, targets Resolver)

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

func ApplyStrict(current []byte, p FilePatch) ([]byte, error)

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

type FilePatch struct {
	Path  string
	New   bool
	Hunks []Hunk
}

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

func ParseUnified(text string) ([]FilePatch, error)

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

type Filter struct {
	Status string
	Kind   string
}

Filter narrows List. Empty Status or "all" matches every status; empty Kind matches every kind.

type Hunk

type Hunk struct {
	OldStart, OldLines int
	NewStart, NewLines int
	Lines              []HunkLine
}

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

type HunkLine struct {
	Op        byte
	Text      string
	NoNewline bool
}

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

type ListResult struct {
	Proposals []*Proposal
	Skipped   []Skipped
	Truncated bool
}

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

type Prevalence struct {
	Sessions int `json:"sessions"`
	Runs     int `json:"runs"`
}

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

type RejectRequest struct {
	Reason           string
	ApprovedBy       string
	ApprovedBySource string
}

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 Resolver

type Resolver interface {
	Resolve(ctx context.Context, kind, name string) (Target, error)
}

Resolver builds the Target for a kind and name.

type Skipped

type Skipped struct {
	Path     string `json:"path"`
	Category string `json:"category"`
}

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

func NewWithPath(dir string) *Store

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

func (s *Store) Create(ctx context.Context, req CreateRequest, targets Resolver) (*Proposal, error)

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

func (s *Store) Dir() (string, error)

Dir returns the proposals directory, resolving it if needed.

func (*Store) Get

func (s *Store) Get(ctx context.Context, id string) (*Proposal, error)

Get loads one proposal. An unknown or malformed id wraps ErrNotFound.

func (*Store) List

func (s *Store) List(ctx context.Context, f Filter) (ListResult, error)

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

func (s *Store) Reject(ctx context.Context, id string, req RejectRequest) (*Proposal, error)

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

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

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.

func (*Targets) Resolve

func (t *Targets) Resolve(ctx context.Context, kind, name string) (Target, error)

Resolve validates the name for its kind and checks that the target exists. A missing target wraps ErrTargetNotFound; an invalid name wraps ErrInvalidInput.

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

type WriteError struct {
	ID           string
	File         string
	Err          error
	Restored     []string
	StillChanged []string
}

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.

Jump to

Keyboard shortcuts

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