Files
2026-09-27 16:04:02 -06:00

251 lines
6.9 KiB
Go

package policy
import (
"bufio"
"fmt"
"strconv"
"strings"
"time"
)
// Parse extrae la política del contenido de agent.md.
//
// Por qué un bloque dedicado: agent.md es Markdown libre y puede contener
// ejemplos como "EXECUTION_MODE=unrestricted" dentro de la documentación.
// Si se interpretara cualquier línea KEY=VALUE, un ejemplo podría conceder
// permisos por accidente. Por eso solo se leen directivas dentro de un bloque
// de código cercado cuyo info string es exactamente "policy":
//
// ```policy
// EXECUTION_MODE=privileged
// ALLOW_NETWORK=true
// ```
//
// Reglas:
// - Sin bloque: política por defecto (guarded, nada permitido).
// - Más de un bloque: error, porque la intención sería ambigua.
// - Directiva repetida o valor inválido: error (no se adivina).
// - Directiva desconocida: se ignora y se reporta en Policy.Unknown.
func Parse(markdown string) (*Policy, error) {
block, found, err := extractPolicyBlock(markdown)
if err != nil {
return nil, err
}
p := Default()
if !found {
return p, nil
}
p.Declared = true
seen := map[string]int{}
sc := bufio.NewScanner(strings.NewReader(block.body))
lineNo := block.startLine
for sc.Scan() {
lineNo++
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
return nil, fmt.Errorf("agent.md line %d: expected KEY=VALUE in policy block", lineNo)
}
key = strings.ToUpper(strings.TrimSpace(key))
value = strings.TrimSpace(value)
if prev, dup := seen[key]; dup {
return nil, fmt.Errorf("agent.md line %d: duplicate directive %s (first on line %d)", lineNo, key, prev)
}
seen[key] = lineNo
if err := p.apply(key, value); err != nil {
return nil, fmt.Errorf("agent.md line %d: %w", lineNo, err)
}
}
p.sortedUnknown()
return p, nil
}
// apply interpreta una única directiva sobre la política.
func (p *Policy) apply(key, value string) error {
switch {
case key == directiveMode:
m := Mode(strings.ToLower(value))
switch m {
case ModeGuarded, ModePrivileged, ModeUnrestricted:
p.Mode = m
default:
return fmt.Errorf("invalid %s %q (want guarded, privileged or unrestricted)", key, value)
}
case key == directiveBackup:
b, err := parseBool(value)
if err != nil {
return fmt.Errorf("%s: %w", key, err)
}
p.BackupBeforeSystemEdit = b
case key == directiveTimeout:
n, err := strconv.Atoi(value)
if err != nil || n <= 0 {
return fmt.Errorf("%s must be a positive number of seconds, got %q", key, value)
}
p.DefaultCommandTimeout = time.Duration(n) * time.Second
case key == directiveMaxSteps:
n, err := strconv.Atoi(value)
if err != nil || n <= 0 || n > 1000 {
return fmt.Errorf("%s must be between 1 and 1000, got %q", key, value)
}
p.MaxSteps = n
case key == directiveMaxDL:
n, err := parseSize(value)
if err != nil || n <= 0 {
return fmt.Errorf("%s must be a positive size in bytes (suffixes K, M, G allowed), got %q", key, value)
}
p.MaxDownloadSize = n
case key == directiveTaskOrder:
for _, name := range strings.Split(value, ",") {
if name = strings.TrimSpace(name); name != "" {
p.TaskOrder = append(p.TaskOrder, name)
}
}
case knownCapabilities[Capability(key)]:
b, err := parseBool(value)
if err != nil {
return fmt.Errorf("%s: %w", key, err)
}
p.caps[Capability(key)] = b
default:
p.Unknown = append(p.Unknown, key)
}
return nil
}
// parseBool acepta solo valores inequívocos. "yes"/"1" etc. se aceptan para
// comodidad, pero cualquier otra cosa es un error en lugar de un false
// silencioso, para que un error tipográfico no pase inadvertido.
func parseBool(v string) (bool, error) {
switch strings.ToLower(v) {
case "true", "yes", "1", "on":
return true, nil
case "false", "no", "0", "off":
return false, nil
}
return false, fmt.Errorf("invalid boolean %q", v)
}
type fencedBlock struct {
body string
startLine int // número de línea de la cerca de apertura
}
// extractPolicyBlock localiza el único bloque ```policy del documento.
//
// Se recorren todos los bloques cercados respetando su longitud de cerca
// (``` o más, o ~~~), de modo que un "```policy" que aparezca como texto
// dentro de otro bloque de ejemplo no se confunda con un bloque real.
func extractPolicyBlock(md string) (fencedBlock, bool, error) {
lines := strings.Split(md, "\n")
var (
result fencedBlock
found bool
inFence bool
fence string
isPolicy bool
body strings.Builder
openLine int
)
for i, raw := range lines {
line := strings.TrimRight(raw, "\r")
trimmed := strings.TrimLeft(line, " ")
indent := len(line) - len(trimmed)
if !inFence {
if indent > 3 {
continue
}
marker, info, ok := openingFence(trimmed)
if !ok {
continue
}
inFence, fence, openLine = true, marker, i+1
fields := strings.Fields(info)
isPolicy = len(fields) > 0 && strings.EqualFold(fields[0], "policy")
body.Reset()
continue
}
// Dentro de un bloque: ¿es la cerca de cierre?
if indent <= 3 && isClosingFence(trimmed, fence) {
if isPolicy {
if found {
return fencedBlock{}, false, fmt.Errorf("agent.md: multiple policy blocks (lines %d and %d); keep only one", result.startLine, openLine)
}
result = fencedBlock{body: body.String(), startLine: openLine}
found = true
}
inFence = false
continue
}
if isPolicy {
body.WriteString(line)
body.WriteByte('\n')
}
}
if inFence && isPolicy {
return fencedBlock{}, false, fmt.Errorf("agent.md line %d: unterminated policy block", openLine)
}
return result, found, nil
}
// openingFence detecta ``` o ~~~ (3 o más) y devuelve el marcador y el info string.
func openingFence(s string) (marker, info string, ok bool) {
if len(s) < 3 || (s[0] != '`' && s[0] != '~') {
return "", "", false
}
c := s[0]
n := 0
for n < len(s) && s[n] == c {
n++
}
if n < 3 {
return "", "", false
}
info = strings.TrimSpace(s[n:])
if c == '`' && strings.Contains(info, "`") {
return "", "", false
}
return s[:n], info, true
}
// isClosingFence: mismo carácter, al menos la misma longitud y nada más.
func isClosingFence(s, open string) bool {
s = strings.TrimRight(s, " \t")
if len(s) < len(open) {
return false
}
for i := 0; i < len(s); i++ {
if s[i] != open[0] {
return false
}
}
return true
}
// parseSize acepta bytes con sufijo opcional K, M o G (potencias de 1024).
func parseSize(v string) (int64, error) {
v = strings.ToUpper(strings.TrimSpace(v))
mult := int64(1)
for suffix, m := range map[string]int64{"K": 1 << 10, "M": 1 << 20, "G": 1 << 30, "KB": 1 << 10, "MB": 1 << 20, "GB": 1 << 30, "KIB": 1 << 10, "MIB": 1 << 20, "GIB": 1 << 30} {
if strings.HasSuffix(v, suffix) && len(suffix) > 0 {
if _, err := strconv.ParseInt(strings.TrimSuffix(v, suffix), 10, 64); err == nil {
mult = m
v = strings.TrimSuffix(v, suffix)
break
}
}
}
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
if err != nil {
return 0, err
}
return n * mult, nil
}