package sandbox import ( "context" "fmt" "path" "regexp" "strings" ) // DefaultBashDenylist is the built-in set of dangerous command patterns // DenylistBashHook rejects. It intentionally stays narrow: a false positive // on an ordinary dev workflow (git status, go test ./..., ls, rm of a // specific file, chmod 644, ...) is worse than missing an exotic bypass, // because a rejection is not fatal (see RejectionError) — the model reads // the reason and can still get equivalent work done another way. This list // is a speed bump against obviously catastrophic or exfiltration-flavored // commands, not a security boundary; the sandbox's own isolation (container // or host path confinement) is what actually contains a run. var DefaultBashDenylist = []*regexp.Regexp{ // rm -rf / or rm -rf /* — wipe the filesystem root. Deliberately scoped // to "/" or "/*" as the target so "rm -rf /workspace/tmp" (a normal, // scoped cleanup) is not caught. regexp.MustCompile(`\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*[rf][a-zA-Z]*|--recursive\s+--force|--force\s+--recursive)\s+/(\s|\*|$)`), // force-push to a remote. regexp.MustCompile(`\bgit\s+push\b[^\n]*(--force\b|(^|\s)-f(\s|$))`), // pipe a remote script straight into a shell. regexp.MustCompile(`\bcurl\b[^|\n]*\|\s*(sudo\s+)?(sh|bash|zsh)\b`), regexp.MustCompile(`\bwget\b[^|\n]*\|\s*(sudo\s+)?(sh|bash|zsh)\b`), // privilege escalation. regexp.MustCompile(`(^|[;&|]\s*)sudo\s`), // world-writable permission grants. regexp.MustCompile(`\bchmod\s+(-R\s+)?0?777\b`), // raw disk writes. regexp.MustCompile(`\bdd\s+if=`), } // DenylistBashHook rejects run_bash commands matching any of Patterns // (DefaultBashDenylist if nil). It never inspects write_file calls. type DenylistBashHook struct { // Patterns overrides DefaultBashDenylist when non-nil, so callers can // tune the list without editing this file. Patterns []*regexp.Regexp } func (h *DenylistBashHook) patterns() []*regexp.Regexp { if h.Patterns != nil { return h.Patterns } return DefaultBashDenylist } func (h *DenylistBashHook) CheckBash(_ context.Context, command string) error { for _, p := range h.patterns() { if p.MatchString(command) { return fmt.Errorf("command matches denylisted pattern %q", p.String()) } } return nil } // CheckWrite is a no-op; DenylistBashHook only gates run_bash. func (h *DenylistBashHook) CheckWrite(context.Context, string) error { return nil } var _ Hook = (*DenylistBashHook)(nil) // DefaultProtectedPathPatterns is the built-in set of path patterns // ProtectedPathHook rejects writes to. A pattern ending in "/**" protects // that directory and everything under it at any depth (path.Match has no // cross-slash wildcard, so this suffix is handled specially — see // matchProtectedPattern); any other pattern is matched with path.Match // (shell-glob semantics: "*" matches within one path segment). // // Patterns are checked against every path suffix of the write target // (i.e. at every "/"-boundary), so ".git/**" rejects both ".git/config" and // "nested/project/.git/config", not just a top-level ".git". var DefaultProtectedPathPatterns = []string{ ".git/**", ".env", ".env.*", ".github/workflows/**", } // DefaultProtectedPathSubstrings is matched as a plain substring of the // (cleaned, slash-normalized) write path — for patterns glob syntax can't // express, like "any path containing a credentials/ directory". var DefaultProtectedPathSubstrings = []string{ "credentials/", } // ProtectedPathHook rejects write_file calls whose path matches any of // Patterns/Substrings (DefaultProtectedPathPatterns/ // DefaultProtectedPathSubstrings if nil). It never inspects run_bash // commands — a determined agent can still edit a protected file via // run_bash (echo/sed/etc.); that's accepted for this phase, see // internal/sandbox/dockersandbox.go and hostsandbox.go doc comments on the // overall trust model. ProtectedPathHook exists to stop the common case: a // model calling write_file directly on something like .env or // .git/config. type ProtectedPathHook struct { Patterns []string Substrings []string } func (h *ProtectedPathHook) patterns() []string { if h.Patterns != nil { return h.Patterns } return DefaultProtectedPathPatterns } func (h *ProtectedPathHook) substrings() []string { if h.Substrings != nil { return h.Substrings } return DefaultProtectedPathSubstrings } func (h *ProtectedPathHook) CheckWrite(_ context.Context, p string) error { clean := strings.TrimPrefix(path.Clean(strings.ReplaceAll(p, "\\", "/")), "/") for _, sub := range h.substrings() { if strings.Contains(clean, sub) { return fmt.Errorf("path %q contains protected substring %q", p, sub) } } parts := strings.Split(clean, "/") for i := range parts { suffix := strings.Join(parts[i:], "/") for _, pat := range h.patterns() { if matchProtectedPattern(pat, suffix) { return fmt.Errorf("path %q matches protected pattern %q", p, pat) } } } return nil } // CheckBash is a no-op; ProtectedPathHook only gates write_file. func (h *ProtectedPathHook) CheckBash(context.Context, string) error { return nil } var _ Hook = (*ProtectedPathHook)(nil) // matchProtectedPattern matches name against pat. A pat ending in "/**" // means "this directory or anything beneath it"; any other pat uses // path.Match's shell-glob semantics ("*" bounded by "/"). func matchProtectedPattern(pat, name string) bool { if prefix, ok := strings.CutSuffix(pat, "/**"); ok { return name == prefix || strings.HasPrefix(name, prefix+"/") } ok, _ := path.Match(pat, name) return ok }