mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
The previous commit was made on a stale base and so reverted work that landed on master before it. This commit puts every file back to the state of its parent's parent. The permission enforcement of the previous commit stays. Assisted-by: Claude:claude-sonnet-5-5 go
116 lines
3.2 KiB
Go
116 lines
3.2 KiB
Go
// SPDX-License-Identifier: MIT
|
|
package tools
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mudler/LocalAI/addons/internal/protocol"
|
|
)
|
|
|
|
// bad is a refusal of the request with a short, fixed reason. The reason
|
|
// reaches the model, so it never holds text from GitHub or from the token.
|
|
func bad(format string, a ...any) error {
|
|
return fmt.Errorf("%w: %s", protocol.ErrBadRequest, fmt.Sprintf(format, a...))
|
|
}
|
|
|
|
var (
|
|
repoShape = regexp.MustCompile(`^[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}$`)
|
|
refShape = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$`)
|
|
)
|
|
|
|
// checkRepo accepts the form owner/name and nothing else.
|
|
func checkRepo(repo string) error {
|
|
if !repoShape.MatchString(repo) || strings.Contains(repo, "..") {
|
|
return bad("repo must be owner/name")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkRef accepts a branch, tag or commit name.
|
|
func checkRef(ref string) error {
|
|
if ref != "" && (!refShape.MatchString(ref) || strings.Contains(ref, "..")) {
|
|
return bad("ref is not a branch tag or commit")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkPath accepts a path inside the repository: no empty, dot or dot-dot
|
|
// segment, no .git segment, no backslash, no control character. An empty path
|
|
// is the root when allowRoot.
|
|
func checkPath(p string, allowRoot bool) error {
|
|
if p == "" {
|
|
if allowRoot {
|
|
return nil
|
|
}
|
|
return bad("path is required")
|
|
}
|
|
if len(p) > 400 || !utf8.ValidString(p) || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") {
|
|
return bad("path is not valid")
|
|
}
|
|
for _, r := range p {
|
|
if unicode.IsControl(r) {
|
|
return bad("path is not valid")
|
|
}
|
|
}
|
|
for _, seg := range strings.Split(strings.TrimSuffix(p, "/"), "/") {
|
|
if seg == "" || seg == "." || seg == ".." || strings.EqualFold(seg, ".git") {
|
|
return bad("path is not valid")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// forbiddenQualifiers widen a code search beyond the one repository. They are
|
|
// refused wherever they appear, quoted or not.
|
|
var forbiddenQualifiers = []string{"repo:", "org:", "user:", "owner:", "enterprise:"}
|
|
|
|
func checkQuery(q string) error {
|
|
q = strings.TrimSpace(q)
|
|
if q == "" || utf8.RuneCountInString(q) > 256 || !utf8.ValidString(q) {
|
|
return bad("query has 1 to 256 characters")
|
|
}
|
|
for _, r := range q {
|
|
if unicode.IsControl(r) {
|
|
return bad("query has a control character")
|
|
}
|
|
}
|
|
l := strings.ToLower(q)
|
|
for _, f := range forbiddenQualifiers {
|
|
if strings.Contains(l, f) {
|
|
return bad("query may not name a repo or an owner")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// decodeArgs reads the arguments of a tool, less the tool name, into v. An
|
|
// unknown field is a refusal: the host checked the schema, so one here is a
|
|
// bug to surface and not input to guess at.
|
|
func decodeArgs(raw map[string]json.RawMessage, v any) error {
|
|
b, _ := json.Marshal(raw)
|
|
d := json.NewDecoder(strings.NewReader(string(b)))
|
|
d.DisallowUnknownFields()
|
|
if err := d.Decode(v); err != nil {
|
|
return bad("the arguments do not match the tool")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cut shortens s to at most max bytes at a character boundary and says how
|
|
// much was left out.
|
|
func cut(s string, max int) (out string, omitted int) {
|
|
if len(s) <= max {
|
|
return s, 0
|
|
}
|
|
n := max
|
|
for n > 0 && !utf8.RuneStart(s[n]) {
|
|
n--
|
|
}
|
|
return s[:n], len(s) - n
|
|
}
|