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>
This commit is contained in:
Ettore Di Giacinto committed 2026-11-05 09:53:17 +00:00
1 parent d4c268b644
commit 894692ca16
38 files changed
+1296 -94

No files matched your search

+36
View File
@@ -241,6 +241,8 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
switch {
case path == "user":
writeJSON(w, map[string]any{"login": login, "id": 4242, "type": "User"})
case path == "user/repos":
s.userRepos(w, r)
case path == "search/code":
s.searchCode(w, r)
case strings.HasPrefix(path, "repos/"):
@@ -686,3 +688,37 @@ func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
// userRepos answers GET /user/repos: every repository of the fake, sorted by
// name, one page at a time. The fake knows no ownership, so all are visible.
func (s *Server) userRepos(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
var names []string
for _, repo := range s.repos {
names = append(names, repo.FullName)
}
s.mu.Unlock()
sort.Strings(names)
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
per, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
if page < 1 {
page = 1
}
if per < 1 {
per = 30
}
start := min((page-1)*per, len(names))
end := min(start+per, len(names))
if end < len(names) {
w.Header().Set("Link", `<https://api.github.com/user/repos?page=`+strconv.Itoa(page+1)+`>; rel="next"`)
}
out := []map[string]any{}
for _, n := range names[start:end] {
s.mu.Lock()
repo := s.repos[strings.ToLower(n)]
s.mu.Unlock()
out = append(out, map[string]any{"full_name": repo.FullName, "default_branch": repo.DefaultBranch, "private": repo.Private, "archived": false,
"html_url": "https://github.com/" + repo.FullName, "permissions": map[string]bool{"pull": true, "push": true}})
}
writeJSON(w, out)
}