Documentation
¶
Overview ¶
Package permissions implements the project-local permissions layer that gates tool calls. Two files carry the rules:
- .yottacode/permissions.json committable, team-shared rules
- .yottacode/permissions.local.json gitignored, personal additions
Both files merge at load time. Each file holds three pattern lists:
{
"permissions": {
"allow": ["Bash(go test *)", "Edit(internal/**)"],
"ask": ["Bash(rm *)"],
"deny": ["Bash(curl * | sh)", "Edit(/etc/**)"]
}
}
Each rule is "<Tool>(<pattern>)". Tool names are capitalized rule prefixes (Bash, Read, Write, Edit, Mkdir, Copy, Move, Delete, List, Glob, Grep, Fetch, Git, Github, Memory, Tests, Rollback) — distinct from internal tool names (run_bash, read_file, …). The mapping lives in tool_targets.go so per-tool descriptor extraction stays in one place.
Github(...) descriptors are the canonical verb name:
read_pr, read_pr_review_context, read_issue, list_open_issues, create_pr, update_pr, add_pr_comment
Wildcards work as usual, so `Github(read_*)` covers every read, `Github(*_pr)` covers every PR verb, and `Github(*)` is the catch-all. Owner/repo scoping is deferred until cross-repo work lands — for now everything resolves against the cwd's git remote.
Pattern semantics:
- Path-typed permissions (Read/Write/Edit/Mkdir/Copy/Move/Delete/List) match against a cwd-relative path via doublestar so `**` works as expected. Absolute patterns (leading "/") match against the resolved absolute path.
- String-typed permissions (Bash/Git/Glob/Grep/Fetch/Tests/Rollback) match against a free-form descriptor (Bash command, joined Git args, glob pattern, URL, …) with `*` = "any sequence" and `?` = "any single char".
Decision precedence: Deny > Allow > Ask > Default. Default means "the tool's own RequiresApproval policy decides" (i.e. the agent loop's pre-existing behavior).
Index ¶
- func DeriveAllowRule(toolName, argsJSON, cwd string, normalize PathNormalizer) (rule string, ok bool)
- func ParseDiffPaths(diff string) []string
- type Decision
- type PathNormalizer
- type Permissions
- func (p *Permissions) AddAllow(rule string) error
- func (p *Permissions) EnsureFiles() error
- func (p *Permissions) Evaluate(toolName, argsJSON string) Decision
- func (p *Permissions) LocalPath() string
- func (p *Permissions) Reload() error
- func (p *Permissions) SharedPath() string
- func (p *Permissions) Snapshot() (deny, allow, ask []Rule)
- type Rule
- type Target
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DeriveAllowRule ¶
func DeriveAllowRule(toolName, argsJSON, cwd string, normalize PathNormalizer) (rule string, ok bool)
DeriveAllowRule produces a sensible "always allow" pattern from a single tool call. Used when the user hits `a` in the approval modal.
Strategy per tool family:
- Bash: take the first argv-token of the command + " *". e.g. `go test ./...` → `Bash(go *)`. Compound commands (multiple segments separated by ;|&&|||) and commands whose first token is in a small "obviously dangerous" set return ok=false — the user should write the rule by hand instead of getting a footgun-wide blanket grant.
- Path-typed tools (Edit/Write/Mkdir/Delete/Read/List): In-cwd descriptors → `Tool(<absolute-cwd>/**)`. The rule documents which working directory it applies to, so a permissions.local.json copied to another project doesn't accidentally grant access there. Out-of-cwd descriptors → `Tool(<parent-dir>/**)`. Lets a single click on a write to `~/Desktop/notes.md` produce `Write(/home/me/Desktop/**)`. Suppressed when the parent dir is `/`, the user's $HOME directly, or a known system root (`/etc`, `/usr`, `/bin`, `/sbin`, `/var`, `/sys`, `/proc`, `/dev`, `/boot`, `/lib`, `/lib64`, `/root`, `/srv`). Hand-write those if you really want them — one-click blanket grants on system trees are a footgun.
- Move/Copy: src and dst broadened independently with the same rule, joined with " -> ".
- Git: first arg + " *".
- Tests: same as Bash — first argv-token of the test command + " *".
- Glob/Grep/Fetch/Rollback: not derived (too varied or too high-trust to grant blanket on one click).
ok=false means the modal should suppress the [a]lways-allow option for this call.
func ParseDiffPaths ¶
ParseDiffPaths extracts the unique set of target file paths a unified diff would touch. Used by apply_diff to (a) populate the permissions descriptor so user-authored Deny/Allow rules match the diff's targets, and (b) drive ValidateWritePath against DefaultDenyPaths so yottacode-managed state can't be patched through the diff surface.
Returned paths are repo-relative — the same form `git apply` would resolve against the work tree. The "a/" / "b/" prefixes git emits in `--- a/foo` / `+++ b/foo` are stripped. /dev/null entries (new-file source / deleted-file destination) are skipped; the paired non-null side still produces a path. Renames contribute both source and destination.
Types ¶
type Decision ¶
type Decision int
Decision is the verdict for a single tool call.
const ( // Default means no rule matched; the loop falls back to the tool's // own RequiresApproval (read-only auto-execute, mutators prompt). Default Decision = iota // Allow means a rule explicitly approves this call — execute // silently, no prompt. Allow // Ask means a rule explicitly forces a prompt even if the tool would // normally auto-execute. Useful for things like `Read(.env)`. Ask // Deny means a rule explicitly refuses this call — never execute, // even under --yolo. The user wrote the // rule on purpose; bypass is a "skip prompts" knob, not an "ignore // my policy" knob. Deny )
type PathNormalizer ¶ added in v0.3.0
PathNormalizer is the optional callback DeriveAllowRule applies to path-like inputs (cwd plus any absolute descriptor) before turning them into a glob pattern. Used by the agent loop to inject `worktree.NormalizeForRule`, which rewrites the auto-generated worktree-name segment of a yottacode worktree path to `*` so `[A]-always` clicks don't bake ephemeral names into the saved rule. A nil normalizer is treated as identity.
type Permissions ¶
type Permissions struct {
// contains filtered or unexported fields
}
Permissions holds the loaded rules from a project's permissions files. Concurrency-safe: the agent goroutine reads via Evaluate while the TUI may write via AddAllow.
func Load ¶
func Load(cwd string) (*Permissions, error)
Load reads <cwd>/.yottacode/permissions.json and .yottacode/permissions.local.json (both optional), merges their rule lists, and returns a usable Permissions value. Missing files are not errors. Malformed files are surfaced so the user can fix them rather than silently running with stale rules.
func LoadEmpty ¶
func LoadEmpty(cwd string) *Permissions
LoadEmpty returns a Permissions with no rules but with cwd set so AddAllow knows where to write. Useful for tests and for callers that want to skip disk I/O.
func (*Permissions) AddAllow ¶
func (p *Permissions) AddAllow(rule string) error
AddAllow appends a rule to permissions.local.json (creating the file and parent dir if needed). Used by the TUI's "always allow" path. Idempotent: a duplicate rule is silently dropped.
func (*Permissions) EnsureFiles ¶ added in v0.2.0
func (p *Permissions) EnsureFiles() error
EnsureFiles writes a full {allow, ask, deny} skeleton to any permissions file that is missing or contains only whitespace. Existing content is preserved — even an empty-arrays-only file is left alone so a user-customized layout (formatting, comments later, extra top-level keys) is never clobbered. The /permissions picker calls this before opening vim so the user always edits a fully- shaped file instead of an empty buffer.
func (*Permissions) Evaluate ¶
func (p *Permissions) Evaluate(toolName, argsJSON string) Decision
Evaluate runs the precedence chain (deny > allow > ask) against a single tool call described by toolName + argsJSON. Returns Default when no rule matches, leaving the existing approval flow in charge.
Multi-target calls (Target.Descriptors set, e.g. apply_diff touching several files) use ratcheted semantics: Deny if any path is denied, Allow only if every path matches an allow rule, Ask if any path matches ask. The conservative Allow rule prevents a diff that mixes rule-covered and unknown paths from skipping the modal.
func (*Permissions) LocalPath ¶
func (p *Permissions) LocalPath() string
LocalPath returns the path to permissions.local.json for /permissions UX.
func (*Permissions) Reload ¶
func (p *Permissions) Reload() error
Reload re-reads both files from disk, replacing the in-memory rule set. Useful after the user edits permissions.json with their editor.
reloadMu is held for the whole read+swap so concurrent reloads can't interleave: the disk read and the swap happen as one unit per caller. Evaluate still reads through mu and never blocks on a reload's disk I/O.
func (*Permissions) SharedPath ¶
func (p *Permissions) SharedPath() string
SharedPath returns the path to permissions.json for /permissions UX.
func (*Permissions) Snapshot ¶
func (p *Permissions) Snapshot() (deny, allow, ask []Rule)
Snapshot returns a copy of every loaded rule (deny + allow + ask) for /permissions display.
type Rule ¶
type Rule struct {
Tool string // e.g. "Bash", "Edit"
Pattern string // e.g. "go test *", "internal/**"
// Source is "permissions.json" or "permissions.local.json" — used
// only for diagnostics in /permissions.
Source string
}
Rule is one parsed entry from the permissions file: tool prefix + the raw pattern between the parens.
type Target ¶
type Target struct {
// PermName is the rule prefix the file uses ("Bash", "Edit",
// "Read", …). Empty when the tool isn't subject to permission
// matching (no descriptor extractor is registered for it).
PermName string
// Descriptor is the string the rule pattern matches against:
// command for Bash, cwd-relative path for filesystem tools, joined
// args for Git, URL for Fetch, etc. Used when Descriptors is empty.
Descriptor string
// Descriptors carries multi-target calls (e.g. an apply_diff that
// touches several files). Used in tandem with Multi: Multi=true
// signals the evaluator to use multi-target precedence (any-deny,
// all-allow, any-ask) regardless of slice length, so a tool whose
// path-extraction failed (empty list) doesn't accidentally fall
// back to single-target evaluation against an empty descriptor and
// vacuously match Edit(*) style rules.
Descriptors []string
// Multi is set on tools whose evaluation must iterate Descriptors
// even when the slice is empty. apply_diff is the canonical case:
// a malformed or headerless diff yields zero descriptors but must
// still avoid matching single-descriptor "" rules.
Multi bool
// IsPath flags path-typed targets so matchPattern can route them
// through doublestar instead of the free-form glob matcher.
IsPath bool
}
Target is the (permission-name, descriptor) pair extracted from a single tool call. The agent loop produces it per call and Permissions.Evaluate matches it against the rule set.