- 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>
302 lines
8.4 KiB
Go
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
|
|
})
|
|
}
|