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

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

15 KiB

Workspace JavaScript API

All methods are available on the global workspace object. No constructor needed.

Quick Start

// Create a workspace
const ws = workspace.Create({ name: "my-project", owner: "user-123", node: "default" })

// File I/O
ws.WriteFile("src/main.go", 'package main\n\nfunc main() {}\n')
const content = ws.ReadFile("src/main.go")

// Binary file (Base64)
const b64 = ws.ReadFileBase64("image.png")
ws.WriteFileBase64("copy.png", b64)

// Clean up
workspace.Delete(ws.id)

Static Methods

workspace.Create(options) → WorkspaceFS

Create a new workspace on a Tai node.

const ws = workspace.Create({
  name:   "my-project",       // required — human-readable name
  owner:  "user-123",         // required — user ID
  node:   "default",          // required — target Tai node
  id:     "ws-custom-id",     // optional — auto-generated if empty
  labels: { team: "backend" } // optional — custom labels
})

workspace.Get(id) → WorkspaceFS | null

Get an existing workspace by ID. Returns null if not found.

const ws = workspace.Get("ws-abc123")
if (ws) {
  console.log(ws.id, ws.name, ws.node)
}

workspace.List(filter?) → WorkspaceInfo[]

List all workspaces, optionally filtered.

const all = workspace.List()
const mine = workspace.List({ owner: "user-123" })
const onNode = workspace.List({ node: "gpu-01" })

Each element:

{
  id:         "ws-abc123",
  name:       "my-project",
  owner:      "user-123",
  node:       "default",
  labels:     { team: "backend" },
  created_at: "2026-03-07T10:00:00Z",
  updated_at: "2026-03-07T10:05:00Z"
}

workspace.Delete(id) → void

Delete a workspace and its storage.

workspace.Delete("ws-abc123")

WorkspaceFS Object

Returned by workspace.Create(), workspace.Get(), box.Workspace(), and host.Workspace().

Properties (read-only)

Property Type Description
ws.id string Workspace ID
ws.name string Workspace name
ws.node string Tai node name

File Reading

ws.ReadFile(path) → string

Read file content as UTF-8 string.

const content = ws.ReadFile("src/main.go")

Go: workspace.M().ReadFile(id, name)Volume.ReadFilestring(data)

ws.ReadFileBase64(path) → string

Read file content as Base64-encoded string. Use for binary files (images, archives, etc.).

const b64 = ws.ReadFileBase64("assets/logo.png")

Go: M().ReadFilebase64.StdEncoding.EncodeToString(data)

ws.ReadFileBuffer(path) → string

Read file content as Base64 string. Alias for ReadFileBase64 (temporary — Uint8Array support pending v8go upgrade).

const b64 = ws.ReadFileBuffer("data.bin")

Go: M().ReadFilebase64.StdEncoding.EncodeToString(data)


File Writing

ws.WriteFile(path, data, perm?) → void

Write string data to a file. Creates parent directories if needed.

ws.WriteFile("src/main.go", "package main\n...")
ws.WriteFile("config.yml", yamlContent, 0644)

Go: workspace.M().WriteFile(id, name, []byte(data), perm)Volume.WriteFile — perm defaults to 0644

ws.WriteFileBase64(path, b64, perm?) → void

Write Base64-encoded data to a file. Use for binary files.

ws.WriteFileBase64("assets/logo.png", b64Data)

Go: base64.DecodeM().WriteFileVolume.WriteFile

ws.WriteFileBuffer(path, b64, perm?) → void

Write Base64-encoded data to a file. Alias for WriteFileBase64 (temporary — Uint8Array support pending v8go upgrade).

ws.WriteFileBuffer("data.bin", b64Data)

Go: base64.DecodeM().WriteFileVolume.WriteFile


Directory Operations

ws.ReadDir(path?, recursive?) → DirEntry[]

List directory contents. Defaults to root ("."), non-recursive.

