Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

122 lines
3.0 KiB
Go

package server
import (
"crypto/subtle"
"fmt"
"net/http"
"net/url"
"strings"
tuttiapi "github.com/tutti-os/tutti/services/tuttid/api"
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
tuttitypes "github.com/tutti-os/tutti/services/tuttid/types"
)
type Routes = tuttiapi.Routes
type ListenerSpec struct {
AccessToken string
Addr string
}
func NewMux(routes Routes) *http.ServeMux {
mux := http.NewServeMux()
tuttiapi.RegisterRoutes(mux, routes)
return mux
}
func NewHTTPServer(spec ListenerSpec, routes Routes) *http.Server {
handler := tuttitypes.WithBearerTokenAuthFunc(
spec.AccessToken,
func(r *http.Request, token string) bool {
return authorizeWorkspaceAppServerToken(r, token, spec.AccessToken)
},
NewMux(routes),
)
return &http.Server{
Addr: spec.Addr,
Handler: tuttitypes.WithCORS(handler),
}
}
func authorizeWorkspaceAppServerToken(r *http.Request, token string, accessToken string) bool {
segments := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(segments) < 6 ||
segments[0] != "v1" ||
segments[1] != "workspaces" ||
segments[3] != "apps" {
return false
}
if !isWorkspaceAppServerTokenRoute(r.Method, segments) {
return false
}
workspaceID, err := url.PathUnescape(segments[2])
if err != nil {
return false
}
appID, err := url.PathUnescape(segments[4])
if err != nil {
return false
}
expected := workspacebiz.AppServerToken(accessToken, workspaceID, appID)
return expected != "" && subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
}
func isWorkspaceAppServerTokenRoute(method string, segments []string) bool {
if len(segments) == 8 &&
method == http.MethodPut &&
segments[5] == "uploads" &&
strings.TrimSpace(segments[6]) != "" &&
segments[7] == "content" {
return true
}
if len(segments) == 7 &&
method == http.MethodGet &&
segments[5] == "preferences" &&
segments[6] == "agent" {
return true
}
if len(segments) == 7 &&
method == http.MethodGet &&
segments[5] == "agent-providers" &&
segments[6] == "status" {
return true
}
if len(segments) == 8 &&
method == http.MethodPost &&
segments[5] == "agent-providers" &&
strings.TrimSpace(segments[6]) != "" &&
segments[7] == "composer-options" {
return true
}
if (len(segments) != 7 && len(segments) != 8) ||
segments[5] != "managed-model-grants" {
return false
}
switch method {
case http.MethodPost:
return (len(segments) == 7 && segments[6] == "exchange") ||
(len(segments) == 8 && segments[7] == "credentials")
case http.MethodGet:
return len(segments) == 8 && segments[7] == "models"
case http.MethodDelete:
return len(segments) == 7 && segments[6] != "exchange"
default:
return false
}
}
func ListenerSpecFromEnv() (ListenerSpec, error) {
defaults := tuttitypes.ResolveDefaultsFromEnv()
accessToken := tuttitypes.EnvOrDefault("TUTTID_ACCESS_TOKEN", "")
if accessToken == "" {
return ListenerSpec{}, fmt.Errorf("TUTTID_ACCESS_TOKEN is required")
}
return ListenerSpec{
AccessToken: accessToken,
Addr: defaults.Transport.TCPAddr,
}, nil
}