Add kubeconfig generation for Tai K8s in CI workflows

- Implement steps to generate kubeconfig for both the Tai K8s container and the test runner, ensuring proper configuration for Kubernetes interactions.
- Update the Docker run command to mount the generated kubeconfig, enhancing the integration of Tai with K8s.
- Remove the previous kubeconfig generation step for the Tai K8s proxy, streamlining the workflow.

These changes improve the CI workflows for the Tai service by ensuring accurate kubeconfig generation and integration with Kubernetes environments.
This commit is contained in:
Max 2026-03-05 23:32:09 +08:00
parent c797fcf74d
commit b7eb0e81e3
9 changed files with 768 additions and 220 deletions

View file

@ -1112,6 +1112,25 @@ jobs:
echo "::error::Tai Docker gRPC failed"; docker logs tai-docker 2>&1; exit 1 echo "::error::Tai Docker gRPC failed"; docker logs tai-docker 2>&1; exit 1
} }
- name: Generate kubeconfig for Tai K8s
run: |
K3D_IP=$(docker inspect k3d-tai-test-server-0 | jq -r '.[0].NetworkSettings.Networks["k3d-tai-test"].IPAddress')
echo "k3d server IP: ${K3D_IP}"
k3d kubeconfig get tai-test > /tmp/kubeconfig-k3d.yml
# Kubeconfig for tai-k8s container (uses k3d-internal IP)
sed "s|server: .*|server: https://${K3D_IP}:6443|" /tmp/kubeconfig-k3d.yml \
> /tmp/kubeconfig-tai-k8s.yml
echo "Container kubeconfig server:"
grep server: /tmp/kubeconfig-tai-k8s.yml
# Kubeconfig for test runner (uses localhost via port-mapped 6443)
sed 's|server: .*|server: https://127.0.0.1:6443|' /tmp/kubeconfig-k3d.yml \
> ${{ runner.temp }}/kubeconfig-tai.yml
echo "Test runner kubeconfig server:"
grep server: ${{ runner.temp }}/kubeconfig-tai.yml
- name: Start Tai K8s instance - name: Start Tai K8s instance
run: | run: |
K3D_IP=$(docker inspect k3d-tai-test-server-0 | jq -r '.[0].NetworkSettings.Networks["k3d-tai-test"].IPAddress') K3D_IP=$(docker inspect k3d-tai-test-server-0 | jq -r '.[0].NetworkSettings.Networks["k3d-tai-test"].IPAddress')
@ -1120,7 +1139,9 @@ jobs:
docker run -d --name tai-k8s \ docker run -d --name tai-k8s \
--network k3d-tai-test \ --network k3d-tai-test \
-p 8081:8080 -p 9101:9100 -p 6443:6443 -p 6081:6080 \ -p 8081:8080 -p 9101:9100 -p 6443:6443 -p 6081:6080 \
-v /tmp/kubeconfig-tai-k8s.yml:/etc/tai/kubeconfig.yml:ro \
-e TAI_K8S_UPSTREAM="tcp://${K3D_IP}:6443" \ -e TAI_K8S_UPSTREAM="tcp://${K3D_IP}:6443" \
-e TAI_KUBECONFIG=/etc/tai/kubeconfig.yml \
yaoapp/tai:latest yaoapp/tai:latest
for i in $(seq 1 30); do for i in $(seq 1 30); do
@ -1143,14 +1164,6 @@ jobs:
echo "::error::Tai K8s gRPC failed"; docker logs tai-k8s 2>&1; exit 1 echo "::error::Tai K8s gRPC failed"; docker logs tai-k8s 2>&1; exit 1
} }
- name: Generate kubeconfig for Tai K8s proxy
run: |
k3d kubeconfig get tai-test > /tmp/kubeconfig-k3d.yml
sed 's|server: .*|server: https://127.0.0.1:6443|' /tmp/kubeconfig-k3d.yml \
> ${{ runner.temp }}/kubeconfig-tai.yml
echo "Generated kubeconfig:"
grep server: ${{ runner.temp }}/kubeconfig-tai.yml
- name: Run Sandbox V2 Tests (tai + sandbox-v2 + workspace) - name: Run Sandbox V2 Tests (tai + sandbox-v2 + workspace)
env: env:
TAI_TEST_HOST: "127.0.0.1" TAI_TEST_HOST: "127.0.0.1"

View file

@ -820,6 +820,25 @@ jobs:
echo "::error::Tai Docker gRPC failed"; docker logs tai-docker 2>&1; exit 1 echo "::error::Tai Docker gRPC failed"; docker logs tai-docker 2>&1; exit 1
} }
- name: Generate kubeconfig for Tai K8s
run: |
K3D_IP=$(docker inspect k3d-tai-test-server-0 | jq -r '.[0].NetworkSettings.Networks["k3d-tai-test"].IPAddress')
echo "k3d server IP: ${K3D_IP}"
k3d kubeconfig get tai-test > /tmp/kubeconfig-k3d.yml
# Kubeconfig for tai-k8s container (uses k3d-internal IP)
sed "s|server: .*|server: https://${K3D_IP}:6443|" /tmp/kubeconfig-k3d.yml \
> /tmp/kubeconfig-tai-k8s.yml
echo "Container kubeconfig server:"
grep server: /tmp/kubeconfig-tai-k8s.yml
# Kubeconfig for test runner (uses localhost via port-mapped 6443)
sed 's|server: .*|server: https://127.0.0.1:6443|' /tmp/kubeconfig-k3d.yml \
> ${{ runner.temp }}/kubeconfig-tai.yml
echo "Test runner kubeconfig server:"
grep server: ${{ runner.temp }}/kubeconfig-tai.yml
- name: Start Tai K8s instance - name: Start Tai K8s instance
run: | run: |
K3D_IP=$(docker inspect k3d-tai-test-server-0 | jq -r '.[0].NetworkSettings.Networks["k3d-tai-test"].IPAddress') K3D_IP=$(docker inspect k3d-tai-test-server-0 | jq -r '.[0].NetworkSettings.Networks["k3d-tai-test"].IPAddress')
@ -828,7 +847,9 @@ jobs:
docker run -d --name tai-k8s \ docker run -d --name tai-k8s \
--network k3d-tai-test \ --network k3d-tai-test \
-p 8081:8080 -p 9101:9100 -p 6443:6443 -p 6081:6080 \ -p 8081:8080 -p 9101:9100 -p 6443:6443 -p 6081:6080 \
-v /tmp/kubeconfig-tai-k8s.yml:/etc/tai/kubeconfig.yml:ro \
-e TAI_K8S_UPSTREAM="tcp://${K3D_IP}:6443" \ -e TAI_K8S_UPSTREAM="tcp://${K3D_IP}:6443" \
-e TAI_KUBECONFIG=/etc/tai/kubeconfig.yml \
yaoapp/tai:latest yaoapp/tai:latest
for i in $(seq 1 30); do for i in $(seq 1 30); do
@ -851,14 +872,6 @@ jobs:
echo "::error::Tai K8s gRPC failed"; docker logs tai-k8s 2>&1; exit 1 echo "::error::Tai K8s gRPC failed"; docker logs tai-k8s 2>&1; exit 1
} }
- name: Generate kubeconfig for Tai K8s proxy
run: |
k3d kubeconfig get tai-test > /tmp/kubeconfig-k3d.yml
sed 's|server: .*|server: https://127.0.0.1:6443|' /tmp/kubeconfig-k3d.yml \
> ${{ runner.temp }}/kubeconfig-tai.yml
echo "Generated kubeconfig:"
grep server: ${{ runner.temp }}/kubeconfig-tai.yml
- name: Run Sandbox V2 Tests (tai + sandbox-v2 + workspace) - name: Run Sandbox V2 Tests (tai + sandbox-v2 + workspace)
env: env:
TAI_TEST_HOST: "127.0.0.1" TAI_TEST_HOST: "127.0.0.1"

View file