// One level (default)
const entries = ws.ReadDir("src/")
// → [{ name: "main.go", ... }, { name: "utils", is_dir: true, ... }]

// Recursive — name becomes relative path
const all = ws.ReadDir("src/", true)
// → [{ name: "main.go", ... }, { name: "utils/helper.go", ... }]

entries.forEach(function(e) {
  console.log(e.name, e.is_dir ? "(dir)" : e.size + " bytes")
})

Return type:

{ name: "main.go", is_dir: false, size: 1234 }
// recursive mode: name is relative path, e.g. "utils/helper.go"

Go: non-recursive → workspace.M().ListDirVolume.ListDir; recursive → M().FS()fs.WalkDir

ws.MkdirAll(path) → void

Create a directory tree recursively. Permission is always 0755.

ws.MkdirAll("src/utils/helpers")

Go: workspace.M().MkdirAll(id, name)Volume.MkdirAll (0755)


File Operations

ws.Remove(path) → void

Remove a single file or empty directory.

ws.Remove("tmp.txt")

Go: FS.Remove(name)

ws.RemoveAll(path) → void

Remove a file or directory recursively.

ws.RemoveAll("build/")

Go: FS.RemoveAll(name)

ws.Rename(from, to) → void

Rename or move a file/directory within the workspace.

ws.Rename("old.txt", "new.txt")
ws.Rename("src/foo.go", "src/bar.go")

Go: workspace.M().Rename(id, oldname, newname)Volume.Rename

ws.Copy(src, dst, options?) → SyncResult | void

Unified copy method. Supports workspace-internal copy and host ↔ workspace copy via local:// URI prefix.

Path resolution:

  • No prefix → workspace-internal path (relative to workspace root)
  • local:// prefix → relative to App Root (config.Conf.AppSource), e.g. local:///data/templates{AppRoot}/data/templates
  • tmp:// prefix → relative to os.TempDir(), e.g. tmp:///workspace-staging/tmp/workspace-staging

Security: .. traversal is rejected for both local:// and tmp://. local:// paths escaping App Root are rejected.

Examples:

// workspace → workspace
ws.Copy("src/main.go", "src/main_backup.go")
ws.Copy("templates/", "projects/new/")

// host → workspace
ws.Copy("local:///app/templates/nextjs", "prompts/")
ws.Copy("local:///data/assistants/bot-a/config", "config/")

// workspace → host
ws.Copy("build/dist/", "local:///app/output/dist")

// host → host (no extra fs needed)
ws.Copy("local:///templates/nextjs", "local:///backup/nextjs-backup")

// tmp dir → workspace
ws.Copy("tmp:///workspace-staging/data", "imported/")

// workspace → tmp dir
ws.Copy("build/dist/", "tmp:///export-staging")

// with options (excludes, force)
ws.Copy("local:///app/templates/nextjs", "project/", {
  excludes: ["node_modules", ".git", "*.log"],
  force:    true
})

Options:

{
  excludes: ["node_modules"],     // optional — glob patterns to exclude
  force:    false                 // optional — skip incremental diff, sync everything
}

