yao/sandbox/v2/jsapi/jsapi.go
Max df420978f0 feat(workspace): implement JSAPI with archive support and full test coverage
- tai/volume: add WithRemotePath sync option, 8 archive/compression methods
  (Zip/Unzip/Gzip/Gunzip/Tar/Untar/Tgz/Untgz) with local+remote impls
- workspace/jsapi: implement 24-method WorkspaceFS (file I/O, Copy with
  local:///tmp:// URI dispatch, archive ops) and 4 static methods
- workspace/manager: add Rename, MkdirAll, Volume() accessors via singleton
- Tests: comprehensive Go tests for tai/volume, workspace, workspace/jsapi
  covering both local and remote modes via SANDBOX_TEST_REMOTE_ADDR
- CI: update tai Docker image to latest in pr-test.yml
- sandbox/v2: add JSAPI docs and host/node module stubs

Made-with: Cursor
2026-03-08 14:17:39 +08:00

167 lines
6.6 KiB
Go

// Package jsapi registers the sandbox namespace into the Yao V8 runtime.
//
// All methods are static on the sandbox object — no constructor.
//
// # JavaScript API
//
// const box = sandbox.Create({ image: "node:20", owner: "user1" })
// const result = box.Exec(["node", "-e", "console.log('hi')"])
// console.log(result.stdout)
//
// const box = sandbox.Get(id) // → Box
// const list = sandbox.List({ owner: "u1" }) // → BoxInfo[]
// sandbox.Delete(id) // → void
// const host = sandbox.Host("gpu") // → Host (host_exec on Tai)
// const node = sandbox.GetNode("tai-abc123") // → NodeInfo | null
// const all = sandbox.Nodes() // → NodeInfo[]
// const team = sandbox.NodesByTeam("t-001") // → NodeInfo[]
//
// # Go mapping
//
// sandbox.Create(opts) → Manager.Create(ctx, CreateOptions) → Box
// sandbox.Create(opts) → Manager.GetOrCreate(ctx, opts) → Box (when opts.id is set)
// sandbox.Get(id) → Manager.Get(ctx, id) → Box
// sandbox.List(filter?) → Manager.List(ctx, ListOptions) → []*Box → BoxInfo[]
// sandbox.Delete(id) → Manager.Remove(ctx, id) → void
// sandbox.Host(pool?) → Manager.Host(ctx, pool) → Host
// sandbox.GetNode(id) → registry.Global().Get(id) → NodeInfo | null
// sandbox.Nodes() → registry.Global().List() → NodeInfo[]
// sandbox.NodesByTeam(t)→ registry.Global().ListByTeam(t) → NodeInfo[]
//
// Registration happens via init() — import with:
//
// _ "github.com/yaoapp/yao/sandbox/v2/jsapi"
package jsapi
import (
v8 "github.com/yaoapp/gou/runtime/v8"
"rogchap.com/v8go"
)
func init() {
v8.RegisterObject("sandbox", ExportObject)
}
// ExportObject exports the sandbox namespace object to V8.
func ExportObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
obj := v8go.NewObjectTemplate(iso)
obj.Set("Create", v8go.NewFunctionTemplate(iso, sbCreate))
obj.Set("Get", v8go.NewFunctionTemplate(iso, sbGet))
obj.Set("List", v8go.NewFunctionTemplate(iso, sbList))
obj.Set("Delete", v8go.NewFunctionTemplate(iso, sbDelete))
obj.Set("Host", v8go.NewFunctionTemplate(iso, sbHost))
obj.Set("GetNode", v8go.NewFunctionTemplate(iso, sbGetNode))
obj.Set("Nodes", v8go.NewFunctionTemplate(iso, sbNodes))
obj.Set("NodesByTeam", v8go.NewFunctionTemplate(iso, sbNodesByTeam))
return obj
}
// sbCreate: `sandbox.Create(options)` → Box
//
// Go: Manager.Create(ctx, CreateOptions) (*Box, error)
//
// Manager.GetOrCreate(ctx, CreateOptions) (*Box, error) — when opts.id is set
//
// JS options → Go CreateOptions mapping:
//
// {
// id: string → CreateOptions.ID // optional; triggers GetOrCreate
// owner: string → CreateOptions.Owner // required
// pool: string → CreateOptions.Pool // default: first pool
// image: string → CreateOptions.Image // required
// workdir: string → CreateOptions.WorkDir
// user: string → CreateOptions.User // e.g. "1000:1000"
// env: object → CreateOptions.Env // map[string]string
// memory: number → CreateOptions.Memory // bytes (int64)
// cpus: number → CreateOptions.CPUs // float64 e.g. 1.5
// vnc: boolean → CreateOptions.VNC
// ports: array → CreateOptions.Ports // [{container_port, host_port, host_ip, protocol}] → []PortMapping
// policy: string → CreateOptions.Policy // "oneshot"|"session"|"longrunning"|"persistent"
// idle_timeout: number → CreateOptions.IdleTimeout // ms → time.Duration
// stop_timeout: number → CreateOptions.StopTimeout // ms → time.Duration
// workspace_id: string → CreateOptions.WorkspaceID
// mount_mode: string → CreateOptions.MountMode // "rw"|"ro"
// mount_path: string → CreateOptions.MountPath
// labels: object → CreateOptions.Labels // map[string]string
// }
//
// Returns: Box object (see box.go)
func sbCreate(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2
// 1. Parse options from info.Args()[0]
// 2. Validate required fields (image, owner)
// 3. If opts.id != "" → sandbox.M().GetOrCreate(ctx, opts)
// else → sandbox.M().Create(ctx, opts)
// 4. Return NewBoxObject(v8ctx, box.ID())
return v8go.Undefined(info.Context().Isolate())
}
// sbGet: `sandbox.Get(id)` → Box | null
//
// Go: Manager.Get(ctx, id) (*Box, error)
//
// Args:
//
// id: string — sandbox ID
//
// Returns: Box object if found, null if not found
func sbGet(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2
// 1. id = info.Args()[0].String()
// 2. box, err := sandbox.M().Get(ctx, id)
// 3. Return NewBoxObject(v8ctx, id) or null
return v8go.Undefined(info.Context().Isolate())
}
// sbList: `sandbox.List(filter?)` → BoxInfo[]
//
// Go: Manager.List(ctx, ListOptions) ([]*Box, error)
//
// then Box.Info(ctx) for each → BoxInfo
//
// JS filter → Go ListOptions mapping:
//
// {
// owner: string → ListOptions.Owner // filter by owner; empty = all
// pool: string → ListOptions.Pool // filter by pool; empty = all
// labels: object → ListOptions.Labels // filter by labels
// }
//
// Returns: BoxInfo[] — each element:
//
// {
// id: string ← BoxInfo.ID
// container_id: string ← BoxInfo.ContainerID
// pool: string ← BoxInfo.Pool
// owner: string ← BoxInfo.Owner
// status: string ← BoxInfo.Status
// image: string ← BoxInfo.Image
// vnc: boolean ← BoxInfo.VNC
// policy: string ← BoxInfo.Policy
// labels: object ← BoxInfo.Labels
// created_at: string ← BoxInfo.CreatedAt (ISO 8601)
// last_active: string ← BoxInfo.LastActive (ISO 8601)
// process_count: number ← BoxInfo.ProcessCount
// }
func sbList(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2
// 1. Parse optional filter from info.Args()[0]
// 2. boxes := sandbox.M().List(ctx, opts)
// 3. For each box: box.Info(ctx) → BoxInfo → JS object
// 4. Return JS array of BoxInfo objects
return v8go.Undefined(info.Context().Isolate())
}
// sbDelete: `sandbox.Delete(id)` → void
//
// Go: Manager.Remove(ctx, id) error
//
// Args:
//
// id: string — sandbox ID to remove
func sbDelete(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2
// 1. id = info.Args()[0].String()
// 2. sandbox.M().Remove(ctx, id)
return v8go.Undefined(info.Context().Isolate())
}