@ -21,7 +21,8 @@ Sandbox does NOT import or depend on Agent. Agent is one of many consumers.
``` ```
┌─────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ Consumers (know nothing about tai/Docker/K8s) │ │ Consumers (know nothing about tai/Docker/K8s) │
│ ├── JSAPI: Sandbox("my-app") │ │ ├── JSAPI: sandbox.Create/Get/List/Delete │
│ ├── JSAPI: workspace.Create/Get/List/Delete │
│ ├── Process: sandbox.Create, sandbox.Exec │ │ ├── Process: sandbox.Create, sandbox.Exec │
│ ├── Agent: uses sandbox via interface │ │ ├── Agent: uses sandbox via interface │
│ └── API: /api/__yao/sandbox/* │ │ └── API: /api/__yao/sandbox/* │
@ -576,8 +577,9 @@ sandbox/v2/
├── config.go // Config struct ├── config.go // Config struct
├── errors.go // sentinel errors ├── errors.go // sentinel errors
├── grpc.go // token creation/revocation, gRPC env var injection ├── grpc.go // token creation/revocation, gRPC env var injection
├── jsapi/ // (Phase 2) V8 JSAPI Sandbox() constructor ├── jsapi/ // (Phase 2) V8 JSAPI sandbox.* namespace
│ └── sandbox.go │ ├── jsapi.go // RegisterObject("sandbox"), Create/Get/List/Delete
│ └── box.go // Box JS object: Exec/Attach/VNC/Proxy/Workspace/Info/Start/Stop/Remove
├── export_test.go // ResetForTest() for test isolation ├── export_test.go // ResetForTest() for test isolation
├── testutils_test.go // shared test helpers (multi-pool setup) ├── testutils_test.go // shared test helpers (multi-pool setup)
├── sandbox_test.go // Init/M singleton tests ├── sandbox_test.go // Init/M singleton tests
@ -729,6 +731,9 @@ workspace/
├── workspace.go // types, metadata marshal/unmarshal ├── workspace.go // types, metadata marshal/unmarshal
├── manager.go // Manager: CRUD, file I/O, node management ├── manager.go // Manager: CRUD, file I/O, node management
├── errors.go // sentinel errors ├── errors.go // sentinel errors
├── jsapi/ // (Phase 2) V8 JSAPI workspace.* namespace
│ ├── jsapi.go // RegisterObject("workspace"), Create/Get/List/Delete
│ └── fs.go // WorkspaceFS JS object: ReadFile/WriteFile/ReadDir/Stat/MkdirAll/Remove/RemoveAll/Rename
├── testutils_test.go // shared test helpers ├── testutils_test.go // shared test helpers
├── workspace_test.go // CRUD tests (Create/Get/List/Update/Delete/Nodes) ├── workspace_test.go // CRUD tests (Create/Get/List/Update/Delete/Nodes)
├── fileio_test.go // File I/O + fs.FS tests ├── fileio_test.go // File I/O + fs.FS tests
@ -824,12 +829,269 @@ Docker `StopStart` ~2.2s is expected: `DefaultStopTimeout = 2s` and Docker waits
- Tests: unit + integration + benchmarks - Tests: unit + integration + benchmarks
- CI: consolidated SandboxV2Test + BenchmarkSandboxV2 - CI: consolidated SandboxV2Test + BenchmarkSandboxV2
## Phase 2: JSAPI + OAuth (PENDING) ## Phase 2: JSAPI + OAuth + Auth (PENDING)
### Prerequisites
| Task | Detail | | Task | Detail |
|------|--------| |------|--------|
| `sandbox/v2/jsapi/` | V8 `Sandbox()` / `Workspace()` constructors (registered in gou runtime) |
| Wire `openapi/oauth` | `grpc.go` currently uses random token placeholders; replace with real OAuth issue/revoke | | Wire `openapi/oauth` | `grpc.go` currently uses random token placeholders; replace with real OAuth issue/revoke |
### JSAPI Design
All JSAPI methods are static — no constructors, no Go objects in V8, no bridge/Release.
JS objects only hold string IDs, delegate everything to Go singletons (`sandbox.M()`, `workspace.M()`).
#### sandbox namespace (`RegisterObject("sandbox")`)
Static methods:
| JS | Go | Returns |
|----|-----|---------|
| `sandbox.Create(opts)` | `Manager.Create(ctx, CreateOptions)` | `Box` |
| `sandbox.Create(opts)` (opts.id set) | `Manager.GetOrCreate(ctx, CreateOptions)` | `Box` |
| `sandbox.Get(id)` | `Manager.Get(ctx, id)` | `Box \| null` |
| `sandbox.List(filter?)` | `Manager.List(ctx, ListOptions)``Box.Info()` | `BoxInfo[]` |
| `sandbox.Delete(id)` | `Manager.Remove(ctx, id)` | `void` |
`sandbox.Create(options)` — JS options → Go `CreateOptions`:
```
{
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
vnc: boolean → CreateOptions.VNC
ports: array → CreateOptions.Ports // [{container, host, 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
}
```
`sandbox.List(filter?)` — JS filter → Go `ListOptions`:
```
{
owner: string → ListOptions.Owner // empty = all
pool: string → ListOptions.Pool // empty = all
labels: object → ListOptions.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
}
```
#### Box object
Read-only properties:
| JS | Go |
|----|----|
| `box.id` | `Box.ID()` |
| `box.owner` | `Box.Owner()` |
| `box.pool` | `Box.Pool()` |
Methods:
| JS | Go | Returns |
|----|-----|---------|
| `box.Exec(cmd, opts?)` | `Box.Exec(ctx, cmd, ...ExecOption)` | `ExecResult` |
| `box.Stream(cmd, opts?)` | `Box.Stream(ctx, cmd, ...ExecOption)` | `ExecStream` |
| `box.Attach(port, opts?)` | `Box.Attach(ctx, port, ...AttachOption)` | `ServiceConn` |
| `box.VNC()` | `Box.VNC(ctx)` | `string` |
| `box.Proxy(port, path?)` | `Box.Proxy(ctx, port, path)` | `string` |
| `box.Workspace()` | `Box.WorkspaceID()``NewFSObject` | `WorkspaceFS` |
| `box.Info()` | `Box.Info(ctx)` | `BoxInfo` |
| `box.Start()` | `Box.Start(ctx)` | `void` |
| `box.Stop()` | `Box.Stop(ctx)` | `void` |
| `box.Remove()` | `Box.Remove(ctx)` | `void` |
`box.Exec(cmd, options?)`:
```
cmd: string[] → cmd []string
options: {
workdir: string, → WithWorkDir(dir)
env: object, → WithEnv(map[string]string)
timeout: number → WithTimeout(ms → time.Duration)
}
returns: {
exit_code: number, ← ExecResult.ExitCode
stdout: string, ← ExecResult.Stdout
stderr: string ← ExecResult.Stderr
}
```
`box.Stream(cmd, options?)`:
```
options: same as Exec
returns: {
stdout: ReadableStream, ← ExecStream.Stdout
stderr: ReadableStream, ← ExecStream.Stderr
stdin: WritableStream, ← ExecStream.Stdin
wait: function() → number, ← ExecStream.Wait() (int, error)
cancel: function() → void ← ExecStream.Cancel()
}
```
`box.Attach(port, options?)`:
```
port: number → port int
options: {
protocol: "ws"|"sse", → WithProtocol(protocol)
path: string, → WithPath(path)
headers: object → WithHeaders(map[string]string)
}
returns: {
url: string, ← ServiceConn.URL
read: function() → Uint8Array, ← ServiceConn.Read()
write: function(data) → void, ← ServiceConn.Write(data)
events: AsyncIterable<Uint8Array>, ← ServiceConn.Events
close: function() → void ← ServiceConn.Close()
}
```
`box.Info()` returns same structure as `BoxInfo[]` element above.
#### workspace namespace (`RegisterObject("workspace")`)
Static methods:
| JS | Go | Returns |
|----|-----|---------|
| `workspace.Create(opts)` | `Manager.Create(ctx, CreateOptions)` | `WorkspaceFS` |
| `workspace.Get(id)` | `Manager.Get(ctx, id)` | `WorkspaceFS \| null` |
| `workspace.List(filter?)` | `Manager.List(ctx, ListOptions)` | `WorkspaceInfo[]` |
| `workspace.Delete(id)` | `Manager.Delete(ctx, id, false)` | `void` |
`workspace.Create(options)` — JS options → Go `CreateOptions`:
```
{
id: string → CreateOptions.ID // optional; auto-generated if empty
name: string → CreateOptions.Name // required
owner: string → CreateOptions.Owner // required
node: string → CreateOptions.Node // required
labels: object → CreateOptions.Labels // map[string]string
}
```
`workspace.List(filter?)` — JS filter → Go `ListOptions`:
```
{
owner: string → ListOptions.Owner // empty = all
node: string → ListOptions.Node // empty = all
}
```
Returns `WorkspaceInfo[]` — each element:
```
{
id: string ← Workspace.ID
name: string ← Workspace.Name
owner: string ← Workspace.Owner
node: string ← Workspace.Node
labels: object ← Workspace.Labels
created_at: string ← Workspace.CreatedAt (ISO 8601)
updated_at: string ← Workspace.UpdatedAt (ISO 8601)
}
```
#### WorkspaceFS object
Read-only properties:
| JS | Go |
|----|----|
| `ws.id` | workspace ID |
| `ws.name` | `Workspace.Name` |
| `ws.node` | `Workspace.Node` |
Methods (1:1 to Go `taiworkspace.FS` + `Manager` shortcuts):
| JS | Go | Returns |
|----|-----|---------|
| `ws.ReadFile(path)` | `FS.ReadFile(name)` / `Manager.ReadFile(ctx, id, path)` | `string` |
| `ws.WriteFile(path, data, perm?)` | `FS.WriteFile(name, data, perm)` / `Manager.WriteFile(ctx, id, path, data, perm)` | `void` |
| `ws.ReadDir(path?)` | `FS.ReadDir(name)` / `Manager.ListDir(ctx, id, path)` | `DirEntry[]` |
| `ws.Stat(path)` | `FS.Stat(name)` | `FileInfo` |
| `ws.MkdirAll(path, perm?)` | `FS.MkdirAll(name, perm)` | `void` |
| `ws.Remove(path)` | `FS.Remove(name)` / `Manager.Remove(ctx, id, path)` | `void` |
| `ws.RemoveAll(path)` | `FS.RemoveAll(name)` | `void` |
| `ws.Rename(from, to)` | `FS.Rename(old, new)` | `void` |
Planned (not yet implemented):
| JS | Go | Returns | Note |
|----|-----|---------|------|
| `ws.ReadFileBase64(path)` | `FS.ReadFile``base64.StdEncoding.EncodeToString` | `string` | Avoids V8↔Go binary bridge overhead for images, archives, etc. |
| `ws.WriteFileBase64(path, b64, perm?)` | `base64.StdEncoding.DecodeString``FS.WriteFile` | `void` | Same — base64 string transfer is far more efficient than Uint8Array across the bridge |
| `ws.CopyFromHost(hostPath, destPath?)` | Host `os.Read``FS.WriteFile` / `FS.MkdirAll` per entry | `void` | Copy file/dir from Yao host into workspace; `destPath` defaults to basename |
| `ws.CopyFromHostArchive(hostPath, destPath?)` | Zip on host → Tai Volume upload → Tai-side unarchive | `void` | For large directory trees; requires Tai server-side unarchive support |
Return types:
```
DirEntry: { name: string, is_dir: boolean, size: number }
FileInfo: { name: string, size: number, is_dir: boolean, mod_time: string (ISO 8601) }
```
### Auth
JSAPI does not enforce permissions internally. The Go Manager methods execute operations directly without owner/admin checks.
Developers retrieve the current caller identity via the gou global `Authorized()` function (registered by `gou/runtime/v8/functions/authorized`, reads from `bridge.Share.Authorized` / `__yao_data.AUTHORIZED`) and implement permission logic in their JS scripts.
`Authorized()` returns `map[string]interface{}` (or null if not set). The exact fields depend on what the caller sets via `Context.WithAuthorized()`. There is no fixed schema — typical fields include `user_id`, `team_id`, `scope`, etc.
```javascript
const auth = Authorized() // gou global — returns caller info or null
const box = sandbox.Get(id)
// Developer decides permission logic — fields depend on application's auth setup
if (box.owner !== auth.user_id) {
throw new Error("permission denied")
}
```
Permission control is the responsibility of the caller (JS scripts, Agent hooks, API middleware, etc.).
### Implementation Tasks
| Task | Detail |
|------|--------|
| `sandbox/v2/jsapi/` | `RegisterObject("sandbox")` with Create/Get/List/Delete + Box object |
| `workspace/jsapi/` | `RegisterObject("workspace")` with Create/Get/List/Delete + FS object |
| Integration with `cmd/start.go` | Call `sandbox.Init()` + `sandbox.M().Start()` | | Integration with `cmd/start.go` | Call `sandbox.Init()` + `sandbox.M().Start()` |
## Phase 3: Agent Integration (PENDING) ## Phase 3: Agent Integration (PENDING)

View file

@ -4,49 +4,126 @@ import (
"rogchap.com/v8go" "rogchap.com/v8go"
) )
// NewBoxObject creates a JS Box object with the following methods: // 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.ID() → string // sandbox ID // # Properties (read-only)
// box.Owner() → string // owner
// box.ContainerID() → string // underlying container/pod ID
// box.Pool() → string // pool name
// box.WorkspaceID() → string // mounted workspace ID (empty if none)
// //
// box.Exec(cmd, options?) → ExecResult // run command, wait for completion // box.id → string // sandbox ID ← Box.ID()
// cmd: string[] // command + args // box.owner → string // owner user ID ← Box.Owner()
// options: { workdir, env, timeout } // box.pool → string // pool name ← Box.Pool()
// returns: { exit_code: number, stdout: string, stderr: string }
// //
// box.Stream(cmd, options?) → ExecStream // streaming I/O // # Methods — Go mapping
// returns: { stdout: ReadableStream, stderr: ReadableStream,
// stdin: WritableStream, wait: ()=>number, cancel: ()=>void }
// //
// box.Attach(port, options?) → ServiceConn // WebSocket/SSE attach // box.Exec(cmd, options?) → ExecResult
// port: number // container port
// options: { protocol, path, headers }
// returns: { url: string, close: ()=>void }
// //
// box.VNC() → string // VNC WebSocket URL // Go: Box.Exec(ctx, cmd []string, opts ...ExecOption) (*ExecResult, error)
// box.Proxy(port, path?) → string // HTTP proxy URL
// //
// box.Workspace() → WorkspaceFS // workspace file system // JS args:
// returns WorkspaceFS object (see workspace/jsapi) // cmd: string[] → cmd []string
// options: { → ExecOption functional options
// workdir: string, → WithWorkDir(dir)
// env: object, → WithEnv(map[string]string)
// timeout: number → WithTimeout(ms → time.Duration)
// }
// JS returns: {
// exit_code: number, ← ExecResult.ExitCode
// stdout: string, ← ExecResult.Stdout
// stderr: string ← ExecResult.Stderr
// }
// //
// box.Info() → BoxInfo // container status // box.Stream(cmd, options?) → ExecStream
// returns: { id, container_id, pool, owner, status, policy,
// labels, image, created_at, last_active, process_count, vnc }
// //
// box.Start() → void // start stopped box // Go: Box.Stream(ctx, cmd []string, opts ...ExecOption) (*ExecStream, error)
// box.Stop() → void // stop running box //
// box.Remove() → void // remove box permanently // JS returns: {
// box.Release() → void // release JS bridge ref // stdout: ReadableStream, ← ExecStream.Stdout
func NewBoxObject(v8ctx *v8go.Context /* , box *sandbox.Box */) (*v8go.Value, error) { // stderr: ReadableStream, ← ExecStream.Stderr
// stdin: WritableStream, ← ExecStream.Stdin
// wait: function() → number, ← ExecStream.Wait() (int, error)
// cancel: function() → void ← ExecStream.Cancel()
// }
//
// box.Attach(port, options?) → ServiceConn
//
// Go: Box.Attach(ctx, port int, opts ...AttachOption) (*ServiceConn, error)
//
// JS args:
// port: number → port int
// options: { → AttachOption functional options
// protocol: "ws"|"sse", → WithProtocol(protocol)
// path: string, → WithPath(path)
// headers: object → WithHeaders(map[string]string)
// }
// JS returns: {
// url: string, ← ServiceConn.URL
// read: function() → Uint8Array, ← ServiceConn.Read() ([]byte, error)
// write: function(data) → void, ← ServiceConn.Write(data) error
// events: AsyncIterable<Uint8Array>, ← ServiceConn.Events <-chan []byte
// close: function() → void ← ServiceConn.Close() error
// }
//
// box.VNC() → string
//
// Go: Box.VNC(ctx) (string, error)
// Returns: VNC WebSocket URL
//
// box.Proxy(port, path?) → string
//
// Go: Box.Proxy(ctx, port int, path string) (string, error)
// Returns: HTTP proxy URL
//
// box.Workspace() → WorkspaceFS
//
// Go: Box.Workspace() workspace.FS
// Box.WorkspaceID() string
// Returns: WorkspaceFS object (see workspace/jsapi/fs.go)
// Uses box.WorkspaceID() to create NewFSObject
//
// box.Info() → BoxInfo
//
// Go: Box.Info(ctx) (*BoxInfo, error)
// JS returns: {
// 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 (LifecyclePolicy)
// labels: object, ← BoxInfo.Labels (map[string]string)
// created_at: string, ← BoxInfo.CreatedAt (ISO 8601)
// last_active: string, ← BoxInfo.LastActive (ISO 8601)
// process_count: number ← BoxInfo.ProcessCount
// }
//
// 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 // TODO: Phase 2 implementation
// 1. Create ObjectTemplate with InternalFieldCount(1) // 1. Create JS object via v8go.NewObjectTemplate
// 2. Register box in bridge // 2. Set read-only properties: id, owner, pool (from sandbox.M().Get(boxID))
// 3. Bind property accessors: ID, Owner, ContainerID, Pool, WorkspaceID // 3. Bind each method as FunctionTemplate:
// 4. Bind methods: Exec, Stream, Attach, VNC, Proxy, Workspace, // - Exec → sandbox.M().Get(id).Exec(ctx, cmd, opts...)
// Info, Start, Stop, Remove, Release // - Stream → sandbox.M().Get(id).Stream(ctx, cmd, opts...)
// 5. Create instance, set internal field // - Attach → sandbox.M().Get(id).Attach(ctx, port, opts...)
// - VNC → sandbox.M().Get(id).VNC(ctx)
// - Proxy → sandbox.M().Get(id).Proxy(ctx, port, path)
// - 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 return nil, nil
} }