Return value:

  • Workspace internal: void
  • Host ↔ workspace (local:///tmp:// on one side): SyncResult
  • Host → host (both local:///tmp://): void
{
  files_synced:       42,         // number of files transferred
  bytes_transferred:  1048576,    // total bytes
  duration_ms:        1234        // time taken in ms
}

Dispatch rules (Go layer):

src dst Implementation
workspace workspace FS.ReadFile + FS.WriteFile (recursive for dirs)
host URI workspace Volume.SyncPush(hostPath, wsPath, opts)
workspace host URI Volume.SyncPull(wsPath, hostPath, opts)
host URI host URI os package recursive copy

Where "host URI" = local:// (relative to App Root) or tmp:// (relative to os.TempDir()).


Archive & Compression

All archive methods operate on paths within the workspace. Pack operations support an excludes option.

ws.Zip(src, dst, options?) → ArchiveResult

Create a ZIP archive from src directory to dst file.

const result = ws.Zip("src/", "dist.zip")
const filtered = ws.Zip("src/", "dist.zip", { excludes: ["*.log", "node_modules"] })

ws.Unzip(src, dst) → ArchiveResult

Extract a ZIP archive from src file to dst directory.

const result = ws.Unzip("dist.zip", "extracted/")

ws.Gzip(src, dst) → ArchiveResult

Compress a single file with gzip.

ws.Gzip("data.json", "data.json.gz")

ws.Gunzip(src, dst) → ArchiveResult

Decompress a gzip file.

ws.Gunzip("data.json.gz", "data.json")

ws.Tar(src, dst, options?) → ArchiveResult

Create a tar archive from src directory.

ws.Tar("src/", "archive.tar")
ws.Tar("src/", "archive.tar", { excludes: [".git"] })

ws.Untar(src, dst) → ArchiveResult

Extract a tar archive.

ws.Untar("archive.tar", "extracted/")

ws.Tgz(src, dst, options?) → ArchiveResult

Create a gzip-compressed tar archive (.tar.gz / .tgz).

ws.Tgz("src/", "archive.tgz")

ws.Untgz(src, dst) → ArchiveResult

Extract a gzip-compressed tar archive.

ws.Untgz("archive.tgz", "extracted/")

ArchiveResult:

{
  size_bytes:  102400,  // output file size (pack) or total extracted size (unpack)
  files_count: 15       // number of files processed
}

Go: delegates to Volume.Zip, Volume.Unzip, Volume.Gzip, Volume.Gunzip, Volume.Tar, Volume.Untar, Volume.Tgz, Volume.Untgz.


File Information

ws.Stat(path) → FileInfo

Get file or directory metadata.

const info = ws.Stat("src/main.go")
console.log(info.name, info.size, info.is_dir, info.mod_time, info.mode)

Return type:

{
  name:     "main.go",
  size:     1234,
  is_dir:   false,
  mod_time: "2026-03-07T10:00:00Z",
  mode:     0644
}

Go: FS.Stat(name) → fs.FileInfo

ws.Exists(path) → boolean

Check if a file or directory exists.

if (ws.Exists("config.yml")) {
  // ...
}

Go: FS.Stat(name) — returns true if err == nil

ws.IsDir(path) → boolean

Check if a path is a directory. Returns false if not found.

if (ws.IsDir("src/")) {
  // ...
}

Go: FS.Stat(name) → info.IsDir()

ws.IsFile(path) → boolean

Check if a path is a regular file. Returns false if not found.

if (ws.IsFile("main.go")) {
  // ...
}

Go: FS.Stat(name) → !info.IsDir()


Go Interface Reference

WorkspaceFS methods map to taiworkspace.FS and volume.Volume. Some methods (Stat, Remove, RemoveAll) use workspace.M().FS() directly; others (ReadFile, WriteFile, Rename, MkdirAll) go through workspace.M()Volume:

FS interface (defined in tai/workspace/workspace.go):

type FS interface {
    fs.FS         // Open(name) (fs.File, error)
    fs.StatFS     // Stat(name) (fs.FileInfo, error)
    fs.ReadFileFS // ReadFile(name) ([]byte, error)
    fs.ReadDirFS  // ReadDir(name) ([]fs.DirEntry, error)
    io.Closer

    WriteFile(name string, data []byte, perm os.FileMode) error
    Remove(name string) error
    RemoveAll(name string) error
    Rename(oldname, newname string) error
    MkdirAll(name string, perm os.FileMode) error
}

Archive methods delegate to volume.Volume (defined in tai/volume/volume.go):

type Volume interface {
    // ... FS methods ...
    Zip(ctx, sessionID, src, dst string, excludes []string) (*ArchiveResult, error)
    Unzip(ctx, sessionID, src, dst string) (*ArchiveResult, error)
    Gzip(ctx, sessionID, src, dst string) (*ArchiveResult, error)
    Gunzip(ctx, sessionID, src, dst string) (*ArchiveResult, error)
    Tar(ctx, sessionID, src, dst string, excludes []string) (*ArchiveResult, error)
    Untar(ctx, sessionID, src, dst string) (*ArchiveResult, error)
    Tgz(ctx, sessionID, src, dst string, excludes []string) (*ArchiveResult, error)
    Untgz(ctx, sessionID, src, dst string) (*ArchiveResult, error)
}

Exists, IsDir, IsFile are thin JSAPI wrappers over FS.Stat.

ReadDir adds a recursive parameter in Go — non-recursive calls Volume.ListDir, recursive uses M().FS() + fs.WalkDir.

Copy is implemented in Go with host URI dispatch: workspace paths use FS.ReadFile/FS.WriteFile; local:// (App Root) and tmp:// (os.TempDir()) paths use Volume.SyncPush/SyncPull with WithRemotePath; host-to-host uses os package.

Base64/Buffer variants (ReadFileBase64, ReadFileBuffer, WriteFileBase64, WriteFileBuffer) are Go-side encoding wrappers around M().ReadFile / M().WriteFile (→ Volume). Buffer variants currently use Base64 encoding (Uint8Array support pending v8go upgrade).


Method Summary

Standard FS — core file operations

Method Returns Go mapping
ws.ReadFile(path) string M().ReadFileVolume.ReadFile
ws.WriteFile(path, data, perm?) void M().WriteFileVolume.WriteFile
ws.Stat(path) FileInfo M().FS()FS.Stat
ws.MkdirAll(path) void M().MkdirAllVolume.MkdirAll (0755)
ws.Remove(path) void M().FS()FS.Remove
ws.RemoveAll(path) void M().FS()FS.RemoveAll
ws.Rename(from, to) void M().RenameVolume.Rename

Go wrapper — implemented in Go, called directly from JSAPI

Method Returns Go implementation
ws.ReadDir(path?, recursive?) DirEntry[] non-recursive: Volume.ListDir; recursive: FS + fs.WalkDir
ws.ReadFileBase64(path) string M().ReadFilebase64.Encode
ws.ReadFileBuffer(path) string M().ReadFilebase64.Encode (temp, Uint8Array pending)
ws.WriteFileBase64(path, b64, perm?) void base64.DecodeM().WriteFile
ws.WriteFileBuffer(path, b64, perm?) void base64.DecodeM().WriteFile (temp, Uint8Array pending)
ws.Copy(src, dst, opts?) void / SyncResult dispatch by host URI prefix (see below)

ws.Copy dispatch (all handled in Go):

src dst Returns Go implementation
workspace workspace void FS.ReadFile + FS.WriteFile (recursive for dirs)
host URI workspace SyncResult Volume.SyncPush with WithRemotePath
workspace host URI SyncResult Volume.SyncPull with WithRemotePath
host URI host URI void os package recursive copy

Where "host URI" = local:// (App Root) or tmp:// (os.TempDir()).

Archive — delegates to Volume interface

Method Returns Go implementation
ws.Zip(src, dst, opts?) ArchiveResult Volume.Zip
ws.Unzip(src, dst) ArchiveResult Volume.Unzip
ws.Gzip(src, dst) ArchiveResult Volume.Gzip
ws.Gunzip(src, dst) ArchiveResult Volume.Gunzip
ws.Tar(src, dst, opts?) ArchiveResult Volume.Tar
ws.Untar(src, dst) ArchiveResult Volume.Untar
ws.Tgz(src, dst, opts?) ArchiveResult Volume.Tgz
ws.Untgz(src, dst) ArchiveResult Volume.Untgz

JSAPI composition — thin JS wrappers over standard FS

Method Returns Composed from
ws.Exists(path) boolean Stat → err == nil
ws.IsDir(path) boolean Statinfo.IsDir()
ws.IsFile(path) boolean Stat!info.IsDir()

Total: 24 methods + 3 read-only properties (7 standard FS + 6 Go wrapper + 8 archive + 3 JSAPI composition)