Files
zeekay c6dc8ac2f7 Hanzo Team
The Go transactor and account service for Hanzo Team.

History starts here. The previous 107 commits carried the fork's origin in
their messages and their tree, and none of it described this code: the Go
under pkg/transactor is Hanzo's own work implementing a wire protocol, not
a port of anyone's implementation.

pkg/transactor/model.json remains, and remains attributed. It is a compiled
platform model this transactor loads through go:embed, governed by EPL-2.0
and named in NOTICE. Removing it is a replacement project -- a new compiler
and a move off the protocol -- not a deletion.
2026-08-30 17:07:38 -07:00

122 lines
3.1 KiB
Go

// Package bot wires hanzo.bot (the in-app AI agent) into team-go.
//
// hanzo.bot is the unified Hanzo Bot framework (hanzoai/bot v1.0.0
// wrapper, runtimes in hanzobot/ts, hanzobot/go, etc.). This package
// exposes a single SSE endpoint that team's Svelte UI can talk to:
//
// POST /v1/bot/chat { messages: [...] } → SSE stream
// GET /v1/bot/skills → skill list (cached)
//
// Skills + auth + rate limits live inside hanzo.bot. team-go just
// forwards requests with the JWT-validated identity so the bot can
// scope tool access per user/org.
package bot
import (
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/tools/hook"
)
const (
defaultEndpoint = "https://hanzo.bot"
envEndpoint = "BOT_ENDPOINT"
)
// Register binds /v1/bot/* onto app.
func Register(app core.App) {
endpoint, err := url.Parse(botEndpoint())
if err != nil {
app.Logger().Error("bot: invalid BOT_ENDPOINT", "err", err)
return
}
// SSE-friendly client: no global timeout; per-request stream.
client := &http.Client{}
app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{
Func: func(e *core.ServeEvent) error {
e.Router.Any("/v1/bot/{path...}", func(re *core.RequestEvent) error {
return proxy(re, endpoint, client)
})
return e.Next()
},
})
}
func botEndpoint() string {
if v := os.Getenv(envEndpoint); v != "" {
return v
}
return defaultEndpoint
}
func proxy(re *core.RequestEvent, endpoint *url.URL, client *http.Client) error {
if re.Auth == nil {
return re.UnauthorizedError("bot requires auth", nil)
}
upstream := *endpoint
upstream.Path = strings.TrimRight(endpoint.Path, "/") + re.Request.URL.Path
upstream.RawQuery = re.Request.URL.RawQuery
req, err := http.NewRequestWithContext(re.Request.Context(),
re.Request.Method, upstream.String(), re.Request.Body)
if err != nil {
return re.InternalServerError("bot proxy build failed", err)
}
for k, v := range re.Request.Header {
if k == "X-User-Id" || k == "X-Org-Id" || k == "X-User-Email" {
continue
}
req.Header[k] = v
}
req.Header.Set("X-User-Id", re.Auth.Id)
req.Header.Set("X-User-Email", re.Auth.Email())
if owner := re.Auth.GetString("owner"); owner != "" {
req.Header.Set("X-Org-Id", owner)
}
// Force chunked transfer + disable proxy buffering so SSE streams
// reach the browser as they come off hanzo.bot.
req.Header.Set("X-Accel-Buffering", "no")
// Override client timeout for SSE-style streams.
if strings.Contains(req.URL.Path, "/chat") || strings.Contains(req.URL.Path, "/stream") {
client.Timeout = 0
} else {
client.Timeout = 30 * time.Second
}
resp, err := client.Do(req)
if err != nil {
return re.InternalServerError("bot unreachable", err)
}
defer resp.Body.Close()
for k, v := range resp.Header {
re.Response.Header()[k] = v
}
re.Response.WriteHeader(resp.StatusCode)
flusher, _ := re.Response.(http.Flusher)
buf := make([]byte, 4096)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
_, _ = re.Response.Write(buf[:n])
if flusher != nil {
flusher.Flush()
}
}
if err != nil {
break
}
}
return nil
}