mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
Add a manifest credentials declaration, host-enforced per-binding resource lists, Request.Credential and ResourceKey set by the host, and a typed ErrRateLimited that reaches the model as rate_limited. 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
|
|
}
|