Files
LocalAI/addons/github/internal/tools/repos.go
T
Ettore Di Giacinto 894692ca16 feat(github): allow classic tokens and any-repository lists, both opt-in
The GitHub add-on refused classic tokens in the page only, and its
repository list took exact owner/name entries.

A classic token (ghp_) is now an explicit choice of the connection. The
kind declares a token guard: the host refuses a token with that prefix
unless the connection sets the option classicToken to allow, and the
request that turns it on must carry confirm_wider_token. It is checked
when the token is saved and again each time it is used, so switching
the option off stops a stored token. The list and the write mode still
apply. The page shows a warning and two ticks.

The list takes owner/* and *. The host matches them before the add-on
runs, without regard to case and on whole names, so other-owner/x and
owner-evil/x are not covered by acme/*, and a key that holds a star is
never a name. A kind opts in with wildcardResources; only GitHub does,
so the other add-ons keep exact matching. Saving * needs
confirm_any_resource, and the write mode stays separate. A read tool
that declares enumerates (the new list-repos) runs only for a list of *
and asks GitHub which repositories the token reaches.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-05 09:53:17 +00:00

67 lines
2.1 KiB
Go

// SPDX-License-Identifier: MIT
package tools
import (
"encoding/json"
"net/url"
"strconv"
"strings"
)
type listReposArgs struct {
Page int `json:"page"`
PerPage int `json:"perPage"`
}
// listRepos lists the repositories the token can reach, as GitHub says. The
// host runs it only for a connection that lists * (every repository the token
// reaches): that list is the permission, so the add-on asks GitHub and does
// not narrow the answer. The key it gets is *; any other key is a bug of the
// caller and is refused.
func (c call) listRepos(raw map[string]json.RawMessage) (any, error) {
var a listReposArgs
if err := decodeArgs(raw, &a); err != nil {
return nil, err
}
if c.key != "*" {
return nil, bad("the host did not allow every repository for this call")
}
page, per := pageOr(a.Page, 1), perPageOr(a.PerPage, 30)
q := url.Values{"sort": {"full_name"}, "page": {strconv.Itoa(page)}, "per_page": {strconv.Itoa(per)}}
var list []struct {
FullName string `json:"full_name"`
DefaultBranch string `json:"default_branch"`
Private bool `json:"private"`
Archived bool `json:"archived"`
HTMLURL string `json:"html_url"`
Permissions struct {
Push bool `json:"push"`
} `json:"permissions"`
}
resp, err := c.r.API.GetJSON(c.ctx, c.token, "/user/repos", q, &list)
if err != nil {
return nil, err
}
type item struct {
FullName string `json:"fullName"`
DefaultBranch string `json:"defaultBranch"`
Private bool `json:"private"`
Archived bool `json:"archived"`
CanWrite bool `json:"canWrite"`
URL string `json:"url"`
}
items := make([]item, 0, len(list))
for _, r := range list {
// A name that is not owner/name is dropped; it could not be used anyway.
if checkRepo(r.FullName) != nil || strings.Contains(r.FullName, "*") {
continue
}
items = append(items, item{FullName: r.FullName, DefaultBranch: r.DefaultBranch, Private: r.Private, Archived: r.Archived, CanWrite: r.Permissions.Push, URL: r.HTMLURL})
}
res := map[string]any{"page": page, "items": items}
if resp.NextPage() {
res["next"] = page + 1
}
return res, nil
}