View file

@ -1,14 +1,24 @@
// Package jsapi registers the Sandbox() constructor into the Yao V8 runtime. // Package jsapi registers the sandbox namespace into the Yao V8 runtime.
//
// All methods are static on the sandbox object — no constructor.
// //
// # JavaScript API // # JavaScript API
// //
// const sb = new Sandbox({ pool: "default", image: "node:20", owner: "user1" }) // const box = sandbox.Create({ image: "node:20", owner: "user1" })
// const box = sb.Create({ workdir: "/app", env: { NODE_ENV: "dev" } })
// const result = box.Exec(["node", "-e", "console.log('hi')"]) // const result = box.Exec(["node", "-e", "console.log('hi')"])
// box.Remove() // console.log(result.stdout)
// //
// The constructor returns a SandboxManager object; Create/GetOrCreate returns // const box = sandbox.Get(id) // → Box
// a Box object with Exec/Stream/Attach/VNC/Proxy/Workspace/Info/Stop/Start/Remove. // const list = sandbox.List({ owner: "u1" }) // → BoxInfo[]
// sandbox.Delete(id) // → void
//
// # 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
// //
// Registration happens via init() — import with: // Registration happens via init() — import with:
// //
@ -21,30 +31,125 @@ import (
) )
func init() { func init() {
v8.RegisterFunction("Sandbox", ExportFunction) v8.RegisterObject("sandbox", ExportObject)
} }
// ExportFunction exports the Sandbox constructor to V8. // ExportObject exports the sandbox namespace object to V8.
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { func ExportObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
return v8go.NewFunctionTemplate(iso, sandboxConstructor) 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))
return obj
} }
// sandboxConstructor is called when JS executes `new Sandbox(options)`. // sbCreate: `sandbox.Create(options)` → Box
// //
// Options: // Go: Manager.Create(ctx, CreateOptions) (*Box, error)
//
// Manager.GetOrCreate(ctx, CreateOptions) (*Box, error) — when opts.id is set
//
// JS options → Go CreateOptions mapping:
// //
// { // {
// pool: string // pool name (required) // id: string → CreateOptions.ID // optional; triggers GetOrCreate
// image: string // container image (required) // owner: string → CreateOptions.Owner // required
// owner: string // owner ID (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, host, 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 a SandboxManager JS object. // Returns: Box object (see box.go)
func sandboxConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value { func sbCreate(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2 implementation // TODO: Phase 2
// 1. Parse options from args[0] // 1. Parse options from info.Args()[0]
// 2. Validate required fields (pool, image, owner) // 2. Validate required fields (image, owner)
// 3. Get sandbox.M() singleton // 3. If opts.id != "" → sandbox.M().GetOrCreate(ctx, opts)
// 4. Return NewManagerObject(v8ctx, manager, options) // 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()) return v8go.Undefined(info.Context().Isolate())
} }

