- 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
167 lines
6.6 KiB
Go
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())
|
|
}
|