yao/agent/context/jsapi_sandbox.go
Max 0a965c383d Add sandbox ID and VNC URL methods to sandbox executor
- Implement GetSandboxID method to return a mock sandbox ID for testing.
- Add GetVNCUrl method to return an empty string for VNC access in tests.
- Update SandboxExecutor interface to include new methods for sandbox identification and VNC URL retrieval.
- Enhance context creation to set sandbox ID and VNC URL properties in the sandbox instance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 21:46:07 +08:00

302 lines
8.4 KiB
Go

package context
import (
"context"
"github.com/yaoapp/gou/runtime/v8/bridge"
openapiSandbox "github.com/yaoapp/yao/openapi/sandbox"
infraSandbox "github.com/yaoapp/yao/sandbox"
"rogchap.com/v8go"
)
// SandboxExecutor defines the interface for sandbox operations
// This interface is implemented by agent/sandbox.Executor
// It's defined here to avoid import cycles
type SandboxExecutor interface {
// Filesystem operations
ReadFile(ctx context.Context, path string) ([]byte, error)
WriteFile(ctx context.Context, path string, content []byte) error
ListDir(ctx context.Context, path string) ([]infraSandbox.FileInfo, error)
// Command execution
Exec(ctx context.Context, cmd []string) (string, error)
// Workspace info
GetWorkDir() string
// Sandbox identification
GetSandboxID() string
// VNC access (returns empty string if not available)
GetVNCUrl() string
}
// SetSandboxExecutor sets the sandbox executor for this context
// This should be called before hooks are executed
func (ctx *Context) SetSandboxExecutor(executor SandboxExecutor) {
ctx.sandboxExecutor = executor
}
// GetSandboxExecutor returns the sandbox executor if available
func (ctx *Context) GetSandboxExecutor() SandboxExecutor {
return ctx.sandboxExecutor
}
// HasSandbox returns true if sandbox executor is available
func (ctx *Context) HasSandbox() bool {
return ctx.sandboxExecutor != nil
}
// newSandboxObject creates the ctx.sandbox JavaScript object
// Returns nil if sandbox executor is not available
func (ctx *Context) newSandboxObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
if ctx.sandboxExecutor == nil {
return nil
}
sandboxObj := v8go.NewObjectTemplate(iso)
// Set methods
sandboxObj.Set("ReadFile", ctx.sandboxReadFileMethod(iso))
sandboxObj.Set("WriteFile", ctx.sandboxWriteFileMethod(iso))
sandboxObj.Set("ListDir", ctx.sandboxListDirMethod(iso))
sandboxObj.Set("Exec", ctx.sandboxExecMethod(iso))
sandboxObj.Set("GetVNCUrl", ctx.sandboxGetVNCUrlMethod(iso))
sandboxObj.Set("GetSandboxID", ctx.sandboxGetSandboxIDMethod(iso))
return sandboxObj
}
// createSandboxInstance creates the sandbox object instance with workdir property
func (ctx *Context) createSandboxInstance(v8ctx *v8go.Context) *v8go.Value {
if ctx.sandboxExecutor == nil {
return nil
}
sandboxTemplate := ctx.newSandboxObject(v8ctx.Isolate())
if sandboxTemplate == nil {
return nil
}
// Set workdir as a property
sandboxTemplate.Set("workdir", ctx.sandboxExecutor.GetWorkDir())
// Set sandbox_id as a property
sandboxID := ctx.sandboxExecutor.GetSandboxID()
sandboxTemplate.Set("sandbox_id", sandboxID)
// Set vnc_url as a property (empty string if not available)
// GetVNCUrl returns sandbox ID if VNC is supported, empty otherwise
vncSandboxID := ctx.sandboxExecutor.GetVNCUrl()
if vncSandboxID != "" {
sandboxTemplate.Set("vnc_url", openapiSandbox.GetVNCClientURL(vncSandboxID))
} else {
sandboxTemplate.Set("vnc_url", "")
}
instance, err := sandboxTemplate.NewInstance(v8ctx)
if err != nil {
return nil
}
return instance.Value
}
// sandboxReadFileMethod implements ctx.sandbox.ReadFile(path)
func (ctx *Context) sandboxReadFileMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
if ctx.sandboxExecutor == nil {
return bridge.JsException(v8ctx, "sandbox executor not available")
}
if len(args) < 1 {
return bridge.JsException(v8ctx, "ReadFile requires path parameter")
}
path := args[0].String()
content, err := ctx.sandboxExecutor.ReadFile(context.Background(), path)
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
// Return as string
jsVal, err := v8go.NewValue(iso, string(content))
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
return jsVal
})
}
// sandboxWriteFileMethod implements ctx.sandbox.WriteFile(path, content)
func (ctx *Context) sandboxWriteFileMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
if ctx.sandboxExecutor == nil {
return bridge.JsException(v8ctx, "sandbox executor not available")
}
if len(args) < 2 {
return bridge.JsException(v8ctx, "WriteFile requires path and content parameters")
}
path := args[0].String()
content := args[1].String()
err := ctx.sandboxExecutor.WriteFile(context.Background(), path, []byte(content))
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
// Return undefined on success
return v8go.Undefined(iso)
})
}
// sandboxListDirMethod implements ctx.sandbox.ListDir(path)
func (ctx *Context) sandboxListDirMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
if ctx.sandboxExecutor == nil {
return bridge.JsException(v8ctx, "sandbox executor not available")
}
if len(args) < 1 {
return bridge.JsException(v8ctx, "ListDir requires path parameter")
}
path := args[0].String()
files, err := ctx.sandboxExecutor.ListDir(context.Background(), path)
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
// Convert to JavaScript array of objects
result := make([]map[string]interface{}, len(files))
for i, f := range files {
result[i] = map[string]interface{}{
"name": f.Name,
"size": f.Size,
"is_dir": f.IsDir,
}
}
jsVal, err := bridge.JsValue(v8ctx, result)
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
return jsVal
})
}
// sandboxExecMethod implements ctx.sandbox.Exec(cmd)
func (ctx *Context) sandboxExecMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
if ctx.sandboxExecutor == nil {
return bridge.JsException(v8ctx, "sandbox executor not available")
}
if len(args) < 1 {
return bridge.JsException(v8ctx, "Exec requires cmd parameter (array of strings)")
}
// Parse command array
cmdArg := args[0]
if !cmdArg.IsArray() {
return bridge.JsException(v8ctx, "Exec requires cmd to be an array of strings")
}
cmdObj, err := cmdArg.AsObject()
if err != nil {
return bridge.JsException(v8ctx, "failed to parse cmd array: "+err.Error())
}
// Get array length
lengthVal, err := cmdObj.Get("length")
if err != nil {
return bridge.JsException(v8ctx, "failed to get cmd array length: "+err.Error())
}
length := int(lengthVal.Integer())
// Build command slice
cmd := make([]string, length)
for i := 0; i < length; i++ {
itemVal, err := cmdObj.GetIdx(uint32(i))
if err != nil {
return bridge.JsException(v8ctx, "failed to get cmd array element: "+err.Error())
}
cmd[i] = itemVal.String()
}
output, err := ctx.sandboxExecutor.Exec(context.Background(), cmd)
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
jsVal, err := v8go.NewValue(v8ctx.Isolate(), output)
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
return jsVal
})
}
// sandboxGetVNCUrlMethod implements ctx.sandbox.GetVNCUrl()
func (ctx *Context) sandboxGetVNCUrlMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
if ctx.sandboxExecutor == nil {
return bridge.JsException(v8ctx, "sandbox executor not available")
}
// GetVNCUrl returns sandbox ID if VNC is supported, empty otherwise
vncSandboxID := ctx.sandboxExecutor.GetVNCUrl()
vncUrl := ""
if vncSandboxID != "" {
vncUrl = openapiSandbox.GetVNCClientURL(vncSandboxID)
}
jsVal, err := v8go.NewValue(iso, vncUrl)
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
return jsVal
})
}
// sandboxGetSandboxIDMethod implements ctx.sandbox.GetSandboxID()
func (ctx *Context) sandboxGetSandboxIDMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
if ctx.sandboxExecutor == nil {
return bridge.JsException(v8ctx, "sandbox executor not available")
}
sandboxID := ctx.sandboxExecutor.GetSandboxID()
jsVal, err := v8go.NewValue(iso, sandboxID)
if err != nil {
return bridge.JsException(v8ctx, err.Error())
}
return jsVal
})
}