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 }