permission

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package permission evaluates whether a tool call is allowed to run without asking, must ask the user first, or is blocked outright — a deterministic policy layer between the model deciding to call a tool and the tool actually running. This is distinct from toolsettings (enabled/disabled): disabling a tool removes it entirely, while a permission policy can allow a tool in general but ask/deny specific uses of it (e.g. "bash: allow `go test *`, ask before `git push*`").

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SavePolicy

func SavePolicy(pf PolicyFile, path string) error

SavePolicy writes pf as JSON to path, replacing any existing file atomically via the shared localstore.AtomicWrite (temp file + rename).

Types

type Decision

type Decision string

Decision is the outcome of evaluating a rule against a tool call.

const (
	Allow Decision = "allow"
	Ask   Decision = "ask"
	Deny  Decision = "deny"
)

type Evaluator

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

Evaluator resolves a Decision for a tool call against a fixed rule set — policy rules plus any persisted "always" grants, evaluated together as one list. It's immutable once built: a grant earned mid-run takes effect on the *next* run (Registry is rebuilt per daemon process, not per turn), which avoids the same prompt-cache-hostile problem Kram's memory snapshotting already solved once — see DECISIONS.md.

func NewEvaluator

func NewEvaluator(policy PolicyFile, grants []Rule) *Evaluator

NewEvaluator builds an Evaluator from a policy file and any grants earned via a prior "always" approval. Grants are appended after the policy's own rules: at equal specificity, later rules win the tie (see Evaluate), so a grant naturally overrides a same-specificity "ask" rule from the policy — approving something "always" is supposed to stop it asking again.

func (*Evaluator) Evaluate

func (e *Evaluator) Evaluate(tool, subject string) Decision

Evaluate returns the decision for one call to tool, whose policy subject (the command, path, or raw args — see policySubject) is subject.

func (*Evaluator) FullyDenied

func (e *Evaluator) FullyDenied(tool string) bool

FullyDenied reports whether every rule mentioning tool says Deny (and there's at least one, or the compatibility default itself is Deny) — meaning no call to this tool could ever be anything but denied. A tool in that state is removed from what the model is offered entirely (see Registry.Definitions), the same treatment a disabled tool gets: a capability the model can never use shouldn't cost tokens in the schema. A *partial* deny (some patterns denied, others allowed/asked) leaves the tool visible, since it's still sometimes usable.

type Grant

type Grant struct {
	Tool      string `json:"tool"`
	Pattern   string `json:"pattern"` // literal subject text, matched exactly (see Rule.matchesSubject)
	CreatedAt int64  `json:"created_at"`
}

Grant is one persisted "always" approval — the exact subject the user approved, not a broadened wildcard. If a user approves `bash` running `git push origin feature/foo`, the grant remembers exactly that command, never "bash: allow *" — a policy engine that silently widens what it remembers approving is more dangerous than one that asks too often.

type GrantStore

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

GrantStore persists grants per project (<workspace>/.kram/permission_grants.json) — deliberately not global: an approval made in one project has no business silently applying to another.

func LoadGrants

func LoadGrants(workspace string) (*GrantStore, error)

LoadGrants reads the project's grant file, or returns an empty store if it doesn't exist yet (the normal case — most projects have never approved anything "always").

func (*GrantStore) Add

func (gs *GrantStore) Add(tool, subject string) error

Add records a new "always" grant and persists it immediately. Takes effect starting with the *next* agent run in this workspace (the Evaluator that gated the call being approved right now was already built for this run — see Evaluator's doc comment for why that's deliberate).

func (*GrantStore) Rules

func (gs *GrantStore) Rules() []Rule

Rules exposes every persisted grant as an Allow rule, ready to feed into NewEvaluator.

type PolicyFile

type PolicyFile struct {
	// Default is the decision when no rule matches at all. Empty means
	// Allow — a fresh install with no policy file behaves exactly like
	// Kram did before this feature existed. This compatibility default is
	// deliberate: shipping this change must never make existing
	// installations start asking about everything.
	Default Decision `json:"default,omitempty"`
	Rules   []Rule   `json:"rules,omitempty"`
}

PolicyFile is the on-disk shape of a permissions.json — global (kramhome/permissions.json) or project (<workspace>/.kram/permissions.json).

func AutonomousPolicy

func AutonomousPolicy() PolicyFile

AutonomousPolicy allows everything except recursive deletes rooted at an absolute path ("rm -rf /*" is a prefix match, so it catches "rm -rf /", "rm -rf /home/x", etc. — anything relative, like "rm -rf node_modules", still runs unprompted). This is one guardrail against catastrophic mistakes, not a broad safety net — callers/UI copy presenting this preset should say so.

func LoadConfig

func LoadConfig(workspace string) PolicyFile

LoadConfig merges the global policy (kramhome/permissions.json) with the project's own (<workspace>/.kram/permissions.json), project rules appended after global ones. Best-effort like mcp.LoadConfig and toolsettings.Load: a missing file is the normal case (most projects have no custom policy) and a malformed one must not stop the daemon from starting — it just contributes nothing.

func RecommendedPolicy

func RecommendedPolicy() PolicyFile

RecommendedPolicy asks before the handful of operations that are genuinely hard to undo (recursive delete, force-pushing, removing or moving a file) and otherwise allows everyday work — the first-run wizard's suggested default.

func StrictPolicy

func StrictPolicy() PolicyFile

StrictPolicy asks before anything not explicitly allow-listed — because Default is Ask, this includes every tool call this policy doesn't otherwise mention, MCP tools included, not just the tool *names* enumerated here.

type Rule

type Rule struct {
	Tool     string   `json:"tool"`
	Pattern  string   `json:"pattern,omitempty"`
	Decision Decision `json:"decision"`
}

Rule matches a tool call and says what to do with it. Tool is either an exact tool name ("bash", "delete_file") or a "*"-suffixed prefix glob ("mcp__github__*", "mcp__*") — the only wildcard form supported, which is enough to cover MCP's real `mcp__<server>__<tool>` namespacing without inventing categories the code doesn't actually have. Pattern is matched against the tool's "subject" (see policySubject in internal/daemon/tools/tools.go — bash's command, a file tool's path, everything else's raw argument JSON as a fallback): "" or "*" matches any subject, a "*"-suffixed string is a prefix glob, anything else must match exactly (this last form is what a persisted "always" grant uses — see grants.go — so approving one exact command never silently broadens into approving a whole prefix the user never saw).

Jump to

Keyboard shortcuts

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