123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 | package policy
import (
"fmt"
"slices"
"strings"
)
// ToolRule is a policy rule for tools: whether a tool (or every tool,
// "*") may run, allow or deny. The last matching rule wins; a tool no
// rule matches is denied. A tool the agent's safe policy allows runs
// without asking; one only its unsafe policy allows runs once the user
// approves the call (see ToolAccess). Tools themselves are files that
// internal/tooldef reads.
type ToolRule struct {
Tool string
Access NetPerm
Note string
Line int
From string
}
// BuiltinTools are the names tools cannot take.
var BuiltinTools = []string{"Read", "Write", "Patch", "Safe_Bash", "Unsafe_Bash"}
func validName(s string) bool {
if s == "" || len(s) > 64 {
return false
}
for i, c := range s {
letter := c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
if !(letter || c == '_' || i > 0 && c >= '0' && c <= '9') {
return false
}
}
return true
}
// ValidToolName checks a tool's name.
func ValidToolName(name string) error {
if !validName(name) {
return fmt.Errorf("tool name %q must be letters, digits and _, starting with a letter", name)
}
for _, b := range BuiltinTools {
if strings.EqualFold(name, b) {
return fmt.Errorf("%s is a built-in tool", b)
}
}
return nil
}
// ToolAccess is the access the agent's policies' tool rules give each of
// the named tools: Allow if the safe policy allows it, Ask if only the
// unsafe one does, Deny otherwise. In each, the last matching rule wins,
// and none matching denies. A rule naming a tool that is not among names
// is an error, so a typo is not silently ignored.
func ToolAccess(safe, unsafe Layer, names []string) ([]NetPerm, error) {
for _, l := range []Layer{safe, unsafe} {
for i, r := range l.File.Tools {
if r.Tool != "*" && !slices.Contains(names, r.Tool) {
return nil, fmt.Errorf("%s: there is no tool named %q", l.ruleWhere(r.From, "tool", i, r.Line), r.Tool)
}
}
}
allows := func(f *File, name string) bool {
ok := false
for _, r := range f.Tools {
if r.Tool == "*" || r.Tool == name {
ok = r.Access == Allow
}
}
return ok
}
out := make([]NetPerm, len(names))
for i, name := range names {
switch {
case allows(safe.File, name):
out[i] = Allow
case allows(unsafe.File, name):
out[i] = Ask
default:
out[i] = Deny
}
}
return out, nil
}
// WithTool returns the policy a tool runs under, as the tool sees it: the
// tool's file rules and hosts added after c's, so they win where they
// match. The floor still applies after them.
func (c *Compiled) WithTool(name string, files []FileRule, network []string) (*Compiled, error) {
tc := *c
tc.rules = append([]rule(nil), c.rules...)
tc.net = append([]netRule(nil), c.net...)
for i, r := range files {
m, links, err := newMatcher(r.Path, c.Env.Home, c.Env.Project)
if err != nil {
return nil, fmt.Errorf("tool %s: %w", name, err)
}
tc.rules = append(tc.rules, rule{
FileRule: FileRule{Path: r.Path, Access: r.Access},
m: m,
where: fmt.Sprintf("tool %s rule %d", name, i+1),
links: links,
})
}
for i, host := range network {
tc.net = append(tc.net, netRule{
NetRule: NetRule{Host: host, Access: Allow},
where: fmt.Sprintf("tool %s network %d", name, i+1),
})
}
// As in Compile: the tool's rules must not go through a symlink
// commands may replace.
if err := tc.checkLinks(); err != nil {
return nil, err
}
return &tc, nil
}
|