// Package stubclient provides a gRPC client for the Dify Agent Stub service. // // The Agent Stub service runs on the host (dify-agent backend) and exposes // Connect, FileUpload, and FileDownload RPCs to sandbox-resident processes. // This client is used by the dify-agent CLI binary inside the sandbox container. package stubclient import ( "context" "fmt" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" stubv1 "github.com/langgenius/dify/dify-agent-runtime/gen/dify/agent/stub/v1" ) // Client wraps the gRPC AgentStubService client with connection lifecycle management. type Client struct { conn *grpc.ClientConn stub stubv1.AgentStubServiceClient timeout time.Duration } // Option configures a Client. type Option func(*Client) // WithTimeout sets the default per-call timeout. Default is 30s. func WithTimeout(d time.Duration) Option { return func(c *Client) { c.timeout = d } } // Dial creates a new Client connected to the given target address (host:port). func Dial(target string, opts ...Option) (*Client, error) { c := &Client{timeout: 30 * time.Second} for _, o := range opts { o(c) } conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, fmt.Errorf("stubclient: dial %s: %w", target, err) } c.conn = conn c.stub = stubv1.NewAgentStubServiceClient(conn) return c, nil } // Close closes the underlying gRPC connection. func (c *Client) Close() error { if c.conn != nil { return c.conn.Close() } return nil } // ConnectResult holds the response from a Connect RPC. type ConnectResult struct { ConnectionID string Status string } // Connect establishes a logical connection with the Agent Stub server. func (c *Client) Connect(ctx context.Context, protocolVersion int32, argv []string, metadataJSON string) (*ConnectResult, error) { ctx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() resp, err := c.stub.Connect(ctx, &stubv1.ConnectRequest{ ProtocolVersion: protocolVersion, Argv: argv, MetadataJson: metadataJSON, }) if err != nil { return nil, fmt.Errorf("stubclient: connect: %w", err) } return &ConnectResult{ ConnectionID: resp.GetConnectionId(), Status: resp.GetStatus(), }, nil } // FileUploadResult holds the response from a CreateFileUploadRequest RPC. type FileUploadResult struct { UploadURL string } // CreateFileUpload requests an upload URL from the Agent Stub server. func (c *Client) CreateFileUpload(ctx context.Context, filename, mimetype string) (*FileUploadResult, error) { ctx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() resp, err := c.stub.CreateFileUploadRequest(ctx, &stubv1.FileUploadRequest{ Filename: filename, Mimetype: mimetype, }) if err != nil { return nil, fmt.Errorf("stubclient: file upload: %w", err) } return &FileUploadResult{ UploadURL: resp.GetUploadUrl(), }, nil } // FileDownloadResult holds the response from a CreateFileDownloadRequest RPC. type FileDownloadResult struct { Filename string MimeType string Size int64 DownloadURL string } // CreateFileDownload requests a download URL from the Agent Stub server. func (c *Client) CreateFileDownload(ctx context.Context, transferMethod string, reference, url *string, forExternal bool) (*FileDownloadResult, error) { ctx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() fileMapping := &stubv1.FileMapping{ TransferMethod: transferMethod, } if reference != nil { fileMapping.Reference = reference } if url != nil { fileMapping.Url = url } resp, err := c.stub.CreateFileDownloadRequest(ctx, &stubv1.FileDownloadRequest{ File: fileMapping, ForExternal: &forExternal, }) if err != nil { return nil, fmt.Errorf("stubclient: file download: %w", err) } return &FileDownloadResult{ Filename: resp.GetFilename(), MimeType: resp.GetMimeType(), Size: resp.GetSize(), DownloadURL: resp.GetDownloadUrl(), }, nil }