yao/grpc/grpc.go
Max dfb33681f9 Implement Sandbox V2 support in the Yao SDK
- Add new gRPC endpoint for Heartbeat in the Yao service, enabling communication with the sandbox.
- Update Makefile to include a dedicated unit test target for Sandbox V2, ensuring proper testing of new features.
- Enhance CI workflows to incorporate Sandbox V2 tests, allowing for dual-mode testing (local and remote) with Docker.
- Modify .gitignore to exclude specific Docker files while allowing shell scripts for Sandbox V2.
- Update documentation in DESIGN.md to reflect the new architecture and capabilities of the Sandbox V2.

These changes enhance the Yao SDK's functionality, providing improved support for sandbox operations and testing.
2026-03-05 13:21:09 +08:00

202 lines
7 KiB
Go

package grpc
import (
"context"
"net"
"strconv"
"strings"
"sync"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/config"
agenthandler "github.com/yaoapp/yao/grpc/agent"
apihandler "github.com/yaoapp/yao/grpc/api"
"github.com/yaoapp/yao/grpc/auth"
"github.com/yaoapp/yao/grpc/health"
llmhandler "github.com/yaoapp/yao/grpc/llm"
mcphandler "github.com/yaoapp/yao/grpc/mcp"
"github.com/yaoapp/yao/grpc/pb"
runhandler "github.com/yaoapp/yao/grpc/run"
sandboxhandler "github.com/yaoapp/yao/grpc/sandbox"
shellhandler "github.com/yaoapp/yao/grpc/shell"
)
var (
mu sync.Mutex
server *grpc.Server
listeners []net.Listener
addrs []string
)
type yaoServer struct {
pb.UnimplementedYaoServer
health health.Handler
run runhandler.Handler
shell shellhandler.Handler
api apihandler.Handler
mcp mcphandler.Handler
llm llmhandler.Handler
agent agenthandler.Handler
sandbox *sandboxhandler.Handler
}
// ── Health ───────────────────────────────────────────────────────────────────
func (s *yaoServer) Healthz(ctx context.Context, req *pb.Empty) (*pb.HealthzResponse, error) {
return s.health.Healthz(ctx, req)
}
// ── Base ─────────────────────────────────────────────────────────────────────
func (s *yaoServer) Run(ctx context.Context, req *pb.RunRequest) (*pb.RunResponse, error) {
return s.run.Run(ctx, req)
}
func (s *yaoServer) Shell(ctx context.Context, req *pb.ShellRequest) (*pb.ShellResponse, error) {
return s.shell.Shell(ctx, req)
}
// V2 stubs — Stream and ShellStream depend on gou/stream package.
func (s *yaoServer) Stream(req *pb.RunRequest, stream grpc.ServerStreamingServer[pb.Chunk]) error {
return status.Error(codes.Unimplemented, "Stream not implemented (V2)")
}
func (s *yaoServer) ShellStream(req *pb.ShellRequest, stream grpc.ServerStreamingServer[pb.Chunk]) error {
return status.Error(codes.Unimplemented, "ShellStream not implemented (V2)")
}
// ── API ──────────────────────────────────────────────────────────────────────
func (s *yaoServer) API(ctx context.Context, req *pb.APIRequest) (*pb.APIResponse, error) {
return s.api.API(ctx, req)
}
// ── MCP ──────────────────────────────────────────────────────────────────────
func (s *yaoServer) MCPListTools(ctx context.Context, req *pb.MCPListRequest) (*pb.MCPListResponse, error) {
return s.mcp.MCPListTools(ctx, req)
}
func (s *yaoServer) MCPCallTool(ctx context.Context, req *pb.MCPCallRequest) (*pb.MCPCallResponse, error) {
return s.mcp.MCPCallTool(ctx, req)
}
func (s *yaoServer) MCPListResources(ctx context.Context, req *pb.MCPListRequest) (*pb.MCPResourcesResponse, error) {
return s.mcp.MCPListResources(ctx, req)
}
func (s *yaoServer) MCPReadResource(ctx context.Context, req *pb.MCPResourceRequest) (*pb.MCPResourceResponse, error) {
return s.mcp.MCPReadResource(ctx, req)
}
// ── LLM ──────────────────────────────────────────────────────────────────────
func (s *yaoServer) ChatCompletions(ctx context.Context, req *pb.ChatRequest) (*pb.ChatResponse, error) {
return s.llm.ChatCompletions(ctx, req)
}
func (s *yaoServer) ChatCompletionsStream(req *pb.ChatRequest, stream grpc.ServerStreamingServer[pb.ChatChunk]) error {
return s.llm.ChatCompletionsStream(req, stream)
}
// ── Agent ────────────────────────────────────────────────────────────────────
func (s *yaoServer) AgentStream(req *pb.AgentRequest, stream grpc.ServerStreamingServer[pb.AgentChunk]) error {
return s.agent.AgentStream(req, stream)
}
// ── Sandbox ──────────────────────────────────────────────────────────────────
func (s *yaoServer) Heartbeat(ctx context.Context, req *pb.HeartbeatRequest) (*pb.HeartbeatResponse, error) {
if s.sandbox == nil {
return &pb.HeartbeatResponse{Action: "ok"}, nil
}
return s.sandbox.Heartbeat(ctx, req)
}
// SandboxHandler returns the sandbox handler for external access (e.g., Manager integration).
func SandboxHandler() *sandboxhandler.Handler {
mu.Lock()
defer mu.Unlock()
return sandboxH
}
var sandboxH *sandboxhandler.Handler
// SetSandboxOnBeat sets the heartbeat callback for the sandbox handler.
// Must be called before StartServer.
func SetSandboxOnBeat(fn func(data *sandboxhandler.HeartbeatData) string) {
sandboxH = sandboxhandler.NewHandler(fn)
}
// ── Server lifecycle ─────────────────────────────────────────────────────────
// StartServer initializes and starts the gRPC server based on config.
// It supports multiple bind addresses and returns immediately (listeners run in goroutines).
func StartServer(cfg config.Config) error {
if strings.ToLower(cfg.GRPC.Enabled) == "off" {
log.Info("gRPC server disabled (YAO_GRPC=off)")
return nil
}
mu.Lock()
defer mu.Unlock()
server = grpc.NewServer(
grpc.ChainUnaryInterceptor(auth.UnaryInterceptor),
grpc.ChainStreamInterceptor(auth.StreamInterceptor),
)
if sandboxH == nil {
sandboxH = sandboxhandler.NewHandler(nil)
}
pb.RegisterYaoServer(server, &yaoServer{sandbox: sandboxH})
hosts := strings.Split(cfg.GRPC.Host, ",")
port := strconv.Itoa(cfg.GRPC.Port)
for _, h := range hosts {
addr := net.JoinHostPort(strings.TrimSpace(h), port)
lis, err := net.Listen("tcp", addr)
if err != nil {
Stop()
return err
}
listeners = append(listeners, lis)
addrs = append(addrs, lis.Addr().String())
log.Info("gRPC server listening on %s", lis.Addr().String())
go func(l net.Listener) {
if err := server.Serve(l); err != nil {
log.Error("gRPC server error on %s: %s", l.Addr().String(), err.Error())
}
}(lis)
}
return nil
}
// Stop gracefully stops the gRPC server. Safe to call if server was never started.
func Stop() {
mu.Lock()
defer mu.Unlock()
if server != nil {
server.GracefulStop()
server = nil
}
listeners = nil
addrs = nil
}
// Addr returns all addresses the gRPC server is listening on.
func Addr() []string {
mu.Lock()
defer mu.Unlock()
result := make([]string, len(addrs))
copy(result, addrs)
return result
}