Help us shape the future of Go! Take the 2026 Go Developer Survey

policy

package
v0.2.11 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package policy implements policy-as-code (design 013 §1): governance rules as a versioned file in the repo (.rysh/policy.yaml), loaded at session start, optionally constrained by an org-level policy file (config `policy.org_file` or RYSH_ORG_POLICY).

The format is open (it also underpins CI mode, design 009). Org-level central *distribution* is the private/enterprise half and is out of scope here — this package only consumes an org file already on disk.

Org merge (Merge) is strictest-wins: a project file may narrow the org policy but can never loosen it. Per rule class:

  • approval.always_gate / bash.deny: org rules are evaluated before any project rule, so anything the org gates or denies stays gated/denied no matter what the project file says;
  • approval.auto_approve / bash.allow (permissive): the lists intersect — when both files are present a call is auto-approved only if BOTH allow it; with only an org file, its permissive rules stand alone;
  • budget.ceilings: the lower ceiling wins per pane id;
  • proxy.required: required by either file ⇒ required;
  • snat.required: the union of both files' required secrets.

Fail-closed: a policy file that exists but fails to parse is NOT silently ignored — Load returns a Policy whose Err is set and whose rule sets are empty, so nothing is auto-approved on a broken policy. Callers surface the error loudly (##policy) rather than running as if governed. The org file is held to a stricter standard: one that is configured but MISSING also fails closed (LoadOrg) — a governed session silently losing its org policy is a tamper signal, not "no rules".

Index

Constants

View Source
const FileName = "policy.yaml"

FileName is the repo-level policy file under .rysh/.

Variables

This section is empty.

Functions

func Blocked

func Blocked() (string, bool)

Blocked reports whether governed execution must be refused, and why.

Callers must fail closed: refuse the prompt and tell the user. Shell execution and ## commands are intentionally NOT gated on this, so the operator can repair the file and run ##policy reload.

func BlockedMessage

func BlockedMessage(reason string) string

BlockedMessage renders the standard refusal shown in a pane. The reason is cause-specific (a parse failure or a missing snat.required secret); the footer stays generic so it reads correctly for either.

func ClearBlocked

func ClearBlocked()

ClearBlocked lifts the block after a policy file parses cleanly.

func SetBlocked

func SetBlocked(reason string)

SetBlocked marks governed execution as refused, with a human-readable reason that dispatch sites surface to the user.

Types

type ApprovalAction

type ApprovalAction int

ApprovalAction is a policy verdict for a tool call.

const (
	// ApprovalDefault — no rule matched; use the tool's own classifier.
	ApprovalDefault ApprovalAction = iota
	// ApprovalAuto — a permissive rule matched; skip approval.
	ApprovalAuto
	// ApprovalGate — a restrictive rule matched; force approval.
	ApprovalGate
)

type Policy

type Policy struct {
	// Bash allowlist adjustments layered over the built-in classifier.
	BashAllow []string
	BashDeny  []string
	// Approval rules: glob/pattern lists.
	AutoApprove []string
	AlwaysGate  []string
	// BudgetCeilings maps a pane/agent id to a hard token ceiling.
	BudgetCeilings map[string]int64
	// ProxyRequired forces the governance proxy on and blocks ##proxy off.
	ProxyRequired bool
	// ProxyStrict escalates the ungoverned-CLI WARNING (design 022 §4.4) to a
	// block: a known agent CLI that runs past the grace window with no governed
	// request is stopped.
	//
	// Separate from ProxyRequired on purpose (022 §8.2). "The proxy must be
	// running" is a fact about rysh and is always safe to enforce. "This CLI is
	// escaping the proxy" is NEGATIVE evidence — an idle CLI is indistinguishable
	// from an escaped one — so acting on it can stop innocent work, and that is
	// a decision an operator has to make deliberately.
	ProxyStrict bool
	// SNATRequired names registered secrets that must be present.
	SNATRequired []string

	// Org overlay (design 013 §1), populated by Merge when an org policy is
	// active. Restrictive org rules are evaluated before any project rule;
	// permissive org lists intersect with the project's — see
	// EvaluateToolApproval.
	OrgBashAllow   []string
	OrgBashDeny    []string
	OrgAutoApprove []string
	OrgAlwaysGate  []string

	// Provenance / status.
	Path      string // project policy file ("" if none)
	Loaded    bool   // a policy file existed and parsed
	Err       error  // set when a policy could not be loaded (fail-closed)
	OrgPath   string // org policy file ("" when no org policy is configured)
	OrgActive bool   // an org policy parsed and its constraints are merged in
	// contains filtered or unexported fields
}

Policy is the resolved rule set.

func Empty

func Empty() *Policy

Empty returns an empty policy (no file present).

func Load

func Load(ryshDir string) *Policy

Load reads .rysh/policy.yaml. A missing file yields an empty policy (not an error). A present-but-unparseable file yields an empty policy with Err set (fail-closed): the caller must surface Err and must not treat the session as governed.

func LoadOrg

func LoadOrg(path string) *Policy

LoadOrg reads the org-level policy file named by config `policy.org_file` / RYSH_ORG_POLICY. An empty path means no org policy is configured: nil is returned and Merge leaves the project policy untouched (pre-org behavior).

Stricter than Load: a configured org file that is MISSING is an error, not an empty policy — an absent org policy is a tamper signal (the file the org mandates was deleted or diverted), so it fails closed exactly like an unparseable one.

func Merge

func Merge(org, project *Policy) *Policy

Merge layers an org policy over the project policy, strictest-wins (the package comment states the per-class rules). org == nil means no org policy is configured: project is returned unchanged, so the not-configured path behaves exactly as before the org feature existed.

A load error on either side poisons the merge: the result carries Err (and the failing file's Path) with no rules, so the fail-closed gate engages. A broken project file must not drop the org constraints any more than a broken org file may run ungoverned.

func (*Policy) EvaluateToolApproval

func (p *Policy) EvaluateToolApproval(toolName string, input json.RawMessage) (ApprovalAction, string)

EvaluateToolApproval matches a tool call against the four rule classes and returns the verdict plus the matched rule ID (e.g. "bash.deny[1]"; org rules cite an "org." prefix, e.g. "org.bash.deny[0]", so the audit line names the file the rule came from).

Restrictive-wins: always_gate / bash.deny are evaluated before auto_approve / bash.allow, so a permissive rule can never silently un-gate a call the operator also listed as gated. With an org policy merged in (OrgActive), org restrictive rules are evaluated before everything else — nothing in the project file can override them — and permissive rules intersect: both files must allow, so the project can narrow the org allowlist but never widen it (the cited ID then names both concurring rules). Tool-name rule classes (approval.*) match the tool name; command rule classes (bash.*) match the bash command string (bash / bash_background). Globs are whole-string with '*' as the only wildcard — use "*x*" for a substring match (so a deny survives compound commands like `cd /t && rm -rf x`).

func (*Policy) Summary

func (p *Policy) Summary() string

Summary renders a human-readable overview for ##policy.

Jump to

Keyboard shortcuts

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