View file

@ -1,45 +0,0 @@
package jsapi
import (
"rogchap.com/v8go"
)
// NewManagerObject creates a JS SandboxManager object with the following methods:
//
// manager.Create(options?) → Box // create a new sandbox box
// manager.GetOrCreate(opts) → Box // get existing or create
// manager.Get(id) → Box|null // get by sandbox ID
// manager.List(options?) → Box[] // list boxes
// manager.Remove(id) → void // remove a box
// manager.EnsureImage(ref) → void // pull image if missing
// manager.ImageExists(ref) → boolean // check image presence
// manager.Pools() → PoolInfo[] // list pool info
// manager.Release() → void // release JS bridge ref
//
// Create options (merged with constructor defaults):
//
// {
// id: string // explicit sandbox ID (optional)
// workdir: string // container working directory
// user: string // container user (e.g. "1000:1000")
// env: object // environment variables
// memory: number // memory limit in bytes
// cpus: number // CPU limit (e.g. 1.5)
// vnc: boolean // enable VNC
// ports: array // port mappings [{container: 8080, host: 0}]
// policy: string // "oneshot"|"session"|"longrunning"|"persistent"
// idle_timeout: number // idle timeout in ms
// stop_timeout: number // stop timeout in ms
// workspace_id: string // workspace to mount
// mount_mode: string // "rw"|"ro"
// mount_path: string // mount target in container
// }
func NewManagerObject(v8ctx *v8go.Context /* manager *sandbox.Manager, defaults CreateDefaults */) (*v8go.Value, error) {
// TODO: Phase 2 implementation
// 1. Create ObjectTemplate with InternalFieldCount(1)
// 2. Register manager in bridge
// 3. Bind methods: Create, GetOrCreate, Get, List, Remove,
// EnsureImage, ImageExists, Pools, Release
// 4. Create instance, set internal field
return nil, nil
}

