Documentation
¶
Overview ¶
Package permissions decides whether a shell command may run, from what the command actually is rather than from what its text looks like.
The old check was a list of regular expressions over the command string. It could not tell a program being invoked from the same word appearing as a grep pattern without a hand-written boundary rule for each one, it could not see past the first segment of "git diff && rm -rf /", and it had no way to say "yes to this one" — a blocked command stayed blocked. Parsing the command gives all three: every simple command in it is found, wherever it sits, and each one can be named by a rule and granted on its own.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GlobalStorePath ¶
GlobalStorePath is where grants that apply everywhere are kept.
func ProjectStorePath ¶
ProjectStorePath is where grants for one workspace are kept. It is under the user's home rather than in the workspace, because a file inside the workspace is one the agent can edit.
func ReadOnly ¶ added in v0.2.0
ReadOnly reports whether running command can only read: every simple command in it is a program on the read-only list below, used without the flags that make it write, and no redirection sends output to a file.
It answers "does this turn owe a check of what it changed", so being wrong in one direction is much worse than in the other. Calling a command read-only when it wrote something would excuse a turn that changed files from saying how it checked them, while calling a read-only command a change costs one not_run check on the report. Anything that cannot be settled from the text therefore counts as a change: a parse error, a program not on the list, a variable in the program's place, a script run by an interpreter.
Types ¶
type Effect ¶
type Effect string
Effect is what a rule says about a command.
const ( // EffectAllow lets the command run. EffectAllow Effect = "allow" // EffectDeny refuses it outright. A denial cannot be granted away. EffectDeny Effect = "deny" // EffectAsk refuses it unless someone says yes. This is the answer for // things that are dangerous in general and ordinary in context — a curl to // localhost, a package installed inside a container. EffectAsk Effect = "ask" )
type GrantScope ¶
type GrantScope string
Scope of a grant: how long it lasts and how far it reaches.
const ( // GrantOnce lets this one command through now and is not written down. GrantOnce GrantScope = "once" // GrantSession lasts for this conversation. GrantSession GrantScope = "session" // GrantProject lasts for this workspace, across conversations. GrantProject GrantScope = "project" // GrantGlobal lasts everywhere on this machine. GrantGlobal GrantScope = "global" )
type Grantor ¶
type Grantor struct {
// contains filtered or unexported fields
}
Grantor holds the grants that apply to a run and can add to them. The three stores are separate files with different lifetimes, and "once" is not written down at all.
func NewGrantor ¶
NewGrantor opens the three grant files. A nil store is fine — it contributes nothing and refuses grants at that scope.
func (*Grantor) Grant ¶
func (g *Grantor) Grant(scope GrantScope, pattern string) error
Grant records permission for a pattern at the given scope.
func (*Grantor) Where ¶
func (g *Grantor) Where(scope GrantScope) string
Where says which file a grant at this scope is written to, so the person making it knows where to go to take it back.
type Options ¶
type Options struct {
// AllowInterpreters lifts the rules on `eval` and on interpreter
// one-liners (python -c, node -e). The benchmark harnesses set it: their
// tasks are interpreter-driven and run inside an OS sandbox, where a
// one-liner grants nothing the task did not already have.
AllowInterpreters bool
// EditToolAvailable adds the rule against editing a file with sed -i,
// which only makes sense when there is an Edit tool to use instead.
EditToolAvailable bool
// Strong says agent.profile is "strong". Rules written to stop a model
// from working around the Edit tool do not apply to a model that reaches
// for the right tool on its own, and refusing it only costs iterations.
Strong bool
}
Options are the few things about the surrounding configuration that change which builtin rules apply.
type Policy ¶
type Policy struct {
// Builtin rules are kvit-coder's own and cannot be removed by config.
Builtin []Rule
// Config rules come from tools.shell.allowed_commands and
// disallowed_commands.
Config []Rule
// Session rules were granted during this conversation, or in an earlier
// one for this project or this machine.
Session []Rule
// AllowlistOnly makes anything not explicitly allowed a denial. It is set
// when tools.shell.allowed_commands is non-empty, which is how that option
// has always behaved.
AllowlistOnly bool
// AllowAsks answers yes to every command that would need permission,
// except the ones marked Irreversible. --yolo sets it. A refusal stays a
// refusal: this answers questions, and a denial was never a question.
AllowAsks bool
// HomeDir is used to recognise a command that would delete the home
// directory. Empty falls back to the process's own home.
HomeDir string
}
Policy is the rules in the order they are consulted. Denials are looked for first across every tier, so a later allow cannot open something an earlier deny closed — a session grant for "git *" does not make "sudo git" acceptable.
type Rule ¶
type Rule struct {
Pattern string
Effect Effect
Reason string
// Source says where the rule came from, for a message that explains
// itself: "builtin", "config", "session", "project", "global".
Source string
// Irreversible says the command destroys something that cannot be got
// back — a disk overwritten, a filesystem written over what was there.
// Everything else asked about can be undone or lived with: a fetch can be
// deleted, a listening port closed. It is the one thing --yolo does not
// answer for you, because the reason an ask is safe to answer blind is
// that a wrong answer can be corrected.
Irreversible bool
}
Rule names a set of commands and says what to do about them.
A pattern is a list of words. Ending it with "*" matches any command whose first words are the rest of the pattern, so "curl *" is every curl and "python3 -c *" is only the one-liner form. Without a trailing "*" the whole command must match word for word.
The program itself may also end in "*", which matches every program whose name starts with what comes before it: "mkfs.* *" is mkfs.ext4, mkfs.xfs and every other member of that family. Those are separate binaries rather than mkfs with an argument, so no pattern over arguments can reach them, and there are too many to list. Only the program accepts a partial word; the arguments are compared whole.
func Builtin ¶
Builtin returns kvit-coder's own rules. Denials here are things no grant should open. The rest are asks: dangerous in general and ordinary in context, which is exactly the case a grant exists for.
func FromConfig ¶
FromConfig turns tools.shell.allowed_commands and disallowed_commands into rules. An entry is a pattern: one word means any invocation of that program, and more words mean the command must start with them. That is close enough to the old prefix-on-the-whole-string behaviour for existing configs, and it now applies to every command in a line rather than only the first.
type Scope ¶
type Scope struct {
// Program is what is being run, with any directory prefix removed:
// /usr/bin/curl and ./curl are both "curl".
Program string
// Args are the words after the program, as written.
Args []string
// Literal is the whole simple command as written, which is what a grant
// names when the command cannot safely be generalized.
Literal string
// Paths are the arguments that name a file or directory.
Paths []string
// Generalizable says whether a grant for this scope may be widened to
// "<program> *". It is false inside a loop or a conditional, where the
// arguments come from the surrounding construct rather than from the text
// in front of you: granting "rm *" on the strength of "for f in *; do rm
// $f; done" would grant every rm there will ever be.
Generalizable bool
}
Scope is one simple command found inside a command line: the program and the words handed to it. A command line has as many scopes as it has commands, so "git diff && rm -rf /" has two and each is judged on its own.
func Parse ¶
Parse finds every simple command in a command line, including the ones inside pipelines, chains, subshells, command substitutions, loops and conditionals.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a file of grants. Grant files live outside the workspace on purpose: the agent can write anywhere inside the workspace, and a permission file it could edit would not be a permission file.
type Verdict ¶
type Verdict struct {
Effect Effect
Scope Scope
Rule Rule
// AutoAllowed says this was a command someone would normally be asked
// about, allowed because AllowAsks is set. The caller says so on the
// terminal, so a run started with --yolo leaves a record of what it
// decided rather than only of having been started.
AutoAllowed bool
}
Verdict is what the policy decided about one command.