| 1 | package permission |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "strings" |
| 6 | |
| 7 | "reasonix/internal/shellsafe" |
| 8 | ) |
| 9 | |
| 10 | // BashCommandIsReadOnly reports whether a bash tool call is a known foreground |
| 11 | // read-only command. Capability-restricted runners use this directly instead of |
| 12 | // depending on Plan mode: Plan is a collaboration workflow, while this check is |
| 13 | // an execution permission boundary. |
| 14 | func BashCommandIsReadOnly(args json.RawMessage) bool { |
| 15 | var p struct { |
| 16 | Command string `json:"command"` |
| 17 | RunInBackground bool `json:"run_in_background"` |
| 18 | PreserveBackgroundProcesses bool `json:"preserve_background_processes"` |
| 19 | } |
| 20 | if err := json.Unmarshal(args, &p); err != nil || strings.TrimSpace(p.Command) == "" { |
| 21 | return false |
| 22 | } |
| 23 | if p.RunInBackground || p.PreserveBackgroundProcesses { |
| 24 | return false |
| 25 | } |
| 26 | return isReadOnlyBashSubject(p.Command) |
| 27 | } |
| 28 | |
| 29 | // isReadOnlyBashSubject returns true when a bash command is a known read-only |
| 30 | // operation. The subject is the JSON arg value extracted by Subject() — for bash |
| 31 | // it is the raw command string. Both command membership and argument effects |
| 32 | // come from shellsafe so permission and mutation accounting cannot drift. |
| 33 | func isReadOnlyBashSubject(subject string) bool { |
| 34 | return shellsafe.ClassifyBash(subject).IsPermissionReader() |
| 35 | } |
| 36 | |
| 37 | // containsShellSyntax delegates to the shared classifier; retained for the other |
| 38 | // permission call sites (permission.go). |
| 39 | func containsShellSyntax(cmd string) bool { |
| 40 | return shellsafe.ContainsShellSyntax(cmd) |
| 41 | } |
| 42 | |
| 43 | // dangerousBashPatterns are glob-like patterns that match destructive |
| 44 | // commands. Used only for a UI warning — the deny list is the actual |
| 45 | // enforcement mechanism. |
| 46 | var dangerousBashPatterns = []struct { |
| 47 | pattern string |
| 48 | label string |
| 49 | }{ |
| 50 | {"rm -rf*", "recursive delete"}, |
| 51 | {"rm -r *", "recursive delete"}, |
| 52 | {"rm -fr*", "recursive delete"}, |
| 53 | {"git push*--force*", "force push"}, |
| 54 | {"git push*-f*", "force push"}, |
| 55 | {"git reset --hard*", "hard reset"}, |
| 56 | {"git clean -f*", "force clean"}, |
| 57 | {"git restore*", "discards uncommitted changes"}, |
| 58 | {"git checkout -- *", "discards uncommitted changes"}, |
| 59 | {"git checkout .*", "discards uncommitted changes"}, |
| 60 | {"git stash drop*", "drops stashed changes"}, |
| 61 | {"git stash clear*", "drops stashed changes"}, |
| 62 | {"chmod 777*", "world-writable"}, |
| 63 | {"chmod -R 777*", "world-writable recursive"}, |
| 64 | {"chown *", "ownership change"}, |
| 65 | {"sudo *", "superuser"}, |
| 66 | {"mkfs*", "filesystem format"}, |
| 67 | {"dd if=*", "raw device write"}, |
| 68 | {"fdisk*", "partition table"}, |
| 69 | {"> /dev/*", "device overwrite"}, |
| 70 | } |
| 71 | |
| 72 | // BashDangerWarning returns a short label if subject matches a known |
| 73 | // dangerous pattern, or "" when the command looks safe. This is a visual |
| 74 | // hint only — the Policy rules are the authority. |
| 75 | func BashDangerWarning(subject string) string { |
| 76 | s := strings.TrimSpace(subject) |
| 77 | for _, d := range dangerousBashPatterns { |
| 78 | if matchGlob(d.pattern, s) { |
| 79 | return d.label |
| 80 | } |
| 81 | } |
| 82 | return "" |
| 83 | } |
| 84 |