View file

@ -4,33 +4,127 @@ import (
"rogchap.com/v8go" "rogchap.com/v8go"
) )
// NewFSObject creates a JS WorkspaceFS object implementing a file system interface. // NewFSObject creates a JS WorkspaceFS object backed by a workspace ID string.
// All methods delegate to workspace.M() → FS — no Go object passed to V8.
// //
// This is the object returned by both: // # Properties (read-only)
// - WorkspaceManager.FS(id) (standalone workspace access)
// - Box.Workspace() (sandbox-mounted workspace access)
// //
// Methods: // ws.id → string // workspace ID ← workspaceID arg
// ws.name → string // workspace name ← Workspace.Name
// ws.node → string // tai node name ← Workspace.Node
// //
// fs.ReadFile(path) → string // read UTF-8 content // # Methods — Go mapping
// fs.ReadFileBytes(path) → ArrayBuffer // read binary content //
// fs.WriteFile(path, data) → void // write string or ArrayBuffer // Each method internally does: fs, _ := workspace.M().FS(ctx, workspaceID)
// fs.Stat(path) → FileInfo // file metadata // then calls the corresponding method on taiworkspace.FS.
// returns: { name, size, mode, mod_time, is_dir } //
// fs.ReadDir(path?) → DirEntry[] // list directory (default ".") // ws.ReadFile(path) → string
// returns: [{ name, is_dir, size }] //
// fs.MkdirAll(path) → void // create directory tree // Go: FS.ReadFile(name string) ([]byte, error)
// fs.Remove(path) → void // remove single file/empty dir // — also available via Manager.ReadFile(ctx, id, path)
// fs.RemoveAll(path) → void // remove recursively // JS args: path string
// fs.Rename(from, to) → void // rename/move // JS returns: string (UTF-8 content of the file)
// fs.Close() → void // close FS handle //
// fs.Release() → void // release JS bridge ref // ws.WriteFile(path, data, perm?) → void
func NewFSObject(v8ctx *v8go.Context /* , wfs taiworkspace.FS */) (*v8go.Value, error) { //
// Go: FS.WriteFile(name string, data []byte, perm os.FileMode) error
// — also available via Manager.WriteFile(ctx, id, path, data, perm)
// JS args: path string, data string|Uint8Array, perm? number (default 0644)
//
// ws.ReadDir(path?) → DirEntry[]
//
// Go: FS.ReadDir(name string) ([]fs.DirEntry, error)
// — also available via Manager.ListDir(ctx, id, path)
// JS args: path string (default ".")
// JS returns: [{
// name: string, ← DirEntry.Name()
// is_dir: boolean, ← DirEntry.IsDir()
// size: number ← DirEntry.Info().Size()
// }]
//
// ws.Stat(path) → FileInfo
//
// Go: FS.Stat(name string) (fs.FileInfo, error)
// JS args: path string
// JS returns: {
// name: string, ← FileInfo.Name()
// size: number, ← FileInfo.Size()
// is_dir: boolean, ← FileInfo.IsDir()
// mod_time: string ← FileInfo.ModTime() (ISO 8601)
// }
//
// ws.MkdirAll(path, perm?) → void
//
// Go: FS.MkdirAll(name string, perm os.FileMode) error
// JS args: path string, perm? number (default 0755)
//
// ws.Remove(path) → void
//
// Go: FS.Remove(name string) error
// — also available via Manager.Remove(ctx, id, path)
// JS args: path string (single file or empty directory)
//
// ws.RemoveAll(path) → void
//
// Go: FS.RemoveAll(name string) error
// JS args: path string (recursive removal)
//
// ws.Rename(from, to) → void
//
// Go: FS.Rename(oldname, newname string) error
// JS args: from string, to string
//
// # Base64 variants (PLANNED — not yet implemented)
//
// Avoids V8↔Go binary bridge overhead for images, archives, etc.
//
// ws.ReadFileBase64(path) → string
//
// Go: FS.ReadFile(name) → base64.StdEncoding.EncodeToString(data)
// JS args: path string
// JS returns: string (base64-encoded content)
//
// ws.WriteFileBase64(path, b64, perm?) → void
//
// Go: base64.StdEncoding.DecodeString(b64) → FS.WriteFile(name, data, perm)
// JS args: path string, b64 string, perm? number (default 0644)
//
// # Host copy (PLANNED — not yet implemented)
//
// Copy files/dirs from Yao host filesystem into the workspace volume.
// Useful for seeding workspaces with templates, config files, assets, etc.
//
// ws.CopyFromHost(hostPath, destPath?) → void
//
// Copies a single file or directory tree from the Yao host into the workspace.
// Go: read host file(s) → FS.WriteFile / FS.MkdirAll for each entry
// JS args: hostPath string (absolute path on Yao host),
// destPath? string (target path inside workspace, default basename of hostPath)
//
// ws.CopyFromHostArchive(hostPath, destPath?) → void
//
// For large directory trees: zip on host → transfer → unzip on Tai node.
// Requires Tai server-side unarchive support.
// Go: zip hostPath → tai Volume upload → tai unarchive at destPath
// JS args: hostPath string, destPath? string (default ".")
func NewFSObject(v8ctx *v8go.Context, workspaceID string) (*v8go.Value, error) {
// TODO: Phase 2 implementation // TODO: Phase 2 implementation
// 1. Create ObjectTemplate with InternalFieldCount(1) // 1. Create JS object via v8go.NewObjectTemplate
// 2. Register FS in bridge // 2. Set read-only properties: id, name, node (from workspace.M().Get(workspaceID))
// 3. Bind methods: ReadFile, ReadFileBytes, WriteFile, // 3. Bind each method as FunctionTemplate:
// Stat, ReadDir, MkdirAll, Remove, RemoveAll, Rename, Close, Release // - ReadFile → workspace.M().FS(ctx, id).ReadFile(path)
// 4. Create instance, set internal field // - WriteFile → workspace.M().FS(ctx, id).WriteFile(path, data, perm)
// - ReadDir → workspace.M().FS(ctx, id).ReadDir(path)
// - Stat → workspace.M().FS(ctx, id).Stat(path)
// - MkdirAll → workspace.M().FS(ctx, id).MkdirAll(path, perm)
// - Remove → workspace.M().FS(ctx, id).Remove(path)
// - RemoveAll → workspace.M().FS(ctx, id).RemoveAll(path)
// - Rename → workspace.M().FS(ctx, id).Rename(old, new)
//
// PLANNED (not yet implemented):
// - ReadFileBase64 → ReadFile + base64 encode in Go
// - WriteFileBase64 → base64 decode in Go + WriteFile
// - CopyFromHost → host fs.Read → FS.Write (file-by-file)
// - CopyFromHostArchive → zip on host → tai transfer → unzip (needs Tai support)
return nil, nil return nil, nil
} }

