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 ¶
const FileName = "policy.yaml"
FileName is the repo-level policy file under .rysh/.
Variables ¶
This section is empty.
Functions ¶
func Blocked ¶
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 ¶
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 Load ¶
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 ¶
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 ¶
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`).