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.
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 ¶
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 ¶
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).