View file

@ -1,14 +1,22 @@
// Package jsapi registers the Workspace() constructor into the Yao V8 runtime. // Package jsapi registers the workspace namespace into the Yao V8 runtime.
//
// All methods are static on the workspace object — no constructor.
// //
// # JavaScript API // # JavaScript API
// //
// const ws = new Workspace({ node: "tai-1" }) // const ws = workspace.Create({ name: "proj", owner: "user1", node: "default" })
// const info = ws.Create({ name: "my-project", owner: "user1" }) // const ws = workspace.Get(id)
// const file = ws.ReadFile(info.id, "/README.md") // ws.ReadFile("main.go") → string
// ws.WriteFile(info.id, "/app.ts", content) // ws.WriteFile("out.txt", data) → void
// ws.ReadDir("src/") → [{ name, is_dir, size }]
// workspace.Delete(id) → void
// //
// The constructor returns a WorkspaceManager object; individual workspace // # Go mapping
// files are accessed through ReadFile/WriteFile/ListDir or the FS() handle. //
// workspace.Create(opts) → Manager.Create(ctx, CreateOptions) → *Workspace → WorkspaceFS
// workspace.Get(id) → Manager.Get(ctx, id) → *Workspace → WorkspaceFS
// workspace.List(filter?) → Manager.List(ctx, ListOptions) → []*Workspace → WorkspaceInfo[]
// workspace.Delete(id) → Manager.Delete(ctx, id, false) → void
// //
// Registration happens via init() — import with: // Registration happens via init() — import with:
// //
@ -21,27 +29,101 @@ import (
) )
func init() { func init() {
v8.RegisterFunction("Workspace", ExportFunction) v8.RegisterObject("workspace", ExportObject)
} }
// ExportFunction exports the Workspace constructor to V8. // ExportObject exports the workspace namespace object to V8.
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { func ExportObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
return v8go.NewFunctionTemplate(iso, workspaceConstructor) obj := v8go.NewObjectTemplate(iso)
obj.Set("Create", v8go.NewFunctionTemplate(iso, wsCreate))
obj.Set("Get", v8go.NewFunctionTemplate(iso, wsGet))
obj.Set("List", v8go.NewFunctionTemplate(iso, wsList))
obj.Set("Delete", v8go.NewFunctionTemplate(iso, wsDelete))
return obj
} }
// workspaceConstructor is called when JS executes `new Workspace(options?)`. // wsCreate: `workspace.Create(options)` → WorkspaceFS
// //
// Options (all optional — uses global workspace.Manager if omitted): // Go: Manager.Create(ctx, CreateOptions) (*Workspace, error)
//
// JS options → Go CreateOptions mapping:
// //
// { // {
// node: string // default target node for Create (optional) // id: string → CreateOptions.ID // optional; auto-generated if empty
// name: string → CreateOptions.Name // required, human-readable name
// owner: string → CreateOptions.Owner // required, user ID
// node: string → CreateOptions.Node // required, target Tai node
// labels: object → CreateOptions.Labels // optional, map[string]string
// } // }
// //
// Returns a WorkspaceManager JS object. // Returns: WorkspaceFS object (see fs.go)
func workspaceConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value { func wsCreate(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2 implementation // TODO: Phase 2
// 1. Parse optional options from args[0] // 1. Parse options from info.Args()[0]
// 2. Get workspace manager instance // 2. Validate required fields (name, owner, node)
// 3. Return NewManagerObject(v8ctx, manager, defaults) // 3. ws := workspace.M().Create(ctx, opts)
// 4. Return NewFSObject(v8ctx, ws.ID)
return v8go.Undefined(info.Context().Isolate())
}
// wsGet: `workspace.Get(id)` → WorkspaceFS | null
//
// Go: Manager.Get(ctx, id) (*Workspace, error)
//
// Args:
//
// id: string — workspace ID
//
// Returns: WorkspaceFS object if found, null if not found
func wsGet(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2
// 1. id = info.Args()[0].String()
// 2. ws, err := workspace.M().Get(ctx, id)
// 3. Return NewFSObject(v8ctx, id) or null
return v8go.Undefined(info.Context().Isolate())
}
// wsList: `workspace.List(filter?)` → WorkspaceInfo[]
//
// Go: Manager.List(ctx, ListOptions) ([]*Workspace, error)
//
// JS filter → Go ListOptions mapping:
//
// {
// owner: string → ListOptions.Owner // filter by owner; empty = all
// node: string → ListOptions.Node // filter by node; empty = all
// }
//
// Returns: WorkspaceInfo[] — each element:
//
// {
// id: string ← Workspace.ID
// name: string ← Workspace.Name
// owner: string ← Workspace.Owner
// node: string ← Workspace.Node
// labels: object ← Workspace.Labels
// created_at: string ← Workspace.CreatedAt (ISO 8601)
// updated_at: string ← Workspace.UpdatedAt (ISO 8601)
// }
func wsList(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2
// 1. Parse optional filter from info.Args()[0]
// 2. list := workspace.M().List(ctx, opts)
// 3. Convert each *Workspace → JS object
// 4. Return JS array
return v8go.Undefined(info.Context().Isolate())
}
// wsDelete: `workspace.Delete(id)` → void
//
// Go: Manager.Delete(ctx, id string, force bool) error
//
// Args:
//
// id: string — workspace ID to remove (force = false)
func wsDelete(info *v8go.FunctionCallbackInfo) *v8go.Value {
// TODO: Phase 2
// 1. id = info.Args()[0].String()
// 2. workspace.M().Delete(ctx, id, false)
return v8go.Undefined(info.Context().Isolate()) return v8go.Undefined(info.Context().Isolate())
} }

