guardrail

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package guardrail evaluates declarative rules over a worktree diff and returns the tripped rules. Rules are data (globs + thresholds + RE2 patterns), never code, so growing from 2 rules to 20 is a config change. Evaluation is pure and allocation-light.

THE NON-ECHO INVARIANT (security, frozen — P5-design.md §1.1): no hit Message, default or computed, ever contains matched line content or the matched token. Content rules (added_pattern, entropy) report the rule's message plus file + line + a match COUNT only. This is what keeps secrets out of the SSE stream, the JSON API, wtd's logs, and desktop notifications.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GlobMatch

func GlobMatch(pattern, path string) bool

GlobMatch reports whether pattern matches path. Supports:

  • "*" matches any run of non-slash characters
  • "**" matches any run of characters including slashes
  • "?" matches a single non-slash character

It is anchored (must match the whole path).

Matching runs a bounded dynamic-programming scan over (pattern tokens × path bytes) — O(len(pattern)*len(path)) time, never exponential. This replaced a recursive "try every split point at every '*'/'**'" matcher: against an adversarial pattern (repeated "**a", or even repeated "*a") matched against a long non-matching path, that matcher ran superpolynomially — the same catastrophic-backtracking class RE2 was chosen to avoid for added_pattern regexes, left open here until this fix (BLOCKER-1). A pack's globs are semi-trusted (checked-in .wtcockpit.toml) input evaluated on every refresh; Eval must never be able to wedge on one.

Types

type Effective added in v0.5.0

type Effective struct {
	WorktreeID string           `json:"worktreeId"`
	RepoPath   string           `json:"repoPath"`
	PackPath   string           `json:"packPath"`   // "" when no pack file is present
	PackStatus string           `json:"packStatus"` // "none" | "ok" | "error: <msg>"
	Rules      []RuleWithSource `json:"rules"`
}

Effective is GET /api/rules's payload and `wt rules --json`'s frozen output: the fully-resolved rule set for one worktree's owning repo, with enough pack metadata to make precedence confusion debuggable in one command (P5-design.md §1.3).

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine holds compiled, validated rules. Build with Compile.

func Compile added in v0.5.0

func Compile(rules []Rule) (*Engine, error)

Compile validates rules and returns a ready-to-Eval Engine. It replaces the old can't-fail New: growing the rule language to include regex patterns and threshold combinations means a hand-edited (or agent-authored) rule pack can now be self-contradictory or fail to compile, and that must be caught at load time with a message naming the offending rule — the same "typo fails fast" philosophy config.Load already applies to the TOML shape itself.

Validated, in order: severity enum; path_glob/path_globs mutual exclusion; per-file vs worktree-wide condition mixing (including exclude_globs on a worktree-wide rule); min_token_entropy requires min_token_len >= 8; added_pattern must compile as RE2; rule names must be unique once blank names are auto-filled ("rule-<1-based position>").

func (*Engine) Eval

func (e *Engine) Eval(d model.Diff) []model.GuardrailHit

Eval returns every guardrail hit for the given diff. Per-file rules emit one hit per matching file (at most one, even when a content condition matches several lines — see scanContent); worktree-wide rules emit a single hit with an empty File.

func (*Engine) Rules

func (e *Engine) Rules() []Rule

Rules returns the configured rules (for display/debugging), in their final auto-named form.

type Pack added in v0.5.0

type Pack struct {
	DisableRules []string `toml:"disable_rules"`
	Rules        []Rule   `toml:"rules"`
}

Pack is a per-repo rule-pack file: a checked-in .wtcockpit.toml at a repo's root (P5-design.md §1.3). Rules use the exact same shape (and TOML tags) as config.Config's own [[rules]] table.

func ParsePack added in v0.5.0

func ParsePack(data []byte) (Pack, error)

ParsePack decodes pack TOML bytes strictly: malformed TOML or an unrecognised key (top-level or inside a [[rules]] entry) is an error — mirroring config.Load's own "a typo in a hand-edited guardrails file is a real footgun otherwise" philosophy. The caller (Resolver) is what turns a parse error into the fail-closed-to-global behaviour; ParsePack itself just reports it.

