diff --git a/e2b/README.md b/e2b/README.md index 472038b..3fc0fca 100644 --- a/e2b/README.md +++ b/e2b/README.md @@ -102,6 +102,7 @@ Applied via `e2b.NewConnectionConfig(opts...)` or embedded in `Create/Connect` w | `WithAPIURL(url string)` | **Highest priority**: directly overrides API base URL | | `WithSandboxBaseURL(url string)` | **Highest priority**: directly overrides sandbox envd base URL | | `WithRequestTimeout(d time.Duration)` | HTTP request timeout, defaults to 60s | +| `WithHTTPClient(client *http.Client)` | Custom HTTP client for API requests | #### Priority @@ -193,7 +194,7 @@ e2b.WithDomain("example.com"), | Method | Description | |------------------------------------------------------------------------------|-----------------------------------------------| -| `List(ctx) ([]SandboxInfo, error)` | List all running sandboxes | +| `List(ctx, opts ...ListSandboxOpts) (*ListResult, error)` | List sandboxes with pagination support | | `GetInfo(ctx, sandboxID) (*SandboxInfo, error)` | Get sandbox details; 404 returns `not found` | | `Kill(ctx, sandboxID) (bool, error)` | Destroy sandbox; returns `true` in Debug mode | | `SetTimeout(ctx, sandboxID, timeout int32) error` | Update timeout | @@ -201,6 +202,64 @@ e2b.WithDomain("example.com"), | `ConnectSandbox(ctx, sandboxID, timeout int32) (*client.Sandbox, error)` | Low-level connect API | | `Pause(ctx, sandboxID) (string, error)` | Pause sandbox | +### ListSandboxOpts + +Options for paginated listing: + +```go +type ListSandboxOpts struct { + // Metadata filters sandboxes by metadata key-value pairs. + // Keys and values will be URL encoded automatically. + Metadata map[string]string + // State filters sandboxes by one or more states (e.g., "running", "paused"). + State []api.SandboxState + // NextToken is the cursor to start the list from (for pagination). + NextToken string + // Limit is the maximum number of items to return per page. + Limit int32 +} +``` + +### ListResult + +Result of a list operation with pagination support: + +```go +type ListResult struct { + Sandboxes []SandboxInfo // List of sandboxes in this page + NextToken string // Token for fetching the next page (empty if no more pages) +} +``` + +### Pagination Example + +```go +// Fetch all sandboxes with pagination +var allSandboxes []e2b.SandboxInfo +nextToken := "" +pageSize := int32(10) + +for { + result, err := api.List(ctx, e2b.ListSandboxOpts{ + Limit: pageSize, + NextToken: nextToken, + }) + if err != nil { + log.Fatal(err) + } + + allSandboxes = append(allSandboxes, result.Sandboxes...) + + // Check if there are more pages + if result.NextToken == "" { + break + } + nextToken = result.NextToken +} + +fmt.Printf("Total: %d sandboxes\n", len(allSandboxes)) +``` + --- ## Command Execution (Commands) diff --git a/e2b/README_zh-CH.md b/e2b/README_zh-CH.md index ba89590..367b551 100644 --- a/e2b/README_zh-CH.md +++ b/e2b/README_zh-CH.md @@ -99,6 +99,7 @@ func main() { | `WithAPIURL(url string)` | **最高优先级**:直接覆盖 API base URL,绕过 Protocol/Domain 拼装 | | `WithSandboxBaseURL(url string)` | **最高优先级**:直接覆盖 sandbox envd base URL | | `WithRequestTimeout(d time.Duration)` | HTTP 请求超时,默认 60s | +| `WithHTTPClient(client *http.Client)` | 自定义 HTTP 客户端用于 API 请求 | #### 优先级 @@ -188,7 +189,7 @@ e2b.WithDomain("example.com"), | 方法 | 说明 | |------------------------------------------------------------------------------|-------------------------------------| -| `List(ctx) ([]SandboxInfo, error)` | 列出所有运行中的 sandbox | +| `List(ctx, opts ...ListSandboxOpts) (*ListResult, error)` | 列出 sandbox,支持分页查询 | | `GetInfo(ctx, sandboxID) (*SandboxInfo, error)` | 获取 sandbox 详情,404 返回 `not found` 错误 | | `Kill(ctx, sandboxID) (bool, error)` | 销毁 sandbox;`Debug` 模式下直接返回 `true` | | `SetTimeout(ctx, sandboxID, timeout int32) error` | 修改超时时间 | @@ -196,6 +197,64 @@ e2b.WithDomain("example.com"), | `ConnectSandbox(ctx, sandboxID, timeout int32) (*client.Sandbox, error)` | 底层连接接口 | | `Pause(ctx, sandboxID) (string, error)` | 暂停 sandbox | +### ListSandboxOpts + +分页查询选项: + +```go +type ListSandboxOpts struct { + // Metadata 通过元数据键值对过滤 sandbox。 + // 键和值会自动进行 URL 编码。 + Metadata map[string]string + // State 按一个或多个状态过滤 sandbox(如 "running", "paused")。 + State []api.SandboxState + // NextToken 分页游标,用于从指定位置开始查询。 + NextToken string + // Limit 每页返回的最大数量。 + Limit int32 +} +``` + +### ListResult + +分页查询结果: + +```go +type ListResult struct { + Sandboxes []SandboxInfo // 当前页的 sandbox 列表 + NextToken string // 下一页的游标(为空表示没有更多页) +} +``` + +### 分页查询示例 + +```go +// 分页获取所有 sandbox +var allSandboxes []e2b.SandboxInfo +nextToken := "" +pageSize := int32(10) + +for { + result, err := api.List(ctx, e2b.ListSandboxOpts{ + Limit: pageSize, + NextToken: nextToken, + }) + if err != nil { + log.Fatal(err) + } + + allSandboxes = append(allSandboxes, result.Sandboxes...) + + // 检查是否还有更多页 + if result.NextToken == "" { + break + } + nextToken = result.NextToken +} + +fmt.Printf("总计: %d 个 sandbox\n", len(allSandboxes)) +``` + --- ## 命令执行(Commands) diff --git a/e2b/config.go b/e2b/config.go index 6d7ca29..8fd0e04 100644 --- a/e2b/config.go +++ b/e2b/config.go @@ -54,6 +54,9 @@ type ConnectionConfig struct { RuntimePort int // Headers contains additional headers to send with sandbox requests. Headers map[string]string + // HTTPClient is a custom HTTP client to use for API requests. + // If nil, a default client with RequestTimeout will be created. + HTTPClient *http.Client // apiClient is a lazily-initialized shared API client. apiClient *api.APIClient @@ -159,6 +162,13 @@ func WithSandboxBaseURL(sandboxBaseURL string) ConnectionConfigOption { } } +// WithHTTPClient sets a custom HTTP client for API requests. +func WithHTTPClient(httpClient *http.Client) ConnectionConfigOption { + return func(c *ConnectionConfig) { + c.HTTPClient = httpClient + } +} + // GetAPIURL returns the base API URL. func (c *ConnectionConfig) GetAPIURL() string { if c.APIURL != "" { @@ -194,12 +204,13 @@ func (c *ConnectionConfig) getScheme() string { // toEnvdConfig converts this ConnectionConfig into a runtime.Config for the envd client. func (c *ConnectionConfig) toEnvdConfig(sandboxID string) *runtime.Config { cfg := &runtime.Config{ - Domain: c.Domain, - Scheme: c.Scheme, - RuntimePort: c.RuntimePort, - APIKey: c.APIKey, - RequestTimeout: c.RequestTimeout, - RuntimeToken: c.AccessToken, + Domain: c.Domain, + Scheme: c.Scheme, + RuntimePort: c.RuntimePort, + APIKey: c.APIKey, + RequestTimeout: c.RequestTimeout, + RuntimeToken: c.AccessToken, + CustomHTTPClient: c.HTTPClient, } // Pre-compute the full sandbox URL using the Protocol-aware logic so @@ -224,8 +235,12 @@ func (c *ConnectionConfig) NewAPIClient() *api.APIClient { cfg.Servers = api.ServerConfigurations{ {URL: c.GetAPIURL()}, } - cfg.HTTPClient = &http.Client{ - Timeout: c.RequestTimeout, + if c.HTTPClient != nil { + cfg.HTTPClient = c.HTTPClient + } else { + cfg.HTTPClient = &http.Client{ + Timeout: c.RequestTimeout, + } } if c.APIKey != "" { cfg.DefaultHeader["X-API-Key"] = c.APIKey diff --git a/e2b/sandbox.go b/e2b/sandbox.go index f49a24f..c777788 100644 --- a/e2b/sandbox.go +++ b/e2b/sandbox.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/openkruise/agents-api/e2b/api" "github.com/openkruise/agents-api/runtime" ) @@ -16,11 +17,16 @@ type sandboxOptions struct { configOpts []ConnectionConfigOption // Create-specific options - timeout int32 - autoPause *bool - metadata map[string]string - envVars map[string]string - secure bool + timeout int32 + autoPause *bool + autoResume *api.SandboxAutoResumeConfig + secure bool + allowInternetAccess *bool + network *api.SandboxNetworkConfig + metadata map[string]string + envVars map[string]string + mcp map[string]interface{} + volumeMounts []api.SandboxVolumeMount } // WithTimeout sets the sandbox timeout in seconds. @@ -58,6 +64,41 @@ func WithSecure(secure bool) SandboxOption { } } +// WithAutoResume sets the auto-resume configuration for the sandbox. +func WithAutoResume(autoResume *api.SandboxAutoResumeConfig) SandboxOption { + return func(o *sandboxOptions) { + o.autoResume = autoResume + } +} + +// WithAllowInternetAccess controls whether the sandbox can access the internet. +func WithAllowInternetAccess(allow bool) SandboxOption { + return func(o *sandboxOptions) { + o.allowInternetAccess = &allow + } +} + +// WithNetwork sets the network configuration for the sandbox. +func WithNetwork(network *api.SandboxNetworkConfig) SandboxOption { + return func(o *sandboxOptions) { + o.network = network + } +} + +// WithMcp sets the MCP configuration for the sandbox. +func WithMcp(mcp map[string]interface{}) SandboxOption { + return func(o *sandboxOptions) { + o.mcp = mcp + } +} + +// WithVolumeMounts sets the volume mounts for the sandbox. +func WithVolumeMounts(volumeMounts []api.SandboxVolumeMount) SandboxOption { + return func(o *sandboxOptions) { + o.volumeMounts = volumeMounts + } +} + // WithConfig applies one or more ConnectionConfigOption to the sandbox. func WithConfig(configOpts ...ConnectionConfigOption) SandboxOption { return func(o *sandboxOptions) { @@ -86,12 +127,17 @@ func Create(ctx context.Context, template string, opts ...SandboxOption) (*Sandb } createOpts := CreateSandboxOpts{ - Template: template, - Timeout: options.timeout, - AutoPause: options.autoPause, - Metadata: options.metadata, - EnvVars: options.envVars, - Secure: options.secure, + Template: template, + Timeout: options.timeout, + AutoPause: options.autoPause, + AutoResume: options.autoResume, + Secure: options.secure, + AllowInternetAccess: options.allowInternetAccess, + Network: options.network, + Metadata: options.metadata, + EnvVars: options.envVars, + Mcp: options.mcp, + VolumeMounts: options.volumeMounts, } if createOpts.Timeout <= 0 { createOpts.Timeout = int32(defaultSandboxTimeout) diff --git a/e2b/sandbox_api.go b/e2b/sandbox_api.go index e796517..53c9f46 100644 --- a/e2b/sandbox_api.go +++ b/e2b/sandbox_api.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net/http" + "net/url" + "strings" "time" "github.com/openkruise/agents-api/e2b/api" @@ -11,18 +13,25 @@ import ( // SandboxInfo represents information about a sandbox. type SandboxInfo struct { - SandboxID string - TemplateID string - Alias string - ClientID string - StartedAt time.Time - EndAt time.Time - CpuCount int32 - MemoryMB int32 - DiskSizeMB int32 - EnvdVersion string - Metadata map[string]string - State string + SandboxID string + TemplateID string + Alias string + ClientID string + StartedAt time.Time + EndAt time.Time + CpuCount int32 + MemoryMB int32 + DiskSizeMB int32 + EnvdVersion string + Metadata map[string]string + State string + VolumeMounts []api.SandboxVolumeMount +} + +// ListResult represents the result of a list operation with pagination support. +type ListResult struct { + Sandboxes []SandboxInfo + NextToken string } // SandboxMetrics represents sandbox resource metrics. @@ -65,9 +74,46 @@ func NewSandboxApi(config *ConnectionConfig) *SandboxApi { } } -// List lists all running sandboxes. -func (s *SandboxApi) List(ctx context.Context) ([]SandboxInfo, error) { - resp, httpResp, err := s.apiClient.SandboxesApi.V2SandboxesGet(ctx).Execute() +// ListSandboxOpts contains options for listing sandboxes. +type ListSandboxOpts struct { + // Metadata filters sandboxes by metadata key-value pairs. + // Keys and values will be URL encoded automatically. + Metadata map[string]string + // State filters sandboxes by one or more states (e.g., "running", "paused"). + State []api.SandboxState + // NextToken is the cursor to start the list from (for pagination). + NextToken string + // Limit is the maximum number of items to return per page. + Limit int32 +} + +// List lists all sandboxes with pagination support. +// Returns a ListResult containing the sandboxes and a NextToken for fetching the next page. +func (s *SandboxApi) List(ctx context.Context, opts ...ListSandboxOpts) (*ListResult, error) { + req := s.apiClient.SandboxesApi.V2SandboxesGet(ctx) + + if len(opts) > 0 { + o := opts[0] + if len(o.Metadata) > 0 { + // Build metadata query string: key1=value1&key2=value2 + var pairs []string + for k, v := range o.Metadata { + pairs = append(pairs, url.QueryEscape(k)+"="+url.QueryEscape(v)) + } + req = req.Metadata(strings.Join(pairs, "&")) + } + if len(o.State) > 0 { + req = req.State(o.State) + } + if o.NextToken != "" { + req = req.NextToken(o.NextToken) + } + if o.Limit > 0 { + req = req.Limit(o.Limit) + } + } + + resp, httpResp, err := req.Execute() if err != nil { return nil, fmt.Errorf("failed to list sandboxes: %w", err) } @@ -77,24 +123,42 @@ func (s *SandboxApi) List(ctx context.Context) ([]SandboxInfo, error) { sandboxes := make([]SandboxInfo, len(resp)) for i, sb := range resp { - sandboxes[i] = SandboxInfo{ - SandboxID: sb.GetSandboxID(), - TemplateID: sb.GetTemplateID(), - ClientID: sb.GetClientID(), - StartedAt: sb.GetStartedAt(), - EndAt: sb.GetEndAt(), - CpuCount: sb.GetCpuCount(), - MemoryMB: sb.GetMemoryMB(), - DiskSizeMB: sb.GetDiskSizeMB(), - EnvdVersion: sb.GetEnvdVersion(), - State: string(sb.GetState()), - } - if meta, ok := sb.GetMetadataOk(); ok && meta != nil { - sandboxes[i].Metadata = *meta - } + sandboxes[i] = convertToSandboxInfo(sb) } - return sandboxes, nil + // Extract nextToken from response header for pagination + nextToken := httpResp.Header.Get("x-next-token") + + return &ListResult{ + Sandboxes: sandboxes, + NextToken: nextToken, + }, nil +} + +// convertToSandboxInfo converts an API response item to SandboxInfo. +func convertToSandboxInfo(sb api.SandboxesGet200ResponseInner) SandboxInfo { + info := SandboxInfo{ + SandboxID: sb.GetSandboxID(), + TemplateID: sb.GetTemplateID(), + ClientID: sb.GetClientID(), + StartedAt: sb.GetStartedAt(), + EndAt: sb.GetEndAt(), + CpuCount: sb.GetCpuCount(), + MemoryMB: sb.GetMemoryMB(), + DiskSizeMB: sb.GetDiskSizeMB(), + EnvdVersion: sb.GetEnvdVersion(), + State: string(sb.GetState()), + } + if alias, ok := sb.GetAliasOk(); ok && alias != nil { + info.Alias = *alias + } + if meta, ok := sb.GetMetadataOk(); ok && meta != nil { + info.Metadata = *meta + } + if mounts, ok := sb.GetVolumeMountsOk(); ok && mounts != nil { + info.VolumeMounts = mounts + } + return info } // GetInfo retrieves information about a specific sandbox. @@ -119,9 +183,15 @@ func (s *SandboxApi) GetInfo(ctx context.Context, sandboxID string) (*SandboxInf EnvdVersion: resp.GetEnvdVersion(), State: string(resp.GetState()), } + if alias, ok := resp.GetAliasOk(); ok && alias != nil { + info.Alias = *alias + } if meta, ok := resp.GetMetadataOk(); ok && meta != nil { info.Metadata = *meta } + if mounts, ok := resp.GetVolumeMountsOk(); ok && mounts != nil { + info.VolumeMounts = mounts + } return info, nil } @@ -178,6 +248,24 @@ func (s *SandboxApi) CreateSandbox(ctx context.Context, opts CreateSandboxOpts) if opts.AutoPause != nil { body.SetAutoPause(*opts.AutoPause) } + if opts.Secure { + body.SetSecure(opts.Secure) + } + if opts.AutoResume != nil { + body.SetAutoResume(*opts.AutoResume) + } + if opts.AllowInternetAccess != nil { + body.SetAllowInternetAccess(*opts.AllowInternetAccess) + } + if opts.Network != nil { + body.SetNetwork(*opts.Network) + } + if opts.Mcp != nil { + body.SetMcp(opts.Mcp) + } + if opts.VolumeMounts != nil { + body.SetVolumeMounts(opts.VolumeMounts) + } resp, httpResp, err := s.apiClient.SandboxesApi.SandboxesPost(ctx). CreateSandboxRequest(*body).Execute() @@ -250,10 +338,15 @@ func (s *SandboxApi) Pause(ctx context.Context, sandboxID string) (string, error // CreateSandboxOpts contains options for creating a sandbox. type CreateSandboxOpts struct { - Template string - Timeout int32 - AutoPause *bool - Metadata map[string]string - EnvVars map[string]string - Secure bool + Template string + Timeout int32 + AutoPause *bool + AutoResume *api.SandboxAutoResumeConfig + Secure bool + AllowInternetAccess *bool + Network *api.SandboxNetworkConfig + Metadata map[string]string + EnvVars map[string]string + Mcp map[string]interface{} + VolumeMounts []api.SandboxVolumeMount } diff --git a/examples/e2b-example/main.go b/examples/e2b-example/main.go index 45096b3..b40d94b 100644 --- a/examples/e2b-example/main.go +++ b/examples/e2b-example/main.go @@ -40,6 +40,10 @@ func main() { fmt.Println("\n--- Listing existing sandboxes ---") listSandboxes(ctx, api) + // ========== 2.1. List with Pagination ========== + fmt.Println("\n--- Listing sandboxes with pagination (2 per page) ---") + listSandboxesWithPagination(ctx, api) + // ========== 3. Create Sandbox ========== fmt.Println("\n--- Creating sandbox ---") sb, err := sandbox.Create(ctx, template, @@ -71,15 +75,58 @@ func main() { // listSandboxes lists all running sandboxes. func listSandboxes(ctx context.Context, api *sandbox.SandboxApi) { - sandboxes, err := api.List(ctx) + result, err := api.List(ctx) if err != nil { fmt.Printf("Error listing sandboxes: %v\n", err) return } - fmt.Printf("Total sandboxes: %d\n", len(sandboxes)) - for _, s := range sandboxes { + fmt.Printf("Total sandboxes: %d\n", len(result.Sandboxes)) + for _, s := range result.Sandboxes { fmt.Printf(" - ID: %s, Template: %s, State: %s\n", s.SandboxID, s.TemplateID, s.State) } + if result.NextToken != "" { + fmt.Printf(" Next page token: %s\n", result.NextToken) + } +} + +// listSandboxesWithPagination demonstrates paginated listing of all sandboxes. +// Example: List all 6 sandboxes with 2 per page. +func listSandboxesWithPagination(ctx context.Context, api *sandbox.SandboxApi) { + var allSandboxes []sandbox.SandboxInfo + pageSize := int32(2) + nextToken := "" + page := 1 + + fmt.Println("\n--- Listing all sandboxes with pagination ---") + for { + opts := sandbox.ListSandboxOpts{ + Limit: pageSize, + } + if nextToken != "" { + opts.NextToken = nextToken + } + + result, err := api.List(ctx, opts) + if err != nil { + fmt.Printf("Error listing sandboxes on page %d: %v\n", page, err) + return + } + + fmt.Printf("Page %d: fetched %d sandboxes\n", page, len(result.Sandboxes)) + allSandboxes = append(allSandboxes, result.Sandboxes...) + + // Check if there are more pages + if result.NextToken == "" { + break + } + nextToken = result.NextToken + page++ + } + + fmt.Printf("\nTotal sandboxes fetched: %d\n", len(allSandboxes)) + for i, s := range allSandboxes { + fmt.Printf(" %d. ID: %s, Template: %s, State: %s\n", i+1, s.SandboxID, s.TemplateID, s.State) + } } // showSandboxInfo prints detailed information about the sandbox. diff --git a/runtime/config.go b/runtime/config.go index 6ba06ab..61fbd90 100644 --- a/runtime/config.go +++ b/runtime/config.go @@ -46,6 +46,10 @@ type Config struct { // RequestTimeout is the timeout applied to the underlying HTTP client. RequestTimeout time.Duration + // CustomHTTPClient is a custom HTTP client to use for runtime requests. + // If nil, a default client with RequestTimeout will be created. + CustomHTTPClient *http.Client + // httpClient is a lazily-initialized shared HTTP client. // All sandbox clients created from this Config share the same Transport/connection pool. httpClient *http.Client @@ -143,6 +147,12 @@ func WithRequestTimeout(d time.Duration) Option { return func(c *Config) { c.RequestTimeout = d } } +// WithHTTPClient sets a custom HTTP client for runtime requests. +// This allows users to configure TLS certificates, proxies, etc. +func WithHTTPClient(httpClient *http.Client) Option { + return func(c *Config) { c.CustomHTTPClient = httpClient } +} + // WithConfig replaces the working Config with a pre-built one. func WithConfig(cfg *Config) Option { return func(c *Config) { @@ -196,9 +206,15 @@ func (c *Config) SandboxHeaders(sandboxID string) map[string]string { } // HTTPClient returns the lazily-initialized shared http.Client. +// If a custom HTTPClient was provided via WithHTTPClient, it will be used. +// Otherwise, a default client with RequestTimeout will be created. func (c *Config) HTTPClient() *http.Client { c.httpOnce.Do(func() { - c.httpClient = &http.Client{Timeout: c.RequestTimeout} + if c.CustomHTTPClient != nil { + c.httpClient = c.CustomHTTPClient + } else { + c.httpClient = &http.Client{Timeout: c.RequestTimeout} + } }) return c.httpClient }