View file

@ -1,53 +0,0 @@
package jsapi
import (
"rogchap.com/v8go"
)
// NewManagerObject creates a JS WorkspaceManager object with the following methods:
//
// wm.Create(options) → WorkspaceInfo // create workspace
// options: {
// id: string // explicit ID (optional, auto uuid)
// name: string // display name (required)
// owner: string // owner user ID (required)
// node: string // target Tai node (required, or use constructor default)
// labels: object // metadata key-value pairs
// }
// returns: { id, name, owner, node, labels, created_at, updated_at }
//
// wm.Get(id) → WorkspaceInfo|null
// wm.List(options?) → WorkspaceInfo[]
// options: { owner: string, node: string }
//
// wm.Update(id, options) → WorkspaceInfo
// options: { name: string, labels: object }
//
// wm.Delete(id, force?) → void
// force: boolean // delete even if has active mounts
//
// wm.ReadFile(id, path) → string // read file content (UTF-8)
// wm.ReadFileBytes(id, path) → ArrayBuffer // read file content (binary)
// wm.WriteFile(id, path, data) → void // write file (string or ArrayBuffer)
// wm.ListDir(id, path?) → DirEntry[] // list directory
// returns: [{ name, is_dir, size }]
// wm.Remove(id, path) → void // remove file or dir
// wm.MkdirAll(id, path) → void // create directory tree
// wm.Rename(id, from, to) → void // rename/move file
//
// wm.FS(id) → WorkspaceFS // get full FS handle
// wm.MountPath(id) → string // host mount path
// wm.Nodes() → NodeInfo[] // list available nodes
// returns: [{ name, addr, online }]
//
// wm.Release() → void // release JS bridge ref
func NewManagerObject(v8ctx *v8go.Context /* , manager *workspace.Manager, defaults ManagerDefaults */) (*v8go.Value, error) {
// TODO: Phase 2 implementation
// 1. Create ObjectTemplate with InternalFieldCount(1)
// 2. Register manager in bridge
// 3. Bind methods: Create, Get, List, Update, Delete,
// ReadFile, ReadFileBytes, WriteFile, ListDir, Remove,
// MkdirAll, Rename, FS, MountPath, Nodes, Release
// 4. Create instance, set internal field
return nil, nil
}