type Resolver added in v0.5.0

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver resolves the effective rule engine for a repo, folding in that repo's optional .wtcockpit.toml pack. It holds the compiled global engine (config [[rules]] or DefaultRules()) plus a per-repo cache invalidated by the pack file's mtime+size — one os.Stat per worktree per refresh, negligible (P5-design.md §1.3's "Mechanics").

func NewResolver added in v0.5.0

func NewResolver(globalRules []Rule, source string) (*Resolver, error)

NewResolver compiles the global rule set once (source is "default" or "global", for Effective's provenance tag) and returns a Resolver ready for For/Effective. An error here means the global rules themselves are invalid; config.Load already validates via Compile before this is ever called in production, so this is a defensive check, not a load-bearing one.

func (*Resolver) Effective added in v0.5.0

func (r *Resolver) Effective(worktreeID, repoPath string) Effective

Effective returns worktreeID's owning repo's fully-resolved, provenance- tagged rule set — GET /api/rules and `wt rules`'s payload.

func (*Resolver) For added in v0.5.0

func (r *Resolver) For(repoPath string) *Engine

For returns the compiled rule engine to evaluate a diff from repoPath against: the global engine when no pack exists there, or the pack-merged engine otherwise. A malformed pack fails closed to the global engine — this never returns nil and never disables guardrails outright.

func (*Resolver) Stats added in v0.5.0

func (r *Resolver) Stats() (loaded, errs int)

Stats reports how many currently-cached repos are running a validly-loaded pack ("loaded") vs a malformed one that fell back to global ("errors") — the source for statusPayload's additive `rulePacks` field.

type Rule

type Rule struct {
	Name     string `json:"name" toml:"name"`         // auto-filled "rule-<n>" when empty; duplicates = load error
	Severity string `json:"severity" toml:"severity"` // "" | "warn" | "danger" — validated at Compile
	Message  string `json:"message" toml:"message"`

	// Per-file conditions: all conditions a rule sets must hold for a file to
	// trip it. A rule whose only set field is ExcludeGlobs has no real
	// condition and so matches nothing (hasFileCond).
	PathGlob        string   `json:"pathGlob,omitempty" toml:"path_glob"`                // e.g. "migrations/**"; mutually exclusive with PathGlobs
	PathGlobs       []string `json:"pathGlobs,omitempty" toml:"path_globs"`              // any-of
	ExcludeGlobs    []string `json:"excludeGlobs,omitempty" toml:"exclude_globs"`        // exemption: a matching file skips this rule
	Status          string   `json:"status,omitempty" toml:"status"`                     // "modified"|"added"|"deleted"|"renamed"
	Binary          bool     `json:"binary,omitempty" toml:"binary"`                     // file must be a binary diff
	MinNetDeleted   int      `json:"minNetDeleted,omitempty" toml:"min_net_deleted"`     // del-add >= N in one file
	MinChangedLines int      `json:"minChangedLines,omitempty" toml:"min_changed_lines"` // add+del >= N in one file
	AddedPattern    string   `json:"addedPattern,omitempty" toml:"added_pattern"`        // RE2 over added lines' content
	MinTokenEntropy float64  `json:"minTokenEntropy,omitempty" toml:"min_token_entropy"` // bits/char threshold; requires MinTokenLen >= 8
	MinTokenLen     int      `json:"minTokenLen,omitempty" toml:"min_token_len"`

	// Worktree-wide conditions: a rule using any of these must not also set a
	// per-file condition above (Compile rejects the mix). All that are set on
	// one rule are ANDed, same as the per-file conditions.
	MinDeleteAddRatio float64 `json:"minDeleteAddRatio,omitempty" toml:"min_delete_add_ratio"` // del >= ratio*add
	MinFilesChanged   int     `json:"minFilesChanged,omitempty" toml:"min_files_changed"`      // len(files) >= N
	MinTotalChanged   int     `json:"minTotalChanged,omitempty" toml:"min_total_changed"`      // sum(add+del) >= N
}

Rule is one declarative guardrail. A rule may combine several per-file conditions (ANDed) — see hasFileCond — or exactly one worktree-wide class (ratio / files-changed / total-churn); mixing the two classes on one rule is a Compile error.

func DefaultRules

func DefaultRules() []Rule

DefaultRules is the sensible starter set shipped when no config is present (P5-design.md §1.2). The v0.2 six survive under their original names (the "-root" + nested glob pairs collapse into one path_globs list each); the rest are new: secrets (pattern + entropy), lockfile/manifest churn, file-count/total-churn thresholds, a CI-workflow delete, and an added binary file.

type RuleWithSource added in v0.5.0

type RuleWithSource struct {
	Rule
	Source string `json:"source"` // "default" | "global" | "pack"
}

RuleWithSource is one entry of an Effective payload: a Rule plus where it came from.

func Merge added in v0.5.0

func Merge(global []Rule, globalSource string, pack Pack) []RuleWithSource

Merge computes the effective, provenance-tagged rule list for one repo: global rules (tagged globalSource — "default" or "global") minus any name in pack.DisableRules, plus pack.Rules appended — a pack rule whose name matches a surviving global rule REPLACES it in place, tagged "pack" (the frozen precedence, P5-design.md §1.3). Pure: no I/O, no validation — the caller Compiles the resulting rules (via RuleWithSource.Rule) separately, which is what catches a pack rule that's individually fine but breaks the combined set (e.g. a duplicate name against a rule Merge didn't replace).

A name is only ever replaced-in-place once per Merge call: the first pack rule matching a surviving global name overrides it as before, but a LATER pack rule reusing that same name (a duplicate [[rules]] entry within the pack itself, not an override of anything global) is appended as a second, distinct entry instead of silently overwriting the first — that's what lets the caller's subsequent Compile catch it as a duplicate rule name, exactly like the identical mistake in config.toml's [[rules]] list already does, rather than one pack rule vanishing with no error anywhere.

Jump to

Keyboard shortcuts

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