104 lines
2.7 KiB
Go
104 lines
2.7 KiB
Go
// Package permission entscheidet pro Tool-Aufruf, ob er automatisch laufen
|
|
// darf, eine Rückfrage braucht, oder abgelehnt wird (E-11). Bewusst
|
|
// config-unabhängig (nur Strings/Maps) — der Aufrufer übersetzt
|
|
// config.PermissionsConfig in eine Policy, damit dieses Paket ohne
|
|
// Config-Kopplung testbar bleibt.
|
|
package permission
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/bmatcuk/doublestar/v4"
|
|
)
|
|
|
|
type Mode string
|
|
|
|
const (
|
|
ModeAuto Mode = "auto"
|
|
ModeAsk Mode = "ask"
|
|
ModeDeny Mode = "deny"
|
|
)
|
|
|
|
// Policy ist die ausgewertete Permissions-Config für eine Session.
|
|
type Policy struct {
|
|
// Modes bildet Tool-Namen auf einen Modus ab. Tools ohne Eintrag (z.B.
|
|
// MCP-Tools, todo, read_skill) gelten als ModeAuto.
|
|
Modes map[string]Mode
|
|
|
|
// DenyPaths/DenyBash sind zusätzliche Sperren, die "auto" und "ask"
|
|
// immer überstimmen — eine explizite Deny-Regel gewinnt.
|
|
DenyPaths []string
|
|
DenyBash []string
|
|
}
|
|
|
|
// ModeFor liefert den konfigurierten Modus für ein Tool, ohne Pfad-/Bash-
|
|
// Denylist zu prüfen. Unbekannte Tools und ein nil-Policy ergeben ModeAuto.
|
|
func (p *Policy) ModeFor(toolName string) Mode {
|
|
if p == nil {
|
|
return ModeAuto
|
|
}
|
|
if m, ok := p.Modes[toolName]; ok && m != "" {
|
|
return m
|
|
}
|
|
return ModeAuto
|
|
}
|
|
|
|
// Check entscheidet den tatsächlichen Modus für einen konkreten Aufruf
|
|
// inklusive Pfad-/Bash-Denylist. Eine Deny-Regel überstimmt immer, auch
|
|
// wenn das Tool selbst auf "auto" oder "ask" steht.
|
|
func (p *Policy) Check(toolName string, input json.RawMessage) Mode {
|
|
mode := p.ModeFor(toolName)
|
|
if p == nil {
|
|
return mode
|
|
}
|
|
if path := extractPath(input); path != "" && matchesAny(p.DenyPaths, path) {
|
|
return ModeDeny
|
|
}
|
|
if toolName == "bash" {
|
|
if cmd := extractCommand(input); cmd != "" && matchesAny(p.DenyBash, cmd) {
|
|
return ModeDeny
|
|
}
|
|
}
|
|
return mode
|
|
}
|
|
|
|
func extractPath(input json.RawMessage) string {
|
|
var v struct {
|
|
Path string `json:"path"`
|
|
}
|
|
if err := json.Unmarshal(input, &v); err != nil {
|
|
return ""
|
|
}
|
|
return v.Path
|
|
}
|
|
|
|
func extractCommand(input json.RawMessage) string {
|
|
var v struct {
|
|
Command string `json:"command"`
|
|
}
|
|
if err := json.Unmarshal(input, &v); err != nil {
|
|
return ""
|
|
}
|
|
return v.Command
|
|
}
|
|
|
|
// matchesAny prüft ein Glob-Match (deny_paths: ".git/**", "**/id_rsa*") und
|
|
// zusätzlich ein Substring-Match, weil deny_bash-Beispiele im Konzept
|
|
// literale Fragmente ohne Wildcard sind ("rm -rf /") — ein reines
|
|
// Vollstring-Glob-Match würde "cd /tmp && rm -rf /" nicht fangen. Für eine
|
|
// Denylist ist Über-Treffen die sicherere Richtung als Unter-Treffen.
|
|
func matchesAny(patterns []string, s string) bool {
|
|
for _, p := range patterns {
|
|
if p == "" {
|
|
continue
|
|
}
|
|
if ok, _ := doublestar.Match(p, s); ok {
|
|
return true
|
|
}
|
|
if strings.Contains(s, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|