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:
parent
c797fcf74d
commit
b7eb0e81e3
9 changed files with 768 additions and 220 deletions
29
.github/workflows/pr-test.yml
vendored
29
.github/workflows/pr-test.yml
vendored
|
|
@ -1112,6 +1112,25 @@ jobs:
|
|||
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
|
||||
run: |
|
||||
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 \
|
||||
--network k3d-tai-test \
|
||||
-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_KUBECONFIG=/etc/tai/kubeconfig.yml \
|
||||
yaoapp/tai:latest
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
- 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)
|
||||
env:
|
||||
TAI_TEST_HOST: "127.0.0.1"
|
||||
|
|
|
|||
29
.github/workflows/unit-test.yml
vendored
29
.github/workflows/unit-test.yml
vendored
|
|
@ -820,6 +820,25 @@ jobs:
|
|||
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
|
||||
run: |
|
||||
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 \
|
||||
--network k3d-tai-test \
|
||||
-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_KUBECONFIG=/etc/tai/kubeconfig.yml \
|
||||
yaoapp/tai:latest
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
- 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)
|
||||
env:
|
||||
TAI_TEST_HOST: "127.0.0.1"
|
||||
|
|
|
|||
|
|
@ -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) │
|
||||
│ ├── JSAPI: Sandbox("my-app") │
|
||||
│ ├── JSAPI: sandbox.Create/Get/List/Delete │
|
||||
│ ├── JSAPI: workspace.Create/Get/List/Delete │
|
||||
│ ├── Process: sandbox.Create, sandbox.Exec │
|
||||
│ ├── Agent: uses sandbox via interface │
|
||||
│ └── API: /api/__yao/sandbox/* │
|
||||
|
|
@ -576,8 +577,9 @@ sandbox/v2/
|
|||
├── config.go // Config struct
|
||||
├── errors.go // sentinel errors
|
||||
├── grpc.go // token creation/revocation, gRPC env var injection
|
||||
├── jsapi/ // (Phase 2) V8 JSAPI Sandbox() constructor
|
||||
│ └── sandbox.go
|
||||
├── jsapi/ // (Phase 2) V8 JSAPI sandbox.* namespace
|
||||
│ ├── 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
|
||||
├── testutils_test.go // shared test helpers (multi-pool setup)
|
||||
├── sandbox_test.go // Init/M singleton tests
|
||||
|
|
@ -729,6 +731,9 @@ workspace/
|
|||
├── workspace.go // types, metadata marshal/unmarshal
|
||||
├── manager.go // Manager: CRUD, file I/O, node management
|
||||
├── 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
|
||||
├── workspace_test.go // CRUD tests (Create/Get/List/Update/Delete/Nodes)
|
||||
├── 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
|
||||
- CI: consolidated SandboxV2Test + BenchmarkSandboxV2
|
||||
|
||||
## Phase 2: JSAPI + OAuth (PENDING)
|
||||
## Phase 2: JSAPI + OAuth + Auth (PENDING)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
| 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 |
|
||||
|
||||
### 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()` |
|
||||
|
||||
## Phase 3: Agent Integration (PENDING)
|
||||
|
|
|
|||
|
|
@ -4,49 +4,126 @@ import (
|
|||
"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
|
||||
// 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)
|
||||
// # Properties (read-only)
|
||||
//
|
||||
// box.Exec(cmd, options?) → ExecResult // run command, wait for completion
|
||||
// cmd: string[] // command + args
|
||||
// options: { workdir, env, timeout }
|
||||
// returns: { exit_code: number, stdout: string, stderr: string }
|
||||
// box.id → string // sandbox ID ← Box.ID()
|
||||
// box.owner → string // owner user ID ← Box.Owner()
|
||||
// box.pool → string // pool name ← Box.Pool()
|
||||
//
|
||||
// box.Stream(cmd, options?) → ExecStream // streaming I/O
|
||||
// returns: { stdout: ReadableStream, stderr: ReadableStream,
|
||||
// stdin: WritableStream, wait: ()=>number, cancel: ()=>void }
|
||||
// # Methods — Go mapping
|
||||
//
|
||||
// box.Attach(port, options?) → ServiceConn // WebSocket/SSE attach
|
||||
// port: number // container port
|
||||
// options: { protocol, path, headers }
|
||||
// returns: { url: string, close: ()=>void }
|
||||
// box.Exec(cmd, options?) → ExecResult
|
||||
//
|
||||
// box.VNC() → string // VNC WebSocket URL
|
||||
// box.Proxy(port, path?) → string // HTTP proxy URL
|
||||
// Go: Box.Exec(ctx, cmd []string, opts ...ExecOption) (*ExecResult, error)
|
||||
//
|
||||
// box.Workspace() → WorkspaceFS // workspace file system
|
||||
// returns WorkspaceFS object (see workspace/jsapi)
|
||||
// JS args:
|
||||
// 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
|
||||
// returns: { id, container_id, pool, owner, status, policy,
|
||||
// labels, image, created_at, last_active, process_count, vnc }
|
||||
// box.Stream(cmd, options?) → ExecStream
|
||||
//
|
||||
// box.Start() → void // start stopped box
|
||||
// box.Stop() → void // stop running box
|
||||
// box.Remove() → void // remove box permanently
|
||||
// box.Release() → void // release JS bridge ref
|
||||
func NewBoxObject(v8ctx *v8go.Context /* , box *sandbox.Box */) (*v8go.Value, error) {
|
||||
// Go: Box.Stream(ctx, cmd []string, opts ...ExecOption) (*ExecStream, error)
|
||||
//
|
||||
// JS 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?) → 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
|
||||
// 1. Create ObjectTemplate with InternalFieldCount(1)
|
||||
// 2. Register box in bridge
|
||||
// 3. Bind property accessors: ID, Owner, ContainerID, Pool, WorkspaceID
|
||||
// 4. Bind methods: Exec, Stream, Attach, VNC, Proxy, Workspace,
|
||||
// Info, Start, Stop, Remove, Release
|
||||
// 5. Create instance, set internal field
|
||||
// 1. Create JS object via v8go.NewObjectTemplate
|
||||
// 2. Set read-only properties: id, owner, pool (from sandbox.M().Get(boxID))
|
||||
// 3. Bind each method as FunctionTemplate:
|
||||
// - Exec → sandbox.M().Get(id).Exec(ctx, cmd, opts...)
|
||||
// - Stream → sandbox.M().Get(id).Stream(ctx, cmd, opts...)
|
||||
// - 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//
|
||||
// const sb = new Sandbox({ pool: "default", image: "node:20", owner: "user1" })
|
||||
// const box = sb.Create({ workdir: "/app", env: { NODE_ENV: "dev" } })
|
||||
// const box = sandbox.Create({ image: "node:20", owner: "user1" })
|
||||
// const result = box.Exec(["node", "-e", "console.log('hi')"])
|
||||
// box.Remove()
|
||||
// console.log(result.stdout)
|
||||
//
|
||||
// The constructor returns a SandboxManager object; Create/GetOrCreate returns
|
||||
// a Box object with Exec/Stream/Attach/VNC/Proxy/Workspace/Info/Stop/Start/Remove.
|
||||
// const box = sandbox.Get(id) // → Box
|
||||
// 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:
|
||||
//
|
||||
|
|
@ -21,30 +31,125 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
v8.RegisterFunction("Sandbox", ExportFunction)
|
||||
v8.RegisterObject("sandbox", ExportObject)
|
||||
}
|
||||
|
||||
// ExportFunction exports the Sandbox constructor to V8.
|
||||
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||
return v8go.NewFunctionTemplate(iso, sandboxConstructor)
|
||||
// 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))
|
||||
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)
|
||||
// image: string // container image (required)
|
||||
// owner: string // owner ID (required)
|
||||
// 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, 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.
|
||||
func sandboxConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||
// TODO: Phase 2 implementation
|
||||
// 1. Parse options from args[0]
|
||||
// 2. Validate required fields (pool, image, owner)
|
||||
// 3. Get sandbox.M() singleton
|
||||
// 4. Return NewManagerObject(v8ctx, manager, options)
|
||||
// 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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,33 +4,127 @@ import (
|
|||
"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:
|
||||
// - WorkspaceManager.FS(id) (standalone workspace access)
|
||||
// - Box.Workspace() (sandbox-mounted workspace access)
|
||||
// # Properties (read-only)
|
||||
//
|
||||
// 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
|
||||
// fs.ReadFileBytes(path) → ArrayBuffer // read binary content
|
||||
// fs.WriteFile(path, data) → void // write string or ArrayBuffer
|
||||
// fs.Stat(path) → FileInfo // file metadata
|
||||
// returns: { name, size, mode, mod_time, is_dir }
|
||||
// fs.ReadDir(path?) → DirEntry[] // list directory (default ".")
|
||||
// returns: [{ name, is_dir, size }]
|
||||
// fs.MkdirAll(path) → void // create directory tree
|
||||
// fs.Remove(path) → void // remove single file/empty dir
|
||||
// fs.RemoveAll(path) → void // remove recursively
|
||||
// fs.Rename(from, to) → void // rename/move
|
||||
// fs.Close() → void // close FS handle
|
||||
// fs.Release() → void // release JS bridge ref
|
||||
func NewFSObject(v8ctx *v8go.Context /* , wfs taiworkspace.FS */) (*v8go.Value, error) {
|
||||
// # Methods — Go mapping
|
||||
//
|
||||
// Each method internally does: fs, _ := workspace.M().FS(ctx, workspaceID)
|
||||
// then calls the corresponding method on taiworkspace.FS.
|
||||
//
|
||||
// ws.ReadFile(path) → string
|
||||
//
|
||||
// Go: FS.ReadFile(name string) ([]byte, error)
|
||||
// — also available via Manager.ReadFile(ctx, id, path)
|
||||
// JS args: path string
|
||||
// JS returns: string (UTF-8 content of the file)
|
||||
//
|
||||
// ws.WriteFile(path, data, perm?) → void
|
||||
//
|
||||
// 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
|
||||
// 1. Create ObjectTemplate with InternalFieldCount(1)
|
||||
// 2. Register FS in bridge
|
||||
// 3. Bind methods: ReadFile, ReadFileBytes, WriteFile,
|
||||
// Stat, ReadDir, MkdirAll, Remove, RemoveAll, Rename, Close, Release
|
||||
// 4. Create instance, set internal field
|
||||
// 1. Create JS object via v8go.NewObjectTemplate
|
||||
// 2. Set read-only properties: id, name, node (from workspace.M().Get(workspaceID))
|
||||
// 3. Bind each method as FunctionTemplate:
|
||||
// - ReadFile → workspace.M().FS(ctx, id).ReadFile(path)
|
||||
// - 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//
|
||||
// const ws = new Workspace({ node: "tai-1" })
|
||||
// const info = ws.Create({ name: "my-project", owner: "user1" })
|
||||
// const file = ws.ReadFile(info.id, "/README.md")
|
||||
// ws.WriteFile(info.id, "/app.ts", content)
|
||||
// const ws = workspace.Create({ name: "proj", owner: "user1", node: "default" })
|
||||
// const ws = workspace.Get(id)
|
||||
// ws.ReadFile("main.go") → string
|
||||
// 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
|
||||
// files are accessed through ReadFile/WriteFile/ListDir or the FS() handle.
|
||||
// # Go mapping
|
||||
//
|
||||
// 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:
|
||||
//
|
||||
|
|
@ -21,27 +29,101 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
v8.RegisterFunction("Workspace", ExportFunction)
|
||||
v8.RegisterObject("workspace", ExportObject)
|
||||
}
|
||||
|
||||
// ExportFunction exports the Workspace constructor to V8.
|
||||
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||
return v8go.NewFunctionTemplate(iso, workspaceConstructor)
|
||||
// ExportObject exports the workspace namespace object to V8.
|
||||
func ExportObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
|
||||
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.
|
||||
func workspaceConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||
// TODO: Phase 2 implementation
|
||||
// 1. Parse optional options from args[0]
|
||||
// 2. Get workspace manager instance
|
||||
// 3. Return NewManagerObject(v8ctx, manager, defaults)
|
||||
// Returns: WorkspaceFS object (see fs.go)
|
||||
func wsCreate(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||
// TODO: Phase 2
|
||||
// 1. Parse options from info.Args()[0]
|
||||
// 2. Validate required fields (name, owner, node)
|
||||
// 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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue