chore: update unit-test workflow to use latest Tai image
- Change Docker image references in the unit-test workflow from yaoapp/tai:1.2.0 to yaoapp/tai:latest for consistency and to ensure the latest features and fixes are utilized. - Update related documentation to reflect the unified Computer interface in the sandbox, replacing Box and Host references with Computer. Made-with: Cursor
This commit is contained in:
parent
4d4b03be96
commit
43fd532357
8 changed files with 426 additions and 492 deletions
6
.github/workflows/unit-test.yml
vendored
6
.github/workflows/unit-test.yml
vendored
|
|
@ -782,7 +782,7 @@ jobs:
|
|||
- name: Pull Test Images
|
||||
run: |
|
||||
docker pull yaoapp/tai-sandbox-test:latest || true
|
||||
docker pull yaoapp/tai:1.2.0
|
||||
docker pull yaoapp/tai:latest
|
||||
docker pull alpine:latest
|
||||
|
||||
- name: Install k3d
|
||||
|
|
@ -799,7 +799,7 @@ jobs:
|
|||
docker run -d --name tai-docker \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 19100:19100 -p 8099:8099 -p 12375:12375 -p 16080:16080 \
|
||||
yaoapp/tai:1.2.0 server \
|
||||
yaoapp/tai:latest server \
|
||||
-grpc 0.0.0.0:19100 -http 0.0.0.0:8099 -vnc 0.0.0.0:16080 -docker 0.0.0.0:12375
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
|
|
@ -853,7 +853,7 @@ jobs:
|
|||
-v /tmp/kubeconfig-tai-k8s.yml:/etc/tai/kubeconfig.yml:ro \
|
||||
-e TAI_K8S_UPSTREAM="tcp://${K3D_IP}:6443" \
|
||||
-e TAI_KUBECONFIG=/etc/tai/kubeconfig.yml \
|
||||
yaoapp/tai:1.2.0 server \
|
||||
yaoapp/tai:latest server \
|
||||
-grpc 0.0.0.0:19100 -http 0.0.0.0:8099 -vnc 0.0.0.0:16080 -k8s 0.0.0.0:16443
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
|
|
|
|||
|
|
@ -649,8 +649,8 @@ sandbox/v2/
|
|||
├── grpc.go // token creation/revocation, gRPC env var injection
|
||||
├── jsapi/ // (Phase 2) V8 JSAPI sandbox.* namespace
|
||||
│ ├── jsapi.go // RegisterObject("sandbox"), Create/Get/List/Delete/Host
|
||||
│ ├── box.go // Box JS object: Computer + Attach/Info/Start/Stop/Remove
|
||||
│ └── host.go // Host JS object: Computer (unified with Box)
|
||||
│ ├── computer.go // Unified Computer JS object (box + host), sbHost()
|
||||
│ └── node.go // GetNode/Nodes/NodesByTeam JS bindings
|
||||
├── export_test.go // ResetForTest() for test isolation
|
||||
├── testutils_test.go // shared test helpers (multi-pool setup)
|
||||
├── sandbox_test.go // Init/M singleton tests
|
||||
|
|
|
|||
|
|
@ -5,27 +5,29 @@ All methods are available on the global `sandbox` object. No constructor needed.
|
|||
## Quick Start
|
||||
|
||||
```javascript
|
||||
// Create a sandbox container
|
||||
const box = sandbox.Create({ image: "node:20", owner: "user-123" })
|
||||
|
||||
// Execute a command
|
||||
const result = box.Exec(["node", "-e", "console.log('hello')"])
|
||||
// Create a container computer
|
||||
const pc = sandbox.Create({ image: "node:20", owner: "user-123" })
|
||||
const result = pc.Exec(["node", "-e", "console.log('hello')"])
|
||||
console.log(result.stdout) // "hello\n"
|
||||
pc.Remove()
|
||||
|
||||
// Clean up
|
||||
box.Remove()
|
||||
// Or use the host directly (no container)
|
||||
const host = sandbox.Host()
|
||||
host.Exec(["ls", "-la", "/workspace"])
|
||||
```
|
||||
|
||||
Both `sandbox.Create()` and `sandbox.Host()` return a **Computer** object with the same interface. The `kind` property tells you which type it is.
|
||||
|
||||
---
|
||||
|
||||
## Static Methods
|
||||
|
||||
### sandbox.Create(options) → Box
|
||||
### sandbox.Create(options) → Computer
|
||||
|
||||
Create a new sandbox container. If `options.id` is set and a sandbox with that ID already exists, returns the existing one (GetOrCreate semantics).
|
||||
Create a new sandbox container. Returns a Computer (`kind = "box"`). If `options.id` is set and a sandbox with that ID already exists, returns the existing one (GetOrCreate semantics).
|
||||
|
||||
```javascript
|
||||
const box = sandbox.Create({
|
||||
const pc = sandbox.Create({
|
||||
image: "node:20", // required — container image
|
||||
owner: "user-123", // required — owner identifier
|
||||
pool: "gpu", // optional — pool name (default: first pool)
|
||||
|
|
@ -34,7 +36,7 @@ const box = sandbox.Create({
|
|||
user: "1000:1000", // optional — UID:GID
|
||||
env: { NODE_ENV: "dev" },// optional — environment variables
|
||||
memory: 536870912, // optional — memory limit in bytes (512MB)
|
||||
cpus: 1.5, // optional — CPU limit
|
||||
cpus: 1.5, // optional — CPU limit
|
||||
vnc: true, // optional — enable VNC desktop
|
||||
ports: [ // optional — port mappings
|
||||
{ container_port: 3000, host_port: 3000, host_ip: "", protocol: "tcp" }
|
||||
|
|
@ -49,14 +51,14 @@ const box = sandbox.Create({
|
|||
})
|
||||
```
|
||||
|
||||
### sandbox.Get(id) → Box | null
|
||||
### sandbox.Get(id) → Computer | null
|
||||
|
||||
Get an existing sandbox by ID. Returns `null` if not found.
|
||||
Get an existing sandbox by ID. Returns a Computer (`kind = "box"`) or `null` if not found.
|
||||
|
||||
```javascript
|
||||
const box = sandbox.Get("my-sandbox")
|
||||
if (box) {
|
||||
console.log(box.id, box.owner, box.pool)
|
||||
const pc = sandbox.Get("my-sandbox")
|
||||
if (pc) {
|
||||
console.log(pc.kind, pc.id, pc.owner, pc.pool)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -102,9 +104,9 @@ Remove a sandbox and its container.
|
|||
sandbox.Delete("my-sandbox")
|
||||
```
|
||||
|
||||
### sandbox.Host(pool?) → Host
|
||||
### sandbox.Host(pool?) → Computer
|
||||
|
||||
Get a Host object for executing commands directly on the Tai host machine (no container). Only available when the pool's Tai server has `host_exec` capability.
|
||||
Get a Computer (`kind = "host"`) for executing commands directly on the Tai host machine (no container). Only available when the pool's Tai server has `host_exec` capability.
|
||||
|
||||
```javascript
|
||||
const host = sandbox.Host() // default pool
|
||||
|
|
@ -143,196 +145,35 @@ const nodes = sandbox.NodesByTeam("team-001")
|
|||
|
||||
---
|
||||
|
||||
## Box Object
|
||||
## Computer Object
|
||||
|
||||
Returned by `sandbox.Create()` and `sandbox.Get()`. Holds a sandbox ID internally; all operations delegate to the backend.
|
||||
Returned by `sandbox.Create()`, `sandbox.Get()`, and `sandbox.Host()`. This is the unified interface for all execution environments — containers and bare-metal hosts.
|
||||
|
||||
Use the `kind` property to check the type. Methods marked **box-only** throw an error when called on a host computer.
|
||||
|
||||
### Properties (read-only)
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `box.id` | string | Sandbox ID |
|
||||
| `box.owner` | string | Owner identifier |
|
||||
| `box.pool` | string | Pool name |
|
||||
| `pc.kind` | string | `"box"` or `"host"` |
|
||||
| `pc.id` | string | Sandbox ID (box-only; empty for host) |
|
||||
| `pc.owner` | string | Owner identifier (box-only; empty for host) |
|
||||
| `pc.pool` | string | Pool name |
|
||||
|
||||
### box.Exec(cmd, options?) → ExecResult
|
||||
### pc.Exec(cmd, options?) → ExecResult
|
||||
|
||||
Execute a command in the container and wait for it to finish.
|
||||
Execute a command and wait for it to finish.
|
||||
|
||||
```javascript
|
||||
const result = box.Exec(["ls", "-la", "/app"])
|
||||
const result = pc.Exec(["ls", "-la", "/app"])
|
||||
console.log(result.exit_code) // 0
|
||||
console.log(result.stdout) // file listing
|
||||
console.log(result.stderr) // empty string
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
```javascript
|
||||
box.Exec(["npm", "test"], {
|
||||
workdir: "/app",
|
||||
env: { CI: "true" },
|
||||
timeout: 60000 // ms
|
||||
})
|
||||
```
|
||||
|
||||
Return value:
|
||||
|
||||
```javascript
|
||||
{
|
||||
exit_code: 0, // process exit code
|
||||
stdout: "...", // captured stdout (string)
|
||||
stderr: "..." // captured stderr (string)
|
||||
}
|
||||
```
|
||||
|
||||
### box.Stream(cmd, callback) / box.Stream(cmd, options, callback)
|
||||
|
||||
Execute a command with streaming output via callback. The call blocks until the process exits.
|
||||
|
||||
Callback signature: `function(type, data)`
|
||||
- `type = "stdout"` → `data` is a string chunk from stdout
|
||||
- `type = "stderr"` → `data` is a string chunk from stderr
|
||||
- `type = "exit"` → `data` is the exit code (number)
|
||||
|
||||
```javascript
|
||||
// Basic
|
||||
box.Stream(["npm", "run", "dev"], function(type, data) {
|
||||
if (type === "stdout") console.log(data)
|
||||
if (type === "stderr") console.log("[ERR]", data)
|
||||
if (type === "exit") console.log("exited:", data)
|
||||
})
|
||||
|
||||
// With options
|
||||
box.Stream(["npm", "test"], {
|
||||
workdir: "/app",
|
||||
env: { CI: "true" },
|
||||
timeout: 60000
|
||||
}, function(type, data) {
|
||||
console.log(type, data)
|
||||
})
|
||||
```
|
||||
|
||||
### box.Attach(port, options?) → string
|
||||
|
||||
Get a WebSocket or SSE endpoint URL for a service running inside the container. Use this for persistent connections (WS/SSE). For plain HTTP requests, use `box.Proxy()` instead.
|
||||
|
||||
```javascript
|
||||
// Get WebSocket URL
|
||||
const wsURL = box.Attach(3000, { protocol: "ws", path: "/ws" })
|
||||
// "ws://host:8099/container-id:3000/ws"
|
||||
|
||||
// Get SSE URL
|
||||
const sseURL = box.Attach(8080, { protocol: "sse", path: "/events" })
|
||||
// "http://host:8099/container-id:8080/events"
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
```javascript
|
||||
{
|
||||
protocol: "ws" | "sse", // default "ws"; affects URL scheme (ws:// vs http://)
|
||||
path: "/ws" // optional URL path suffix
|
||||
}
|
||||
```
|
||||
|
||||
### box.VNC() → string
|
||||
|
||||
Get the VNC WebSocket URL for a VNC-enabled sandbox.
|
||||
|
||||
```javascript
|
||||
const url = box.VNC()
|
||||
// "ws://host:16080/vnc/sb-xxx"
|
||||
```
|
||||
|
||||
### box.Proxy(port, path?) → string
|
||||
|
||||
Get an HTTP proxy URL for a port inside the container. Use this for plain HTTP requests. For WebSocket/SSE connections, use `box.Attach()` instead.
|
||||
|
||||
```javascript
|
||||
const url = box.Proxy(3000)
|
||||
// "http://host:8099/proxy/sb-xxx/3000/"
|
||||
|
||||
const url = box.Proxy(8080, "/api/v1")
|
||||
// "http://host:8099/proxy/sb-xxx/8080/api/v1"
|
||||
```
|
||||
|
||||
### box.Workspace() → WorkspaceFS
|
||||
|
||||
Access the workspace filesystem bound to this sandbox. The WorkspaceFS object is implemented in the `workspace/jsapi` package; this method returns it directly by calling `workspace.NewFSObject(v8ctx, box.WorkspaceID())`.
|
||||
|
||||
```javascript
|
||||
const ws = box.Workspace()
|
||||
const content = ws.ReadFile("src/main.go")
|
||||
ws.WriteFile("src/main.go", "package main\n...")
|
||||
ws.MkdirAll("src/utils")
|
||||
const entries = ws.ReadDir("src/")
|
||||
```
|
||||
|
||||
See [WorkspaceFS Object](#workspacefs-object) for the full method list.
|
||||
|
||||
### box.Info() → BoxInfo
|
||||
|
||||
Get current status information.
|
||||
|
||||
```javascript
|
||||
const info = box.Info()
|
||||
console.log(info.status, info.process_count, info.last_active)
|
||||
```
|
||||
|
||||
Returns the same structure as elements in `sandbox.List()`.
|
||||
|
||||
### box.Start() → void
|
||||
|
||||
Start a stopped sandbox.
|
||||
|
||||
```javascript
|
||||
box.Start()
|
||||
```
|
||||
|
||||
### box.Stop() → void
|
||||
|
||||
Stop a running sandbox.
|
||||
|
||||
```javascript
|
||||
box.Stop()
|
||||
```
|
||||
|
||||
### box.Remove() → void
|
||||
|
||||
Remove the sandbox and its container.
|
||||
|
||||
```javascript
|
||||
box.Remove()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Host Object
|
||||
|
||||
Returned by `sandbox.Host()`. Executes commands directly on the Tai host machine without a container. Requires the pool to have `host_exec` capability.
|
||||
|
||||
### Properties (read-only)
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `host.pool` | string | Pool name |
|
||||
|
||||
### host.Exec(cmd, args, options?) → HostExecResult
|
||||
|
||||
Execute a command on the host and wait for it to finish.
|
||||
|
||||
```javascript
|
||||
const result = host.Exec("ls", ["-la", "/workspace"])
|
||||
console.log(result.exit_code) // 0
|
||||
console.log(result.stdout) // file listing
|
||||
console.log(result.duration_ms) // execution time
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
```javascript
|
||||
host.Exec("python3", ["train.py"], {
|
||||
pc.Exec(["python3", "train.py"], {
|
||||
workdir: "/workspace/ml",
|
||||
env: { CUDA_VISIBLE_DEVICES: "0" },
|
||||
stdin: "input data",
|
||||
|
|
@ -354,41 +195,189 @@ Return value:
|
|||
}
|
||||
```
|
||||
|
||||
### host.Stream(cmd, args, callback) / host.Stream(cmd, args, options, callback)
|
||||
### pc.Stream(cmd, callback) / pc.Stream(cmd, options, callback)
|
||||
|
||||
Execute a command on the host with streaming output via callback. The call blocks until the process exits.
|
||||
Execute a command with streaming output via callback. The call blocks until the process exits.
|
||||
|
||||
Callback signature: same as `box.Stream` — `function(type, data)`.
|
||||
Callback signature: `function(type, data)`
|
||||
- `type = "stdout"` → `data` is a string chunk from stdout
|
||||
- `type = "stderr"` → `data` is a string chunk from stderr
|
||||
- `type = "exit"` → `data` is the exit code (number)
|
||||
|
||||
```javascript
|
||||
// Basic
|
||||
host.Stream("tail", ["-f", "/var/log/app.log"], function(type, data) {
|
||||
pc.Stream(["npm", "run", "dev"], function(type, data) {
|
||||
if (type === "stdout") console.log(data)
|
||||
if (type === "stderr") console.log("[ERR]", data)
|
||||
if (type === "exit") console.log("exited:", data)
|
||||
})
|
||||
|
||||
// With options
|
||||
host.Stream("python3", ["train.py"], {
|
||||
workdir: "/workspace/ml",
|
||||
timeout: 3600000
|
||||
pc.Stream(["npm", "test"], {
|
||||
workdir: "/app",
|
||||
env: { CI: "true" },
|
||||
timeout: 60000
|
||||
}, function(type, data) {
|
||||
if (type === "stderr") console.log("[WARN]", data)
|
||||
if (type === "exit") console.log("done, code:", data)
|
||||
console.log(type, data)
|
||||
})
|
||||
```
|
||||
|
||||
### host.Workspace(sessionID) → WorkspaceFS
|
||||
### pc.VNC() → string
|
||||
|
||||
Access a workspace on the host by session ID. Same as `box.Workspace()`, the WorkspaceFS object is implemented in the `workspace/jsapi` package; this method calls `workspace.NewFSObject(v8ctx, sessionID)`.
|
||||
Get the VNC WebSocket URL.
|
||||
|
||||
- **Box**: routes to the container's VNC server (`:5900`)
|
||||
- **Host**: routes to the Tai host via `__host__` identifier (configurable via `host_vnc_port`)
|
||||
|
||||
```javascript
|
||||
const ws = host.Workspace("my-session")
|
||||
const url = pc.VNC()
|
||||
// Box: "ws://tai-host:16080/vnc/container-id/ws"
|
||||
// Host: "ws://tai-host:16080/vnc/__host__/ws"
|
||||
```
|
||||
|
||||
If no VNC server is running, the WebSocket connection will fail — handle this in the caller.
|
||||
|
||||
### pc.Proxy(port, path?) → string
|
||||
|
||||
Get an HTTP proxy URL for a service port.
|
||||
|
||||
- **Box**: routes to `container-ip:{port}`
|
||||
- **Host**: routes to `127.0.0.1:{port}` on the Tai machine via `__host__`
|
||||
|
||||
```javascript
|
||||
const url = pc.Proxy(3000)
|
||||
// Box: "http://tai-host:8099/container-id:3000/"
|
||||
// Host: "http://tai-host:8099/__host__:3000/"
|
||||
|
||||
const url = pc.Proxy(8080, "/api/v1")
|
||||
// Box: "http://tai-host:8099/container-id:8080/api/v1"
|
||||
// Host: "http://tai-host:8099/__host__:8080/api/v1"
|
||||
```
|
||||
|
||||
### pc.ComputerInfo() → ComputerInfo
|
||||
|
||||
Get identity and registry information.
|
||||
|
||||
```javascript
|
||||
const info = pc.ComputerInfo()
|
||||
console.log(info.kind) // "box" or "host"
|
||||
console.log(info.pool) // pool name
|
||||
console.log(info.system.os) // "linux" | "windows" | "darwin"
|
||||
console.log(info.status) // "running" | "stopped" | ...
|
||||
```
|
||||
|
||||
Returns a [ComputerInfo](#computerinfo-object) object.
|
||||
|
||||
### pc.BindWorkplace(workspaceID) → void
|
||||
|
||||
Bind a workspace to this computer for the current session.
|
||||
|
||||
```javascript
|
||||
pc.BindWorkplace("ws-project-abc")
|
||||
```
|
||||
|
||||
### pc.Workplace() → WorkspaceFS | null
|
||||
|
||||
Access the workspace bound via `BindWorkplace()`. Returns `null` if no workspace is bound.
|
||||
|
||||
```javascript
|
||||
pc.BindWorkplace("ws-project-abc")
|
||||
const ws = pc.Workplace()
|
||||
ws.ReadFile("config.yml")
|
||||
ws.WriteFile("output.json", JSON.stringify(data))
|
||||
ws.ReadDir("results/")
|
||||
```
|
||||
|
||||
See [WorkspaceFS Object](#workspacefs-object) for the full method list.
|
||||
|
||||
### pc.Attach(port, options?) → string — box-only
|
||||
|
||||
Get a WebSocket or SSE endpoint URL for a service running inside the container. Throws on host computers.
|
||||
|
||||
```javascript
|
||||
const wsURL = pc.Attach(3000, { protocol: "ws", path: "/ws" })
|
||||
// "ws://tai-host:8099/container-id:3000/ws"
|
||||
|
||||
const sseURL = pc.Attach(8080, { protocol: "sse", path: "/events" })
|
||||
// "http://tai-host:8099/container-id:8080/events"
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
```javascript
|
||||
{
|
||||
protocol: "ws" | "sse", // default "ws"; affects URL scheme (ws:// vs http://)
|
||||
path: "/ws" // optional URL path suffix
|
||||
}
|
||||
```
|
||||
|
||||
### pc.Info() → BoxInfo — box-only
|
||||
|
||||
Get current container status information. Throws on host computers.
|
||||
|
||||
```javascript
|
||||
const info = pc.Info()
|
||||
console.log(info.status, info.process_count, info.last_active)
|
||||
```
|
||||
|
||||
Returns the same structure as elements in `sandbox.List()`.
|
||||
|
||||
### pc.Start() → void — box-only
|
||||
|
||||
Start a stopped container. Throws on host computers.
|
||||
|
||||
```javascript
|
||||
pc.Start()
|
||||
```
|
||||
|
||||
### pc.Stop() → void — box-only
|
||||
|
||||
Stop a running container. Throws on host computers.
|
||||
|
||||
```javascript
|
||||
pc.Stop()
|
||||
```
|
||||
|
||||
### pc.Remove() → void — box-only
|
||||
|
||||
Remove the container. Throws on host computers.
|
||||
|
||||
```javascript
|
||||
pc.Remove()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ComputerInfo Object
|
||||
|
||||
Returned by `pc.ComputerInfo()`. Read-only snapshot of a Computer's identity and state.
|
||||
|
||||
```javascript
|
||||
{
|
||||
kind: "box", // "box" | "host"
|
||||
pool: "default",
|
||||
tai_id: "tai-abc123",
|
||||
machine_id: "m-xyz",
|
||||
version: "1.2.3",
|
||||
mode: "direct", // "direct" | "tunnel"
|
||||
status: "running",
|
||||
capabilities: { docker: true, k8s: false, host_exec: true },
|
||||
system: {
|
||||
os: "linux",
|
||||
arch: "amd64",
|
||||
hostname: "gpu-server-01",
|
||||
num_cpu: 16,
|
||||
total_mem: 68719476736
|
||||
},
|
||||
|
||||
// Box-only fields (empty/zero for host)
|
||||
box_id: "sb-xxx",
|
||||
container_id: "abc123...",
|
||||
owner: "user-123",
|
||||
image: "node:20",
|
||||
policy: "session",
|
||||
labels: { team: "backend" }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NodeInfo Object
|
||||
|
|
@ -399,7 +388,7 @@ Returned by `sandbox.GetNode()`, `sandbox.Nodes()`, `sandbox.NodesByTeam()`. Rea
|
|||
{
|
||||
tai_id: "tai-abc123",
|
||||
machine_id: "m-xyz",
|
||||
version: "1.2.0",
|
||||
version: "1.2.3",
|
||||
mode: "direct", // "direct" | "tunnel"
|
||||
addr: "192.168.1.100",
|
||||
status: "online", // "online" | "offline" | "connecting"
|
||||
|
|
@ -407,11 +396,12 @@ Returned by `sandbox.GetNode()`, `sandbox.Nodes()`, `sandbox.NodesByTeam()`. Rea
|
|||
connected_at: "2026-03-07T08:00:00Z",
|
||||
last_ping: "2026-03-07T10:05:00Z",
|
||||
ports: {
|
||||
grpc: 19100,
|
||||
http: 8099,
|
||||
vnc: 16080,
|
||||
docker: 12375,
|
||||
k8s: 16443
|
||||
grpc: 19100,
|
||||
http: 8099,
|
||||
vnc: 16080,
|
||||
docker: 12375,
|
||||
k8s: 16443,
|
||||
host_vnc: 5900 // VNC port on host for __host__ routing
|
||||
},
|
||||
capabilities: {
|
||||
docker: true,
|
||||
|
|
@ -423,7 +413,7 @@ Returned by `sandbox.GetNode()`, `sandbox.Nodes()`, `sandbox.NodesByTeam()`. Rea
|
|||
arch: "amd64",
|
||||
hostname: "gpu-server-01",
|
||||
num_cpu: 16,
|
||||
total_mem: 68719476736 // bytes (64GB)
|
||||
total_mem: 68719476736 // bytes (64GB)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -432,7 +422,7 @@ Returned by `sandbox.GetNode()`, `sandbox.Nodes()`, `sandbox.NodesByTeam()`. Rea
|
|||
|
||||
## WorkspaceFS Object
|
||||
|
||||
Returned by `box.Workspace()`, `host.Workspace()`, `workspace.Get()`, and `workspace.Create()`.
|
||||
Returned by `pc.Workplace()`, `workspace.Get()`, and `workspace.Create()`.
|
||||
|
||||
### Properties (read-only)
|
||||
|
||||
|
|
@ -472,44 +462,44 @@ Return types:
|
|||
### Run a build and check output
|
||||
|
||||
```javascript
|
||||
const box = sandbox.Create({
|
||||
const pc = sandbox.Create({
|
||||
image: "golang:1.23",
|
||||
owner: "ci-bot",
|
||||
workspace_id: "ws-project-abc"
|
||||
})
|
||||
|
||||
const build = box.Exec(["go", "build", "./..."], {
|
||||
const build = pc.Exec(["go", "build", "./..."], {
|
||||
workdir: "/workspace",
|
||||
timeout: 120000
|
||||
})
|
||||
|
||||
if (build.exit_code !== 0) {
|
||||
console.log("Build failed:", build.stderr)
|
||||
box.Remove()
|
||||
pc.Remove()
|
||||
throw new Error("build failed")
|
||||
}
|
||||
|
||||
const test = box.Exec(["go", "test", "./..."], {
|
||||
const test = pc.Exec(["go", "test", "./..."], {
|
||||
workdir: "/workspace",
|
||||
env: { CGO_ENABLED: "0" }
|
||||
})
|
||||
|
||||
console.log("Tests:", test.exit_code === 0 ? "PASS" : "FAIL")
|
||||
box.Remove()
|
||||
pc.Remove()
|
||||
```
|
||||
|
||||
### Stream a long-running process
|
||||
|
||||
```javascript
|
||||
const box = sandbox.Create({
|
||||
const pc = sandbox.Create({
|
||||
image: "node:20",
|
||||
owner: "user-123",
|
||||
policy: "session"
|
||||
})
|
||||
|
||||
box.Exec(["npm", "install"], { workdir: "/app" })
|
||||
pc.Exec(["npm", "install"], { workdir: "/app" })
|
||||
|
||||
box.Stream(["npm", "run", "dev"], { workdir: "/app" }, function(type, data) {
|
||||
pc.Stream(["npm", "run", "dev"], { workdir: "/app" }, function(type, data) {
|
||||
if (type === "stdout") console.log(data)
|
||||
if (type === "stderr") console.log("[ERR]", data)
|
||||
if (type === "exit") console.log("dev server exited:", data)
|
||||
|
|
@ -521,16 +511,58 @@ box.Stream(["npm", "run", "dev"], { workdir: "/app" }, function(type, data) {
|
|||
```javascript
|
||||
const host = sandbox.Host("gpu")
|
||||
|
||||
const result = host.Exec("nvidia-smi", [])
|
||||
const result = host.Exec(["nvidia-smi"])
|
||||
console.log(result.stdout)
|
||||
|
||||
host.Exec("python3", ["train.py", "--epochs=10"], {
|
||||
host.Exec(["python3", "train.py", "--epochs=10"], {
|
||||
workdir: "/workspace/ml",
|
||||
env: { CUDA_VISIBLE_DEVICES: "0,1" },
|
||||
timeout: 3600000
|
||||
})
|
||||
```
|
||||
|
||||
### Uniform interface — same code for box and host
|
||||
|
||||
```javascript
|
||||
function runTask(pc, cmd, opts) {
|
||||
const result = pc.Exec(cmd, opts)
|
||||
if (result.exit_code !== 0) {
|
||||
throw new Error(pc.kind + " exec failed: " + result.stderr)
|
||||
}
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
// Works the same for both
|
||||
const box = sandbox.Create({ image: "node:20", owner: "u1" })
|
||||
const host = sandbox.Host("gpu")
|
||||
|
||||
runTask(box, ["node", "-e", "console.log('hi')"])
|
||||
runTask(host, ["echo", "hello"])
|
||||
```
|
||||
|
||||
### VNC and HTTP proxy
|
||||
|
||||
```javascript
|
||||
const pc = sandbox.Create({
|
||||
image: "kasmweb/chrome:latest",
|
||||
owner: "user-123",
|
||||
vnc: true
|
||||
})
|
||||
|
||||
// Get VNC desktop URL
|
||||
const vncURL = pc.VNC()
|
||||
// "ws://tai-host:16080/vnc/container-id/ws"
|
||||
|
||||
// Get HTTP proxy to a web service inside the container
|
||||
const appURL = pc.Proxy(3000)
|
||||
// "http://tai-host:8099/container-id:3000/"
|
||||
|
||||
// Same methods work on host
|
||||
const host = sandbox.Host()
|
||||
const hostVNC = host.VNC()
|
||||
// "ws://tai-host:16080/vnc/__host__/ws"
|
||||
```
|
||||
|
||||
### Query cluster nodes
|
||||
|
||||
```javascript
|
||||
|
|
@ -555,12 +587,14 @@ gpuNodes.forEach(function(n) {
|
|||
### Workspace file operations
|
||||
|
||||
```javascript
|
||||
const ws = workspace.Create({
|
||||
name: "my-project",
|
||||
owner: "user-123",
|
||||
node: "default"
|
||||
const pc = sandbox.Create({
|
||||
image: "node:20",
|
||||
owner: "user-123"
|
||||
})
|
||||
|
||||
pc.BindWorkplace("ws-my-project")
|
||||
const ws = pc.Workplace()
|
||||
|
||||
ws.MkdirAll("src/utils")
|
||||
ws.WriteFile("src/main.go", 'package main\n\nfunc main() {\n\tprintln("hello")\n}\n')
|
||||
ws.WriteFile("go.mod", "module myproject\n\ngo 1.23\n")
|
||||
|
|
@ -580,9 +614,9 @@ console.log(content)
|
|||
const auth = Authorized()
|
||||
if (!auth) throw new Error("not authenticated")
|
||||
|
||||
const box = sandbox.Get(id)
|
||||
if (!box) throw new Error("sandbox not found")
|
||||
if (box.owner !== auth.user_id) throw new Error("permission denied")
|
||||
const pc = sandbox.Get(id)
|
||||
if (!pc) throw new Error("sandbox not found")
|
||||
if (pc.owner !== auth.user_id) throw new Error("permission denied")
|
||||
|
||||
box.Exec(["ls", "-la"])
|
||||
pc.Exec(["ls", "-la"])
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
package jsapi
|
||||
|
||||
import (
|
||||
"rogchap.com/v8go"
|
||||
)
|
||||
|
||||
// NewBoxObject creates a JS Box object backed by a sandbox ID string.
|
||||
// All methods delegate to the Go sandbox.M() singleton — no Go object is
|
||||
// passed to V8, no bridge registration, no Release() needed.
|
||||
//
|
||||
// Box implements the Computer interface, so it shares the unified Exec/Stream/
|
||||
// VNC/Proxy/ComputerInfo/BindWorkplace/Workplace methods with Host. It also
|
||||
// has Box-specific methods (Attach, Info, Start, Stop, Remove).
|
||||
//
|
||||
// # Properties (read-only)
|
||||
//
|
||||
// box.id → string // sandbox ID ← Box.ID()
|
||||
// box.owner → string // owner user ID ← Box.Owner()
|
||||
// box.pool → string // pool name ← Box.Pool()
|
||||
//
|
||||
// # Methods — Computer interface (unified with Host)
|
||||
//
|
||||
// box.Exec(cmd, options?) → ExecResult
|
||||
//
|
||||
// Go: Computer.Exec(ctx, cmd []string, opts ...ExecOption) (*ExecResult, error)
|
||||
//
|
||||
// JS args:
|
||||
// cmd: string[] → cmd []string
|
||||
// options: { → ExecOption
|
||||
// workdir: string, → WithWorkDir(dir)
|
||||
// env: object, → WithEnv(map[string]string)
|
||||
// stdin: string, → WithStdin([]byte)
|
||||
// timeout: number, → WithTimeout(ms → time.Duration)
|
||||
// max_output: number → WithMaxOutput(bytes int64)
|
||||
// }
|
||||
// JS returns: {
|
||||
// exit_code: number, ← ExecResult.ExitCode
|
||||
// stdout: string, ← ExecResult.Stdout
|
||||
// stderr: string, ← ExecResult.Stderr
|
||||
// duration_ms: number, ← ExecResult.DurationMs (Host fills; Box = 0)
|
||||
// error: string, ← ExecResult.Error (Host fills; Box = "")
|
||||
// truncated: boolean ← ExecResult.Truncated (Host fills; Box = false)
|
||||
// }
|
||||
//
|
||||
// box.Stream(cmd, callback) / box.Stream(cmd, options, callback)
|
||||
//
|
||||
// Go: Computer.Stream(ctx, cmd []string, opts ...ExecOption) (*ExecStream, error)
|
||||
//
|
||||
// Blocks until the process exits. The last argument must be a JS function.
|
||||
// Callback signature: function(type, data)
|
||||
// type = "stdout" → data is string (chunk)
|
||||
// type = "stderr" → data is string (chunk)
|
||||
// type = "exit" → data is number (exit code)
|
||||
//
|
||||
// box.VNC() → string
|
||||
//
|
||||
// Go: Computer.VNC(ctx) (string, error)
|
||||
// Returns: VNC WebSocket URL
|
||||
//
|
||||
// box.Proxy(port, path?) → string
|
||||
//
|
||||
// Go: Computer.Proxy(ctx, port int, path string) (string, error)
|
||||
// Returns: HTTP proxy URL
|
||||
//
|
||||
// box.ComputerInfo() → ComputerInfo
|
||||
//
|
||||
// Go: Computer.ComputerInfo() ComputerInfo
|
||||
// JS returns: {
|
||||
// kind: "box", pool, status,
|
||||
// box_id, container_id, owner, image, policy, labels, ...
|
||||
// }
|
||||
//
|
||||
// box.BindWorkplace(workspaceID) → void
|
||||
//
|
||||
// Go: Computer.BindWorkplace(workspaceID string)
|
||||
//
|
||||
// box.Workplace() → WorkspaceFS | null
|
||||
//
|
||||
// Go: Computer.Workplace() workspace.FS
|
||||
//
|
||||
// # Methods — Box-specific
|
||||
//
|
||||
// box.Attach(port, options?) → string
|
||||
//
|
||||
// Go: Proxy.URL(ctx, containerID, port, path) (string, error)
|
||||
//
|
||||
// Returns the service URL string.
|
||||
// JS args:
|
||||
// port: number → port int
|
||||
// options: { → AttachOption
|
||||
// protocol: "ws"|"sse", → affects URL scheme
|
||||
// path: string, → URL path suffix
|
||||
// }
|
||||
// JS returns: string (URL)
|
||||
//
|
||||
// box.Workspace() → WorkspaceFS
|
||||
//
|
||||
// Implemented in workspace/jsapi package. Calls:
|
||||
// workspace.NewFSObject(v8ctx, box.WorkspaceID())
|
||||
//
|
||||
// box.Info() → BoxInfo
|
||||
//
|
||||
// Go: Box.Info(ctx) (*BoxInfo, error)
|
||||
// JS returns: {
|
||||
// id, container_id, pool, owner, status, image, vnc, policy,
|
||||
// labels, created_at, last_active, process_count
|
||||
// }
|
||||
//
|
||||
// box.Start() → void
|
||||
//
|
||||
// Go: Box.Start(ctx) error
|
||||
//
|
||||
// box.Stop() → void
|
||||
//
|
||||
// Go: Box.Stop(ctx) error
|
||||
//
|
||||
// box.Remove() → void
|
||||
//
|
||||
// Go: Box.Remove(ctx) error
|
||||
func NewBoxObject(v8ctx *v8go.Context, boxID string) (*v8go.Value, error) {
|
||||
// TODO: Phase 2 implementation
|
||||
// 1. Create JS object via v8go.NewObjectTemplate
|
||||
// 2. Set read-only properties: id, owner, pool (from sandbox.M().Get(boxID))
|
||||
// 3. Bind Computer interface methods:
|
||||
// - Exec, Stream, VNC, Proxy, ComputerInfo, BindWorkplace, Workplace
|
||||
// 4. Bind Box-specific methods:
|
||||
// - Attach → client.Proxy().URL(ctx, containerID, port, path) → string
|
||||
// - Workspace → NewFSObject(v8ctx, sandbox.M().Get(id).WorkspaceID())
|
||||
// - Info → sandbox.M().Get(id).Info(ctx) → JS object
|
||||
// - Start → sandbox.M().Get(id).Start(ctx)
|
||||
// - Stop → sandbox.M().Get(id).Stop(ctx)
|
||||
// - Remove → sandbox.M().Get(id).Remove(ctx)
|
||||
return nil, nil
|
||||
}
|
||||
138
sandbox/v2/jsapi/computer.go
Normal file
138
sandbox/v2/jsapi/computer.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package jsapi
|
||||
|
||||
import (
|
||||
"rogchap.com/v8go"
|
||||
)
|
||||
|
||||
// sbHost: `sandbox.Host(pool?)` → Computer (kind="host")
|
||||
//
|
||||
// Go: Manager.Host(ctx, pool) (*Host, error)
|
||||
//
|
||||
// Args:
|
||||
//
|
||||
// pool: string (optional) — pool name; empty = default pool
|
||||
//
|
||||
// Returns: Computer object (kind="host") if the pool has host_exec capability, otherwise throws.
|
||||
func sbHost(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||
// TODO: Phase 2
|
||||
// 1. pool := ""; if len(info.Args()) > 0 && info.Args()[0].IsString() { pool = info.Args()[0].String() }
|
||||
// 2. host, err := sandbox.M().Host(ctx, pool)
|
||||
// 3. if err != nil { throw in V8 }
|
||||
// 4. Return NewComputerObject(v8ctx, "host", pool)
|
||||
return v8go.Undefined(info.Context().Isolate())
|
||||
}
|
||||
|
||||
// NewComputerObject creates a unified JS Computer object backed by either a Box or Host.
|
||||
// The `kind` field ("box" or "host") determines which methods are available at runtime.
|
||||
// Box-only methods (Attach, Info, Start, Stop, Remove) throw an error when called on a host.
|
||||
//
|
||||
// # Properties (read-only)
|
||||
//
|
||||
// pc.kind → string // "box" | "host" ← ComputerInfo().Kind
|
||||
// pc.id → string // sandbox ID ← Box.ID() (empty for host)
|
||||
// pc.owner → string // owner ← Box.Owner() (empty for host)
|
||||
// pc.pool → string // pool name ← ComputerInfo().Pool
|
||||
//
|
||||
// # Methods — Computer interface (both box and host)
|
||||
//
|
||||
// pc.Exec(cmd, options?) → ExecResult
|
||||
//
|
||||
// Go: Computer.Exec(ctx, cmd []string, opts ...ExecOption) (*ExecResult, error)
|
||||
//
|
||||
// JS args:
|
||||
// cmd: string[] → cmd []string
|
||||
// options: { → ExecOption
|
||||
// workdir: string, → WithWorkDir(dir)
|
||||
// env: object, → WithEnv(map[string]string)
|
||||
// stdin: string, → WithStdin([]byte)
|
||||
// timeout: number, → WithTimeout(ms → time.Duration)
|
||||
// max_output: number → WithMaxOutput(bytes int64)
|
||||
// }
|
||||
// JS returns: {
|
||||
// exit_code: number, ← ExecResult.ExitCode
|
||||
// stdout: string, ← ExecResult.Stdout
|
||||
// stderr: string, ← ExecResult.Stderr
|
||||
// duration_ms: number, ← ExecResult.DurationMs
|
||||
// error: string, ← ExecResult.Error
|
||||
// truncated: boolean ← ExecResult.Truncated
|
||||
// }
|
||||
//
|
||||
// pc.Stream(cmd, callback) / pc.Stream(cmd, options, callback)
|
||||
//
|
||||
// Go: Computer.Stream(ctx, cmd []string, opts ...ExecOption) (*ExecStream, error)
|
||||
//
|
||||
// Blocks until the process exits. The last argument must be a JS function.
|
||||
// Callback signature: function(type, data)
|
||||
// type = "stdout" → data is string (chunk)
|
||||
// type = "stderr" → data is string (chunk)
|
||||
// type = "exit" → data is number (exit code)
|
||||
//
|
||||
// pc.VNC() → string
|
||||
//
|
||||
// Go: Computer.VNC(ctx) (string, error)
|
||||
// Box: returns ws://host:port/vnc/{containerID}/ws
|
||||
// Host: returns ws://host:port/vnc/__host__/ws
|
||||
//
|
||||
// pc.Proxy(port, path?) → string
|
||||
//
|
||||
// Go: Computer.Proxy(ctx, port int, path string) (string, error)
|
||||
// Box: returns http://host:port/{containerID}:{port}/{path}
|
||||
// Host: returns http://host:port/__host__:{port}/{path}
|
||||
//
|
||||
// pc.ComputerInfo() → ComputerInfo
|
||||
//
|
||||
// Go: Computer.ComputerInfo() ComputerInfo
|
||||
// JS returns: { kind, pool, tai_id, machine_id, version, mode, status, capabilities,
|
||||
// system: { os, arch, hostname, num_cpu, total_mem },
|
||||
// box_id, container_id, owner, image, policy, labels }
|
||||
//
|
||||
// pc.BindWorkplace(workspaceID) → void
|
||||
//
|
||||
// Go: Computer.BindWorkplace(workspaceID string)
|
||||
//
|
||||
// pc.Workplace() → WorkspaceFS | null
|
||||
//
|
||||
// Go: Computer.Workplace() workspace.FS
|
||||
// Returns WorkspaceFS if a workplace is bound, null otherwise.
|
||||
//
|
||||
// # Methods — Box-only (throw on host)
|
||||
//
|
||||
// pc.Attach(port, options?) → string
|
||||
//
|
||||
// Gets a WebSocket/SSE endpoint URL for a container service.
|
||||
// JS args:
|
||||
// port: number
|
||||
// options: { protocol: "ws"|"sse", path: string }
|
||||
// JS returns: string (URL)
|
||||
//
|
||||
// pc.Info() → BoxInfo
|
||||
//
|
||||
// Go: Box.Info(ctx) (*BoxInfo, error)
|
||||
// JS returns: { id, container_id, pool, owner, status, image, vnc, policy,
|
||||
// labels, created_at, last_active, process_count }
|
||||
//
|
||||
// pc.Start() → void
|
||||
//
|
||||
// Go: Box.Start(ctx) error
|
||||
//
|
||||
// pc.Stop() → void
|
||||
//
|
||||
// Go: Box.Stop(ctx) error
|
||||
//
|
||||
// pc.Remove() → void
|
||||
//
|
||||
// Go: Box.Remove(ctx) error
|
||||
func NewComputerObject(v8ctx *v8go.Context, kind string, id string) (*v8go.Value, error) {
|
||||
// TODO: Phase 2 implementation
|
||||
// 1. Create JS object via v8go.NewObjectTemplate
|
||||
// 2. Set read-only properties: kind, id, owner, pool
|
||||
// - kind: "box" or "host"
|
||||
// - id/owner: from sandbox.M().Get(id) for box; empty for host
|
||||
// - pool: from ComputerInfo().Pool
|
||||
// 3. Bind Computer interface methods:
|
||||
// - Exec, Stream, VNC, Proxy, ComputerInfo, BindWorkplace, Workplace
|
||||
// 4. Bind box-only methods with kind guard:
|
||||
// - Attach, Info, Start, Stop, Remove
|
||||
// - If kind == "host", these throw: "not supported: {method}() requires a box computer"
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
package jsapi
|
||||
|
||||
import (
|
||||
"rogchap.com/v8go"
|
||||
)
|
||||
|
||||
// sbHost: `sandbox.Host(pool?)` → Computer (Host)
|
||||
//
|
||||
// Go: Manager.Host(ctx, pool) (*Host, error)
|
||||
//
|
||||
// Args:
|
||||
//
|
||||
// pool: string (optional) — pool name; empty = default pool
|
||||
//
|
||||
// Returns: Computer object (Host) if the pool has host_exec capability, otherwise throws.
|
||||
//
|
||||
// Host executes commands on the Tai host machine (no container). Available only
|
||||
// when the pool's Tai server exposes HostExec gRPC.
|
||||
func sbHost(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||
// TODO: Phase 2
|
||||
// 1. pool := ""; if len(info.Args()) > 0 && info.Args()[0].IsString() { pool = info.Args()[0].String() }
|
||||
// 2. host, err := sandbox.M().Host(ctx, pool)
|
||||
// 3. if err != nil { throw in V8 }
|
||||
// 4. Return NewComputerObject(v8ctx, host)
|
||||
return v8go.Undefined(info.Context().Isolate())
|
||||
}
|
||||
|
||||
// NewHostObject creates a JS Computer object backed by a Host.
|
||||
// All methods delegate to the Go sandbox.M() singleton — no Go *Host passed to V8.
|
||||
//
|
||||
// Host implements the unified Computer interface, so the JS object exposes the
|
||||
// same methods as a Box Computer object:
|
||||
//
|
||||
// # Properties (read-only)
|
||||
//
|
||||
// host.pool → string // pool name
|
||||
//
|
||||
// # Methods — Go mapping (unified Computer interface)
|
||||
//
|
||||
// host.Exec(cmd, options?) → ExecResult
|
||||
//
|
||||
// Go: Computer.Exec(ctx, cmd []string, opts ...ExecOption) (*ExecResult, error)
|
||||
//
|
||||
// JS args:
|
||||
// cmd: string[] → cmd []string
|
||||
// options: { → ExecOption
|
||||
// workdir: string, → WithWorkDir(dir)
|
||||
// env: object, → WithEnv(map[string]string)
|
||||
// stdin: string, → WithStdin([]byte)
|
||||
// timeout: number, → WithTimeout(ms → time.Duration)
|
||||
// max_output: number → WithMaxOutput(bytes int64)
|
||||
// }
|
||||
// JS returns: {
|
||||
// exit_code: number, ← ExecResult.ExitCode
|
||||
// stdout: string, ← ExecResult.Stdout
|
||||
// stderr: string, ← ExecResult.Stderr
|
||||
// duration_ms: number, ← ExecResult.DurationMs
|
||||
// error: string, ← ExecResult.Error
|
||||
// truncated: boolean ← ExecResult.Truncated
|
||||
// }
|
||||
//
|
||||
// host.Stream(cmd, callback) / host.Stream(cmd, options, callback)
|
||||
//
|
||||
// Go: Computer.Stream(ctx, cmd []string, opts ...ExecOption) (*ExecStream, error)
|
||||
//
|
||||
// Blocks until the process exits. The last argument must be a JS function.
|
||||
// Callback signature: function(type, data)
|
||||
// type = "stdout" → data is string (chunk)
|
||||
// type = "stderr" → data is string (chunk)
|
||||
// type = "exit" → data is number (exit code)
|
||||
//
|
||||
// host.VNC() → string
|
||||
//
|
||||
// Go: Computer.VNC(ctx) (string, error)
|
||||
// Returns: VNC WebSocket URL (routes to Tai host via __host__ identifier)
|
||||
//
|
||||
// host.Proxy(port, path?) → string
|
||||
//
|
||||
// Go: Computer.Proxy(ctx, port int, path string) (string, error)
|
||||
// Returns: HTTP proxy URL (routes to Tai host via __host__ identifier)
|
||||
//
|
||||
// host.ComputerInfo() → ComputerInfo
|
||||
//
|
||||
// Go: Computer.ComputerInfo() ComputerInfo
|
||||
// JS returns: { kind: "host", pool: string, status: string, ... }
|
||||
//
|
||||
// host.BindWorkplace(workspaceID) → void
|
||||
//
|
||||
// Go: Computer.BindWorkplace(workspaceID string)
|
||||
//
|
||||
// host.Workplace() → WorkspaceFS | null
|
||||
//
|
||||
// Go: Computer.Workplace() workspace.FS
|
||||
// Returns WorkspaceFS if a workplace is bound, null otherwise.
|
||||
func NewHostObject(v8ctx *v8go.Context, pool string) (*v8go.Value, error) {
|
||||
// TODO: Phase 2 implementation
|
||||
// 1. Create JS object via v8go.NewObjectTemplate
|
||||
// 2. Set read-only property: pool
|
||||
// 3. Bind methods via unified Computer interface:
|
||||
// - Exec, Stream, VNC, Proxy, ComputerInfo, BindWorkplace, Workplace
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -1,32 +1,30 @@
|
|||
// Package jsapi registers the sandbox namespace into the Yao V8 runtime.
|
||||
//
|
||||
// All methods are static on the sandbox object — no constructor.
|
||||
// Both sandbox.Create() and sandbox.Host() return a unified Computer object.
|
||||
//
|
||||
// # 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 pc = sandbox.Create({ image: "node:20", owner: "user1" }) // → Computer (kind="box")
|
||||
// const pc = sandbox.Get(id) // → Computer (kind="box") | null
|
||||
// const list = sandbox.List({ owner: "u1" }) // → BoxInfo[]
|
||||
// sandbox.Delete(id) // → void
|
||||
// const host = sandbox.Host("gpu") // → Computer (Host via host_exec on Tai)
|
||||
// const host = sandbox.Host("gpu") // → Computer (kind="host")
|
||||
// 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) → Computer (Host)
|
||||
// sandbox.GetNode(id) → registry.Global().Get(id) → NodeInfo | null
|
||||
// sandbox.Nodes() → registry.Global().List() → NodeInfo[]
|
||||
// sandbox.NodesByTeam(t)→ registry.Global().ListByTeam(t) → NodeInfo[]
|
||||
// sandbox.Create(opts) → Manager.Create(ctx, CreateOptions) → Computer (Box)
|
||||
// sandbox.Create(opts) → Manager.GetOrCreate(ctx, opts) → Computer (Box) (when opts.id is set)
|
||||
// sandbox.Get(id) → Manager.Get(ctx, id) → Computer (Box)
|
||||
// sandbox.List(filter?) → Manager.List(ctx, ListOptions) → BoxInfo[]
|
||||
// sandbox.Delete(id) → Manager.Remove(ctx, id) → void
|
||||
// sandbox.Host(pool?) → Manager.Host(ctx, pool) → Computer (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:
|
||||
//
|
||||
|
|
@ -85,14 +83,14 @@ func ExportObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
|
|||
// labels: object → CreateOptions.Labels // map[string]string
|
||||
// }
|
||||
//
|
||||
// Returns: Box object (see box.go)
|
||||
// Returns: Computer object (kind="box") — see computer.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())
|
||||
// 4. Return NewComputerObject(v8ctx, "box", box.ID())
|
||||
return v8go.Undefined(info.Context().Isolate())
|
||||
}
|
||||
|
||||
|
|
@ -104,12 +102,12 @@ func sbCreate(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
//
|
||||
// id: string — sandbox ID
|
||||
//
|
||||
// Returns: Box object if found, null if not found
|
||||
// Returns: Computer object (kind="box") 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
|
||||
// 3. Return NewComputerObject(v8ctx, "box", id) or null
|
||||
return v8go.Undefined(info.Context().Isolate())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ type Computer interface {
|
|||
|
||||
// ComputerInfo holds identity and registry information for a Computer.
|
||||
type ComputerInfo struct {
|
||||
Kind string // "box" | "host"
|
||||
Kind string // "box" | "host"
|
||||
Pool string
|
||||
TaiID string
|
||||
MachineID string
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue