yao/tai/grpc/cmd/main.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

267 lines
7.1 KiB
Go

package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"syscall"
yaogrpc "github.com/yaoapp/yao/tai/grpc"
)
// Build-time variables set via -ldflags.
var (
Version = "dev"
Commit = "none"
BuildTime = "unknown"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: yao-grpc <version|serve>")
os.Exit(1)
}
switch os.Args[1] {
case "version":
fmt.Printf("yao-grpc %s (commit: %s, built: %s)\n", Version, Commit, BuildTime)
case "serve":
if err := serve(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\nUsage: yao-grpc <version|serve>\n", os.Args[1])
os.Exit(1)
}
}
// jsonrpcRequest is a minimal JSON-RPC 2.0 request.
type jsonrpcRequest struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
// jsonrpcResponse is a minimal JSON-RPC 2.0 response.
type jsonrpcResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Error *jsonrpcError `json:"error,omitempty"`
}
type jsonrpcError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func serve() error {
client, err := yaogrpc.NewFromEnv()
if err != nil {
return err
}
defer client.Close()
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
if sandboxID := os.Getenv("YAO_SANDBOX_ID"); sandboxID != "" {
go yaogrpc.HeartbeatLoop(ctx, client, sandboxID)
}
scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 0, 4*1024*1024), 4*1024*1024)
encoder := json.NewEncoder(os.Stdout)
for scanner.Scan() {
select {
case <-ctx.Done():
return nil
default:
}
line := scanner.Bytes()
if len(line) == 0 {
continue
}
var req jsonrpcRequest
if err := json.Unmarshal(line, &req); err != nil {
encoder.Encode(jsonrpcResponse{
JSONRPC: "2.0",
Error: &jsonrpcError{Code: -32700, Message: "parse error"},
})
continue
}
resp := dispatch(ctx, client, &req)
encoder.Encode(resp)
}
if err := scanner.Err(); err != nil && err != io.EOF {
return fmt.Errorf("stdin read: %w", err)
}
return nil
}
func dispatch(ctx context.Context, client *yaogrpc.Client, req *jsonrpcRequest) jsonrpcResponse {
base := jsonrpcResponse{JSONRPC: "2.0", ID: req.ID}
switch req.Method {
case "run":
return handleRun(ctx, client, req, base)
case "shell":
return handleShell(ctx, client, req, base)
case "mcp/list_tools":
return handleMCPListTools(ctx, client, req, base)
case "mcp/call_tool":
return handleMCPCallTool(ctx, client, req, base)
case "mcp/list_resources":
return handleMCPListResources(ctx, client, req, base)
case "mcp/read_resource":
return handleMCPReadResource(ctx, client, req, base)
case "healthz":
return handleHealthz(ctx, client, base)
default:
base.Error = &jsonrpcError{Code: -32601, Message: "method not found: " + req.Method}
return base
}
}
// --- handlers ---
type runParams struct {
Process string `json:"process"`
Args json.RawMessage `json:"args,omitempty"`
Timeout int32 `json:"timeout,omitempty"`
}
func handleRun(ctx context.Context, c *yaogrpc.Client, req *jsonrpcRequest, base jsonrpcResponse) jsonrpcResponse {
var p runParams
if err := json.Unmarshal(req.Params, &p); err != nil {
base.Error = &jsonrpcError{Code: -32602, Message: "invalid params: " + err.Error()}
return base
}
data, err := c.Run(ctx, p.Process, p.Args, p.Timeout)
if err != nil {
base.Error = &jsonrpcError{Code: -32000, Message: err.Error()}
return base
}
base.Result = data
return base
}
type shellParams struct {
Command string `json:"command"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
Timeout int32 `json:"timeout,omitempty"`
}
func handleShell(ctx context.Context, c *yaogrpc.Client, req *jsonrpcRequest, base jsonrpcResponse) jsonrpcResponse {
var p shellParams
if err := json.Unmarshal(req.Params, &p); err != nil {
base.Error = &jsonrpcError{Code: -32602, Message: "invalid params: " + err.Error()}
return base
}
resp, err := c.Shell(ctx, p.Command, p.Args, p.Env, p.Timeout)
if err != nil {
base.Error = &jsonrpcError{Code: -32000, Message: err.Error()}
return base
}
data, _ := json.Marshal(resp)
base.Result = data
return base
}
type mcpSessionParams struct {
SessionID string `json:"session_id"`
}
func handleMCPListTools(ctx context.Context, c *yaogrpc.Client, req *jsonrpcRequest, base jsonrpcResponse) jsonrpcResponse {
var p mcpSessionParams
if err := json.Unmarshal(req.Params, &p); err != nil {
base.Error = &jsonrpcError{Code: -32602, Message: "invalid params: " + err.Error()}
return base
}
data, err := c.MCPListTools(ctx, p.SessionID)
if err != nil {
base.Error = &jsonrpcError{Code: -32000, Message: err.Error()}
return base
}
base.Result = data
return base
}
type mcpCallParams struct {
SessionID string `json:"session_id"`
Tool string `json:"tool"`
Arguments json.RawMessage `json:"arguments,omitempty"`
}
func handleMCPCallTool(ctx context.Context, c *yaogrpc.Client, req *jsonrpcRequest, base jsonrpcResponse) jsonrpcResponse {
var p mcpCallParams
if err := json.Unmarshal(req.Params, &p); err != nil {
base.Error = &jsonrpcError{Code: -32602, Message: "invalid params: " + err.Error()}
return base
}
data, err := c.MCPCallTool(ctx, p.SessionID, p.Tool, p.Arguments)
if err != nil {
base.Error = &jsonrpcError{Code: -32000, Message: err.Error()}
return base
}
base.Result = data
return base
}
func handleMCPListResources(ctx context.Context, c *yaogrpc.Client, req *jsonrpcRequest, base jsonrpcResponse) jsonrpcResponse {
var p mcpSessionParams
if err := json.Unmarshal(req.Params, &p); err != nil {
base.Error = &jsonrpcError{Code: -32602, Message: "invalid params: " + err.Error()}
return base
}
data, err := c.MCPListResources(ctx, p.SessionID)
if err != nil {
base.Error = &jsonrpcError{Code: -32000, Message: err.Error()}
return base
}
base.Result = data
return base
}
type mcpReadParams struct {
SessionID string `json:"session_id"`
URI string `json:"uri"`
}
func handleMCPReadResource(ctx context.Context, c *yaogrpc.Client, req *jsonrpcRequest, base jsonrpcResponse) jsonrpcResponse {
var p mcpReadParams
if err := json.Unmarshal(req.Params, &p); err != nil {
base.Error = &jsonrpcError{Code: -32602, Message: "invalid params: " + err.Error()}
return base
}
data, err := c.MCPReadResource(ctx, p.SessionID, p.URI)
if err != nil {
base.Error = &jsonrpcError{Code: -32000, Message: err.Error()}
return base
}
base.Result = data
return base
}
func handleHealthz(ctx context.Context, c *yaogrpc.Client, base jsonrpcResponse) jsonrpcResponse {
status, err := c.Healthz(ctx)
if err != nil {
base.Error = &jsonrpcError{Code: -32000, Message: err.Error()}
return base
}
data, _ := json.Marshal(map[string]string{"status": status})
base.Result = data
return base
}