oblien-runtime

Complete Oblien workspace environment — what you are running inside (Firecracker microVM), how auth works (gateway JWT vs raw token), and the full Internal API reference: files, search, exec, terminal, and watcher on port 9990.

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "oblien-runtime" with this command: npx skills add oblien/oblien-runtime

Oblien Workspace Runtime

You are operating inside an Oblien workspace — an isolated Firecracker microVM with its own Linux kernel, filesystem, and network interface. This is not a container. You have full root access to a real Linux environment.

What you can do

The workspace runs an Internal API — an HTTP server on port 9990 inside the VM. Through this API you can:

  • Read and write files anywhere on the filesystem
  • Execute commands synchronously or stream output in real-time via SSE
  • Search code using ripgrep (content search) or filesystem search (filename matching)
  • Open interactive terminal sessions over WebSocket (multiplexed PTY)
  • Watch for file changes in real-time over WebSocket

How authentication works

There are two ways to reach this API:

MethodURLTokenWhen to use
Gatewayhttps://workspace.oblien.comGateway JWT (~1hr, signed)Calling from outside — your app, CI, SDK
Directhttp://10.x.x.x:9990Raw connection token (hex)Calling from another workspace (lower latency)

Gateway access requires public_access: true on the workspace network config. Direct access requires a private link between the two workspaces.

Key facts

  • The filesystem persists across restarts (writable overlay on top of the base image)
  • Default working directory is /root
  • Outbound internet is ON by default, inbound is OFF by default (network-dark)
  • The workspace has dedicated CPU, memory, and disk — configured at creation time
  • Port 9990 is the Internal API. Your application can use any other port.

The reference below covers every Internal API endpoint with parameters, response schemas, and code examples.

Connection & Authentication

Before using the Workspace Internal API, the HTTP server inside the VM must be enabled. Once enabled, there are two ways to connect:

MethodURLAuthNetwork requirementUse case
Gatewayworkspace.oblien.comAuthorization: Bearer <gateway_jwt>public_access: trueExternal access - your app, SDK, CI, MCP
Direct10.x.x.x:9990Authorization: Bearer <raw_token>Private link from callerWorkspace-to-workspace over private network

Both methods hit the same server and the same endpoints. The difference is how you authenticate and how the request reaches the VM.


Enable the server

Start the internal server via the Oblien API. This returns a Gateway JWT for immediate use.

SDK:


const client = new Oblien({
  clientId: process.env.OBLIEN_CLIENT_ID!,
  clientSecret: process.env.OBLIEN_CLIENT_SECRET!,
});

const access = await client.workspaces.apiAccess.enable('ws_a1b2c3d4');
console.log(access.token);   // Gateway JWT (eyJhbG...)
console.log(access.enabled); // true

REST API:

POST https://api.oblien.com/workspace/ws_a1b2c3d4/internal-api-access/enable
X-Client-ID: your_client_id
X-Client-Secret: your_client_secret

cURL:

curl -X POST "https://api.oblien.com/workspace/ws_a1b2c3d4/internal-api-access/enable" \
  -H "X-Client-ID: $OBLIEN_CLIENT_ID" \
  -H "X-Client-Secret: $OBLIEN_CLIENT_SECRET"

Note: Enable is idempotent - calling it on an already-enabled workspace returns a fresh JWT without restarting the server.


Gateway connection

Use the Gateway JWT to access the workspace through workspace.oblien.com. The JWT embeds the VM's private IP and port - the gateway decodes it and routes your request to the correct VM automatically.

Warning: The target workspace must have public_access: true in its network configuration. Without it, the gateway cannot reach the VM through the firewall. Enable it via the Network API:

await client.workspaces.network.update('ws_a1b2c3d4', { public_access: true });

SDK:

// The SDK manages tokens automatically via client.workspaces.runtime()
const rt = await client.workspaces.runtime('ws_a1b2c3d4');

const files = await rt.files.list({ dirPath: '/app' });
const result = await rt.exec.run(['ls', '-la']);
await rt.terminal.create({ shell: '/bin/bash' });

REST API:

GET https://workspace.oblien.com/files?path=/app
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

# HTTP
curl "https://workspace.oblien.com/files?path=/app" \
  -H "Authorization: Bearer $GATEWAY_JWT"

# WebSocket
wscat -c "wss://workspace.oblien.com/ws" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Warning: The URL is workspace.oblien.com/endpoint - not workspace.oblien.com/ws_id/endpoint. Routing is handled by the JWT payload, not the URL path.

Token lifetime

The standard Gateway JWT expires after ~1 hour. You have two options to manage this:

  • Rotate - call rotateToken or re-enable to get a fresh short-lived JWT
  • Force refresh - use client.workspaces.runtime(id, { force: true }) in the SDK to bypass the cached token

Both token types work the same way - Authorization: Bearer <token> against workspace.oblien.com. The only difference is expiry.


Direct connection

For workspace-to-workspace communication, connect directly to the target VM's private IP. This bypasses the gateway entirely - lower latency, no JWT overhead.

Setup flow

1. Enable the server on the target workspace
2. Create a private link from caller → target
3. Get the raw token + private IP of the target
4. Call the target directly from the calling workspace

Step 1: Enable the target

await client.workspaces.apiAccess.enable('ws_target');

Step 2: Create a private link

Private links open a network path between two workspaces. Without a link, VMs cannot reach each other - they are network-dark by default. The link whitelists the caller's IP in the target workspace's firewall.

await client.workspaces.network.update('ws_target', {
  private_link_ids: ['ws_caller'],
});

Note: The private_link_ids field takes workspace IDs, not IPs. The platform resolves each ID to its internal IP and configures the target's firewall automatically. See Private Links for details.

Step 3: Get the raw connection token

The raw token is a hex string used directly by the VM's auth middleware. Unlike the Gateway JWT, it doesn't embed routing info - you provide the IP yourself.

const raw = await client.workspaces.apiAccess.rawToken('ws_target');

console.log(raw.token); // "a1b2c3d4e5f6..."
console.log(raw.ip);    // "10.0.1.42"
console.log(raw.port);  // 9990

See the full endpoint reference at Raw token.

Step 4: Call from the other workspace

From code running inside ws_caller, call ws_target directly over the private network:

// Running inside ws_caller
const res = await fetch('http://10.0.1.42:9990/files?path=/app', {
  headers: { 'Authorization': `Bearer ${raw.token}` },
});
const files = await res.json();
curl "http://10.0.1.42:9990/exec" \
  -H "Authorization: Bearer a1b2c3d4e5f6..." \
  -H "Content-Type: application/json" \
  -d '{"cmd":["npm","test"]}'

Note: Direct calls go VM-to-VM - no gateway, no JWT encoding/decoding overhead. This is the lowest-latency way to interact with a workspace.


Disable the server

Stop the internal server, kill all sessions, and close connections.

await client.workspaces.apiAccess.disable('ws_a1b2c3d4');

See the full endpoint reference at Disable server.


Token comparison

Gateway JWTRaw Connection Token
Use withworkspace.oblien.comDirect 10.x.x.x:9990
Auth headerAuthorization: Bearer <jwt>Authorization: Bearer <raw_token>
Lifetime~1 hour (standard) or 30 days (workspace token)Until rotated
Contains VM IPYes (embedded in JWT)No (you get the IP separately)
How to getenable / rotateTokenrawToken
When to useExternal access - apps, SDK, CI, MCPWorkspace-to-workspace orchestration

Full API access reference

All server management endpoints are on the Oblien API at api.oblien.com:

EndpointMethodDescription
/workspace/:id/internal-api-accessGETServer status
/workspace/:id/internal-api-access/enablePOSTEnable server
/workspace/:id/internal-api-access/disablePOSTDisable server
/workspace/:id/internal-api-access/tokenPOSTRotate token
/workspace/:id/internal-api-access/token/rawGETRaw token + IP
/workspace/:id/internal-api-access/reconnectPOSTReconnect
/workspace/:id/internal-api-access/workspacePOST30-day token

Files

The file system endpoints let you list, read, write, and delete files inside the workspace VM. All paths are absolute filesystem paths (e.g. /app/src/main.go).

Note: Requires the internal server to be enabled. All requests require a valid token - see Connection & Auth.

List directory

List files and directories in a given path. Supports recursive traversal, content inclusion, hash computation, and filtering.

SDK:

const rt = await client.workspaces.runtime('ws_a1b2c3d4');

const result = await rt.files.list({
  dirPath: '/app/src',
  nested: true,
  flatten: true,
  includeContent: true,
  codeFilesOnly: true,
  maxDepth: 5,
});

console.log(result.entries);       // fileEntry[]
console.log(result.count);         // number of entries

REST API:

GET https://workspace.oblien.com/files?path=/app/src&nested=true&flatten=true&include_content=true&code_files_only=true&max_depth=5
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/files?path=/app/src&nested=true&flatten=true&include_content=true&code_files_only=true&max_depth=5" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Parameters

ParameterTypeRequiredDescription
pathstringNoDirectory path to list. Defaults to /
nestedbooleanNoRecurse into subdirectories. Default false
flattenbooleanNoReturn flat list instead of tree. Default false
lightbooleanNoOmit size and modified time for faster response. Default false
include_hashbooleanNoInclude SHA-256 hash for each file. Default false
include_contentbooleanNoInclude file content inline. Default false
include_extensionsbooleanNoInclude file extension field. Default false
code_files_onlybooleanNoOnly return code/config files. Default false
use_gitignorebooleanNoRespect .gitignore rules. Default true
max_depthintegerNoMaximum recursion depth. Default 20
path_filterstringNoCase-insensitive substring filter on path
include_extstringNoComma-separated extensions to include (e.g. js,ts,go)
ignore_patternsstringNoComma-separated glob patterns to ignore
max_content_budgetintegerNoMax total bytes for inline content. Default ~50 MiB

Response

{
  "success": true,
  "path": "/app/src",
  "entries": [
    {
      "name": "main.go",
      "path": "/app/src/main.go",
      "type": "file",
      "size": 1234,
      "modified": "2025-01-15T10:30:00Z",
      "extension": ".go",
      "content": "package main\n...",
      "hash": "a1b2c3..."
    },
    {
      "name": "utils",
      "path": "/app/src/utils",
      "type": "directory",
      "children": [...]
    }
  ],
  "count": 42
}

Warning: The list endpoint is capped at 50,000 entries. For large directories, use path_filter, include_ext, or code_files_only to narrow results, or use the stream endpoint for NDJSON streaming.


Stream directory

Stream directory entries as NDJSON (newline-delimited JSON). Ideal for large directories - entries flow to the client as they're discovered, without accumulating in memory.

SDK:

for await (const entry of rt.files.stream({
  dirPath: '/app',
  includeContent: true,
  codeFilesOnly: true,
})) {
  console.log(entry.name, entry.path);
}

REST API:

GET https://workspace.oblien.com/files/stream?path=/app&include_content=true&code_files_only=true
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response: Content-Type: application/x-ndjson

cURL:

curl -N "https://workspace.oblien.com/files/stream?path=/app&include_content=true&code_files_only=true" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Parameters

Same as List directory. The nested and flatten options are always enabled for streaming.

Response format

Each line is a JSON object. The stream starts with a start event and ends with a done event:

{"event":"start","path":"/app"}
{"name":"main.go","path":"/app/main.go","type":"file","size":1234}
{"name":"utils.go","path":"/app/utils.go","type":"file","size":567}
{"event":"done","count":2}

Note: The stream endpoint uses batched directory reads for memory efficiency. Entries are not sorted - they arrive in filesystem order. Use the list endpoint if you need sorted output.


Read file

Read the content of a file. Supports line ranges for partial reads.

SDK:

const file = await rt.files.read({
  filePath: '/app/src/main.go',
});

console.log(file.content);    // file content as string
console.log(file.lines);      // number of lines returned
console.log(file.size);       // file size in bytes

// Read specific line range
const partial = await rt.files.read({
  filePath: '/app/src/main.go',
  startLine: 10,
  endLine: 25,
  withLineNumbers: true,
});

REST API:

GET https://workspace.oblien.com/files/read?path=/app/src/main.go&start_line=10&end_line=25&with_line_numbers=true
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/files/read?path=/app/src/main.go&start_line=10&end_line=25&with_line_numbers=true" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Parameters

ParameterTypeRequiredDescription
pathstringYesAbsolute path to the file
start_lineintegerNoFirst line to read (1-based)
end_lineintegerNoLast line to read (1-based, inclusive)
with_line_numbersbooleanNoPrefix each line with its line number

Response

{
  "success": true,
  "path": "/app/src/main.go",
  "content": "package main\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}",
  "size": 1234,
  "lines": 5,
  "extension": ".go",
  "start_line": 10,
  "end_line": 25
}

start_line and end_line are only included when a line range was requested.


Write file

Create or overwrite a file. Uses atomic write (temp file + rename) by default. Accepts both POST and PUT.

SDK:

const result = await rt.files.write({
  fullPath: '/app/src/hello.txt',
  content: 'Hello, world!',
  createDirs: true,
});

console.log(result.path);  // "/app/src/hello.txt"
console.log(result.size);  // 13

// Append to an existing file
await rt.files.write({
  fullPath: '/app/logs/output.log',
  content: 'New log entry\n',
  append: true,
  createDirs: true,
});

REST API:

POST https://workspace.oblien.com/files/write
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "path": "/app/src/hello.txt",
  "content": "Hello, world!",
  "create_dirs": true
}

cURL:

curl -X POST "https://workspace.oblien.com/files/write" \
  -H "Authorization: Bearer $GATEWAY_JWT" \
  -H "Content-Type: application/json" \
  -d '{"path":"/app/src/hello.txt","content":"Hello, world!","create_dirs":true}'

Parameters

ParameterTypeRequiredDescription
pathstringYesAbsolute path for the file
contentstringYesFile content
create_dirsbooleanNoCreate parent directories if they don't exist. Default false
appendbooleanNoAppend to existing file instead of overwriting. Default false
modestringNoFile permissions in octal (e.g. "0644"). Default "0644"

Response

{
  "success": true,
  "path": "/app/src/hello.txt",
  "size": 13
}

HTTP status: 201 Created


Create directory

Create a directory and any necessary parent directories.

SDK:

await rt.files.mkdir({
  path: '/app/src/utils/helpers',
});

REST API:

POST https://workspace.oblien.com/files/mkdir
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "path": "/app/src/utils/helpers"
}

cURL:

curl -X POST "https://workspace.oblien.com/files/mkdir" \
  -H "Authorization: Bearer $GATEWAY_JWT" \
  -H "Content-Type: application/json" \
  -d '{"path":"/app/src/utils/helpers"}'

Parameters

ParameterTypeRequiredDescription
pathstringYesDirectory path to create
modestringNoDirectory permissions in octal (e.g. "0755"). Default "0755"

Response

{
  "success": true,
  "path": "/app/src/utils/helpers"
}

HTTP status: 201 Created


Stat

Get detailed information about a file or directory.

SDK:

const info = await rt.files.stat({
  path: '/app/src/main.go',
});

console.log(info.type);        // "file"
console.log(info.size);        // 1234
console.log(info.permissions); // "0644"
console.log(info.is_code);     // true

REST API:

GET https://workspace.oblien.com/files/stat?path=/app/src/main.go
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/files/stat?path=/app/src/main.go" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Parameters

ParameterTypeRequiredDescription
pathstringYesPath to the file or directory

Response

{
  "success": true,
  "path": "/app/src/main.go",
  "name": "main.go",
  "type": "file",
  "size": 1234,
  "modified": "2025-01-15T10:30:00Z",
  "permissions": "0644",
  "is_code": true,
  "extension": ".go"
}

For symlinks:

{
  "success": true,
  "path": "/app/link",
  "name": "link",
  "type": "symlink",
  "size": 0,
  "modified": "2025-01-15T10:30:00Z",
  "permissions": "0777",
  "is_code": false,
  "symlink_target": "/app/src/main.go"
}

Delete

Delete a file or directory. Directories are removed recursively.

SDK:

await rt.files.delete({
  path: '/app/src/old-file.txt',
});

REST API:

DELETE https://workspace.oblien.com/files/delete?path=/app/src/old-file.txt
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl -X DELETE "https://workspace.oblien.com/files/delete?path=/app/src/old-file.txt" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Parameters

ParameterTypeRequiredDescription
pathstringYesPath to the file or directory to delete

The path can also be provided in the request body as {"path": "..."}.

Response

{
  "success": true,
  "path": "/app/src/old-file.txt"
}

Warning: System paths (/, /bin, /sbin, /usr, /lib, /lib64, /etc, /dev, /proc, /sys, /boot, /run) are protected and cannot be deleted.


Error responses

All file endpoints return errors in a consistent format:

{
  "error": "file not found: /app/missing.txt"
}
StatusMeaning
400Invalid parameters (missing path, path is a directory when file expected, etc.)
401Missing or invalid token
403Attempted to delete a protected system path
404File or directory not found
413File too large to read or content too large to write
500Internal server error

Search

Two search modes are available inside the workspace:

  • Content search - powered by ripgrep, searches file contents with regex, whole-word, and context line support
  • Filename search - built-in filesystem search that matches against file paths

Note: Requires the internal server to be enabled. Content search requires ripgrep - use the install endpoint to set it up.


Content search

Search file contents using ripgrep. Results are grouped by file with line numbers and match context.

SDK:

const rt = await client.workspaces.runtime('ws_a1b2c3d4');

const results = await rt.search.content({
  query: 'handleRequest',
  path: '/app/src',
  contextLines: 2,
  maxResults: 100,
});

for (const file of results.results) {
  console.log(file.path);
  for (const match of file.matches) {
    console.log(`  Line ${match.line}: ${match.text}`);
  }
}

REST API:

GET https://workspace.oblien.com/files/search?q=handleRequest&path=/app/src&context_lines=2&max_results=100
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/files/search?q=handleRequest&path=/app/src&context_lines=2&max_results=100" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Parameters

ParameterTypeRequiredDescription
qstringYesSearch query (max 1000 characters)
pathstringNoDirectory or file to search in. Default /
case_sensitivebooleanNoCase-sensitive matching. Default false
regexbooleanNoTreat query as a regex pattern. Default false
whole_wordbooleanNoMatch whole words only. Default false
max_resultsintegerNoMaximum number of matches. Default 100
timeoutintegerNoTimeout in seconds (max 60). Default 10
context_linesintegerNoLines of context around matches (0–10). Default 0
file_typesstringNoComma-separated file extension filters (e.g. go,js,py). Converted to glob patterns internally
include_hiddenbooleanNoInclude hidden files/directories. Default false
no_gitignorebooleanNoIgnore .gitignore rules. Default false
ignore_patternsstringNoComma-separated glob patterns to skip

Response

Results are returned as an object keyed by file path, with each value being an array of matches:

{
  "success": true,
  "query": "handleRequest",
  "path": "/app/src",
  "results": {
    "src/server.go": [
      {
        "line": 42,
        "column": 6,
        "text": "func handleRequest(w http.ResponseWriter, r *http.Request) {"
      },
      {
        "line": 105,
        "column": 2,
        "text": "\thandleRequest(w, r)"
      }
    ],
    "src/router.go": [
      {
        "line": 18,
        "column": 12,
        "text": "\trouter.HandleFunc(\"/\", handleRequest)"
      }
    ]
  },
  "total_matches": 3,
  "total_files": 2,
  "capped": false
}
FieldDescription
resultsObject mapping file paths to arrays of matches
total_matchesTotal number of matching lines across all files
total_filesNumber of files with at least one match
cappedtrue if max_results was reached before all matches were found

Filename search

Search for files by name. Uses a fast filesystem walk with substring matching on relative paths.

SDK:

const results = await rt.search.files({
  query: 'controller',
  path: '/app/src',
  maxResults: 50,
});

console.log(results.files);        // ["auth/controller.go", "user/controller.go"]
console.log(results.total_files);  // 2

REST API:

GET https://workspace.oblien.com/files/search/files?q=controller&path=/app/src&max_results=50
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/files/search/files?q=controller&path=/app/src&max_results=50" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Parameters

ParameterTypeRequiredDescription
qstringYesSearch query - matches against relative file paths (max 1000 characters)
pathstringNoDirectory to search in. Default /
case_sensitivebooleanNoCase-sensitive matching. Default false
include_hiddenbooleanNoInclude hidden files/directories. Default false
max_resultsintegerNoMaximum number of results. Default 200
ignore_patternsstringNoComma-separated glob patterns to skip

Response

{
  "success": true,
  "query": "controller",
  "path": "/app/src",
  "files": [
    "auth/controller.go",
    "user/controller.go"
  ],
  "total_files": 2
}

Note: Filename search does not require ripgrep - it works out of the box with no additional setup.


Ripgrep status

Check if ripgrep is installed and available for content search.

SDK:

const status = await rt.search.status();
console.log(status.installed); // true or false
console.log(status.version);   // "ripgrep 14.1.0" or null

REST API:

GET https://workspace.oblien.com/files/search/init
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/files/search/init" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

When ripgrep is installed:

{
  "success": true,
  "installed": true,
  "path": "/usr/local/bin/rg",
  "version": "ripgrep 14.1.0"
}

When not installed:

{
  "success": true,
  "installed": false,
  "message": "ripgrep is not installed - use POST /files/search/init to install"
}

Install ripgrep

Download and install ripgrep from GitHub releases. This is a one-time setup per workspace.

SDK:

const result = await rt.search.init();
console.log(result.installed); // true
console.log(result.version);   // "14.1.1"

REST API:

POST https://workspace.oblien.com/files/search/init
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl -X POST "https://workspace.oblien.com/files/search/init" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "success": true,
  "version": "ripgrep 14.1.0",
  "path": "/usr/local/bin/rg"
}

If ripgrep is already installed, the endpoint returns the current installation info without re-downloading.

Warning: Installation requires internet access from inside the workspace to download from GitHub releases. The binary is installed to /usr/local/bin/rg.


Error responses

StatusMeaning
400Missing q parameter, query too long, or path is not a directory
401Missing or invalid token
404Search path not found
409Ripgrep installation already in progress
503Content search requested but ripgrep is not installed
504Search timed out

Command Execution

The exec endpoints let you run commands inside the workspace VM. Commands can run synchronously (wait for result) or asynchronously (stream output via SSE). Long-running tasks persist in the background and can be polled, streamed, or killed.

Note: Requires the internal server to be enabled.

Overview

Two execution modes:

ModeHowUse case
SynchronousPOST /exec (no stream flag)Quick commands - get stdout/stderr in response
StreamingPOST /exec with stream: true, or GET /exec/stream?task_id=IDLong-running tasks - real-time output via SSE

Run command

Execute a command inside the workspace.

SDK:

// Synchronous - wait for result
const rt = await client.workspaces.runtime('ws_a1b2c3d4');
const result = await rt.exec.run(['echo', 'hello']);

console.log(result.exit_code);  // 0
console.log(result.stdout);     // "hello\n"
console.log(result.stderr);     // ""
// Streaming - real-time output via async generator
for await (const ev of rt.exec.stream(['npm', 'install'])) {
  if (ev.event === 'stdout') process.stdout.write(atob(ev.data));
  if (ev.event === 'stderr') process.stderr.write(atob(ev.data));
  if (ev.event === 'exit') console.log(`Done: ${ev.exit_code}`);
}

REST API:

Synchronous:

POST https://workspace.oblien.com/exec
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "cmd": ["echo", "hello"]
}

Streaming (SSE):

POST https://workspace.oblien.com/exec
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "cmd": ["npm", "install"],
  "stream": true
}

cURL:

# Synchronous
curl -X POST "https://workspace.oblien.com/exec" \
  -H "Authorization: Bearer $GATEWAY_JWT" \
  -H "Content-Type: application/json" \
  -d '{"cmd": ["echo", "hello"]}'

# Streaming (SSE)
curl -N -X POST "https://workspace.oblien.com/exec" \
  -H "Authorization: Bearer $GATEWAY_JWT" \
  -H "Content-Type: application/json" \
  -d '{"cmd": ["npm", "install"], "stream": true}'

Parameters

ParameterTypeRequiredDescription
cmdstring[]YesThe command to execute as an array (e.g. ["node", "app.js"])
streambooleanNoIf true, returns SSE stream instead of waiting
exec_modestringNoauto (default), shell, or direct
timeout_secondsintegerNoKill command after N seconds. Default 0 (no timeout)
ttl_secondsintegerNoKeep task metadata for N seconds after exit. Default 0 (uses 5-minute server default). Set to -1 to never expire
keep_logsbooleanNoRetain stdout/stderr after completion. Default false

Execution modes

ModeBehavior
autoUses shell if cmd contains shell metacharacters (|, &, ;, etc.), otherwise runs directly
shellAlways wraps in /bin/sh -c "..."
directSplits and runs directly - no shell interpretation

Synchronous response

The response is the full task object:

{
  "id": "abc123",
  "command": ["echo", "hello"],
  "status": "exited",
  "guest_pid": 4521,
  "exit_code": 0,
  "stdout": "hello\n",
  "stderr": "",
  "created_at": "2025-01-15T10:30:00Z",
  "started_at": "2025-01-15T10:30:00Z",
  "exited_at": "2025-01-15T10:30:01Z",
  "ttl_seconds": 300
}

Streaming response (SSE)

When stream: true, the response is an SSE stream. The data payloads are JSON. stdout/stderr content is base64-encoded:

event: task_id
data: {"task_id":"abc123"}

event: stdout
data: {"data":"SW5zdGFsbGluZyBkZXBlbmRlbmNpZXMuLi4="}

event: stderr
data: {"data":"bnBtIHdhcm4gZGVwcmVjYXRlZA=="}

event: exit
data: {"exit_code": 0, "pid": 4521}
EventData formatDescription
task_id{"task_id": "..."}Task identifier for future polling
stdout{"data": "..."}Standard output chunk (base64-encoded)
stderr{"data": "..."}Standard error chunk (base64-encoded)
exit{"exit_code": N, "pid": N}Process finished

List tasks

List all tracked tasks.

SDK:

const tasks = await rt.exec.list();

for (const task of tasks) {
  console.log(`${task.id}: ${task.command.join(' ')} (status: ${task.status})`);
}

REST API:

GET https://workspace.oblien.com/exec
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/exec" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "success": true,
  "tasks": [
    {
      "id": "abc123",
      "command": ["npm", "install"],
      "status": "running",
      "guest_pid": 4521,
      "created_at": "2025-01-15T10:30:00Z",
      "started_at": "2025-01-15T10:30:00Z",
      "ttl_seconds": 300
    }
  ]
}

Task status values: pending, running, exited, failed.


Get task

Get the status and output of a specific task.

SDK:

const task = await rt.exec.get('abc123');

console.log(task.status);     // "exited"
console.log(task.exit_code);  // 0
console.log(task.stdout);     // "full output..."

REST API:

GET https://workspace.oblien.com/exec/abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/exec/abc123" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "id": "abc123",
  "command": ["npm", "install"],
  "status": "exited",
  "guest_pid": 4521,
  "exit_code": 0,
  "stdout": "added 150 packages in 12s\n",
  "stderr": "",
  "created_at": "2025-01-15T10:30:00Z",
  "started_at": "2025-01-15T10:30:00Z",
  "exited_at": "2025-01-15T10:30:12Z",
  "ttl_seconds": 300
}

Kill task

Remove a task from tracking and close its stdin pipe.

SDK:

await rt.exec.kill('abc123');

REST API:

DELETE https://workspace.oblien.com/exec/abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl -X DELETE "https://workspace.oblien.com/exec/abc123" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "success": true
}

Delete all tasks

Remove all tasks from tracking and close stdin pipes.

REST API:

DELETE https://workspace.oblien.com/exec
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl -X DELETE "https://workspace.oblien.com/exec" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "success": true,
  "deleted": 3
}

Send stdin

Send input to a running task's stdin. The request body is sent as raw bytes (not JSON).

SDK:

await rt.exec.input('abc123', 'yes\n');

REST API:

POST https://workspace.oblien.com/exec/abc123/input
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/octet-stream

yes

cURL:

curl -X POST "https://workspace.oblien.com/exec/abc123/input" \
  -H "Authorization: Bearer $GATEWAY_JWT" \
  --data-binary 'yes
'

Response

{
  "success": true,
  "bytes_written": 4
}

Stream output (SSE)

Subscribe to real-time output from a running task. This is useful when you started a task with stream: false or from another client and want to attach to its output.

SDK:

for await (const ev of rt.exec.subscribe('abc123')) {
  if (ev.event === 'stdout') process.stdout.write(atob(ev.data));
  if (ev.event === 'stderr') process.stderr.write(atob(ev.data));
  if (ev.event === 'exit') console.log(`Exited: ${ev.exit_code}`);
}

REST API:

GET https://workspace.oblien.com/exec/stream?task_id=abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: text/event-stream

Or create and stream a new task via POST (alias for POST /exec with stream: true):

POST https://workspace.oblien.com/exec/stream
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "cmd": ["npm", "install"]
}

cURL:

curl -N "https://workspace.oblien.com/exec/stream?task_id=abc123" \
  -H "Authorization: Bearer $GATEWAY_JWT"

SSE events

event: stdout
data: {"data":"SW5zdGFsbGluZyBkZXBlbmRlbmNpZXMuLi4="}

event: stderr
data: {"data":"bnBtIHdhcm4gZGVwcmVjYXRlZA=="}

event: exit
data: {"exit_code": 0, "pid": 4521}

When subscribing to a task that has already finished, the server sends an output event with buffered stdout/stderr as raw text, then the exit event.


Error responses

StatusMeaning
400Missing cmd field or invalid parameters
401Missing or invalid token
404Task not found
405Method not allowed
429Too many tasks (max 50 concurrent)
500Failed to start process

Terminal Sessions

The terminal endpoints let you create interactive PTY sessions inside the workspace VM. Terminal I/O is multiplexed over a single WebSocket connection.

Note: Requires the internal server to be enabled. Up to 10 concurrent terminal sessions per workspace.

Overview

1. Create a terminal session  →  get session ID
2. Open WebSocket at /ws      →  bidirectional I/O
3. Send stdin as binary       →  [id_byte][data]
4. Receive stdout as binary   →  [id_byte][data]
5. Resize / close via JSON messages or REST

Create session

Create a new terminal session with an interactive PTY.

SDK:

const rt = await client.workspaces.runtime('ws_a1b2c3d4');

const term = await rt.terminal.create({
  shell: '/bin/bash',
  cols: 120,
  rows: 40,
});

console.log(term.id);      // "1"
console.log(term.cols);    // 120
console.log(term.rows);    // 40

REST API:

POST https://workspace.oblien.com/terminals
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "cmd": ["/bin/bash"],
  "cols": 120,
  "rows": 40
}

cURL:

curl -X POST "https://workspace.oblien.com/terminals" \
  -H "Authorization: Bearer $GATEWAY_JWT" \
  -H "Content-Type: application/json" \
  -d '{"cmd":["/bin/bash"],"cols":120,"rows":40}'

Parameters

ParameterTypeRequiredDescription
cmdstring[]NoCommand to run (e.g. ["/bin/bash"]). Falls back to default shell
commandstring[]NoAlias for cmd
colsintegerNoTerminal width in columns
rowsintegerNoTerminal height in rows

Response

{
  "success": true,
  "id": "1",
  "cols": 120,
  "rows": 40,
  "command": ["/bin/bash"]
}

HTTP status: 201 Created


List sessions

List all active terminal sessions.

SDK:

const sessions = await rt.terminal.list();

for (const term of sessions) {
  console.log(`${term.id}: ${term.command.join(' ')} (alive: ${term.alive})`);
}

REST API:

GET https://workspace.oblien.com/terminals
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/terminals" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "success": true,
  "terminals": [
    {
      "id": "1",
      "command": ["/bin/bash"],
      "cols": 120,
      "rows": 40,
      "alive": true,
      "exit_code": 0,
      "created_at": "2025-01-15T10:30:00Z"
    }
  ]
}

Close session

Close a terminal session and kill its process.

SDK:

await rt.terminal.close('1');

REST API:

DELETE https://workspace.oblien.com/terminals/1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl -X DELETE "https://workspace.oblien.com/terminals/1" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "success": true,
  "terminal_id": "1"
}

Get scrollback

Retrieve the scrollback buffer for a terminal session. Useful for restoring terminal state after reconnection.

SDK:

const scrollback = await rt.terminal.scrollback('1');

console.log(scrollback.size);       // bytes in buffer
console.log(scrollback.alive);      // session still running
console.log(scrollback.scrollback); // base64-encoded data

REST API:

GET https://workspace.oblien.com/terminals/1/scrollback
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/terminals/1/scrollback" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "success": true,
  "scrollback": "dXNlckBzYW5kYm94Oi9hcHAkIA==",
  "size": 2048,
  "alive": true,
  "exit_code": 0
}
FieldDescription
scrollbackBase64-encoded terminal output (64 KiB ring buffer)
sizeSize of the scrollback data in bytes
aliveWhether the session is still running
exit_codeProcess exit code (0 if still alive)

WebSocket

Terminal I/O flows over a single multiplexed WebSocket connection at /ws. Multiple terminal sessions share the same connection.

Connect

SDK:

// Create a terminal session
const term = await rt.terminal.create({ shell: '/bin/bash' });

// Open a RuntimeWebSocket for bidirectional I/O
const ws = rt.ws();

ws.onTerminalOutput((id, data) => {
  process.stdout.write(data);
});

ws.onClose(() => console.log('WebSocket closed'));

await ws.connect();
ws.writeTerminalInput(term.id, 'ls -la\n');
ws.resizeTerminal(term.id, 160, 50);

REST API:

WebSocket: wss://workspace.oblien.com/ws
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

JavaScript:

const ws = new WebSocket('wss://workspace.oblien.com/ws', {
  headers: { Authorization: `Bearer ${gatewayJwt}` },
});

ws.binaryType = 'arraybuffer';

ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    // Binary: terminal output
    const bytes = new Uint8Array(event.data);
    const terminalId = bytes[0];
    const data = bytes.slice(1);
    console.log(`Terminal ${terminalId}:`, new TextDecoder().decode(data));
  } else {
    // Text: control messages (exit, etc.)
    const msg = JSON.parse(event.data);
    console.log('Control:', msg);
  }
};

Protocol

Binary frames

DirectionFormatDescription
Client → Server[id_byte][stdin_data]Send input to terminal
Server → Client[id_byte][stdout_data]Receive output from terminal

The first byte is the terminal ID byte (mapped from the session ID). The remaining bytes are raw terminal data.

Text frames

Resize a terminal:

{
  "channel": "terminal",
  "type": "resize",
  "id": "1",
  "cols": 160,
  "rows": 50
}

Terminal exit notification (server → client):

{
  "channel": "terminal",
  "type": "exit",
  "id": "1",
  "code": 0
}

On connect

When a WebSocket connection is established, the server automatically sends:

  1. Scrollback data - binary frames with buffered output for each active session
  2. Exit notifications - text frames for any sessions that have already exited

This allows clients to restore terminal state after reconnection without explicit scrollback requests.


Error responses

StatusMeaning
400Missing terminal ID
401Missing or invalid token
404Terminal session not found
405Method not allowed
500Failed to create PTY session

File Watcher

Watch directories for file system changes in real time. The watcher monitors filesystem events and streams them over the WebSocket connection at /ws.

Note: Requires the internal server to be enabled. Up to 5 concurrent watchers per workspace.

Overview

1. Create a watcher via REST    →  get watcher ID
2. Open WebSocket at /ws        →  receive "ready" event
3. File changes in watched dir  →  receive "change" events
4. Delete watcher when done     →  cleanup

Watcher events arrive as JSON text frames on the "watcher" channel of the same WebSocket used for terminal I/O.


Create watcher

Start watching a directory for changes. The watcher recursively monitors all subdirectories and streams events over the WebSocket.

SDK:

const rt = await client.workspaces.runtime('ws_a1b2c3d4');

const watcher = await rt.watcher.create({
  path: '/app/src',
  excludes: ['*.log', 'tmp'],
});

console.log(watcher.id);   // "1"
console.log(watcher.root); // "/app/src"
console.log(watcher.dirs); // 42

REST API:

POST https://workspace.oblien.com/watchers
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "path": "/app/src",
  "excludes": ["*.log", "tmp"]
}

cURL:

curl -X POST "https://workspace.oblien.com/watchers" \
  -H "Authorization: Bearer $GATEWAY_JWT" \
  -H "Content-Type: application/json" \
  -d '{"path": "/app/src", "excludes": ["*.log", "tmp"]}'

Parameters

ParameterTypeRequiredDescription
pathstringYesDirectory to watch (recursively)
excludesstring[]NoGlob patterns to exclude (merged with defaults)

Response

{
  "id": "1",
  "root": "/app/src",
  "dirs": 42,
  "excludes": ["node_modules", ".git", "*.log", "tmp"]
}

Default excludes

These patterns are always excluded, even if you don't specify any:

node_modules, .git, .svn, .hg, __pycache__, .pytest_cache,
.mypy_cache, .next, .nuxt, dist, build, .DS_Store, *.swp, *.swo, *~

Your custom excludes are merged with these defaults.


List watchers

Get all active watchers.

SDK:

const watchers = await rt.watcher.list();

for (const w of watchers) {
  console.log(`${w.id}: watching ${w.root} (${w.dirs} dirs)`);
}

REST API:

GET https://workspace.oblien.com/watchers
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/watchers" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "watchers": [
    {
      "id": "1",
      "root": "/app/src",
      "dirs": 42,
      "excludes": ["node_modules", ".git"]
    }
  ]
}

Get watcher

Get info for a specific watcher.

REST API:

GET https://workspace.oblien.com/watchers/1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl "https://workspace.oblien.com/watchers/1" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "id": "1",
  "root": "/app/src",
  "dirs": 42,
  "excludes": ["node_modules", ".git"]
}

Delete watcher

Stop a watcher and release its resources.

SDK:

await rt.watcher.delete('1');

REST API:

DELETE https://workspace.oblien.com/watchers/1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

cURL:

curl -X DELETE "https://workspace.oblien.com/watchers/1" \
  -H "Authorization: Bearer $GATEWAY_JWT"

Response

{
  "success": true
}

WebSocket events

Watcher events are delivered as JSON text frames on the "watcher" channel of the /ws WebSocket. All events include the watcher_id so you can distinguish events from multiple watchers on the same connection.

ready

Sent immediately after a watcher is created and has finished scanning the directory tree.

{
  "channel": "watcher",
  "type": "ready",
  "watcher_id": "1",
  "root": "/app/src",
  "dirs": 42
}

change

Sent when a file is created, modified, deleted, or renamed within the watched directory.

{
  "channel": "watcher",
  "type": "change",
  "watcher_id": "1",
  "path": "/app/src/index.ts",
  "op": "write"
}

Operations

op valueTrigger
createFile or directory created, or moved into the watched tree
writeFile content modified or saved
removeFile or directory deleted
renameFile or directory moved out of the watched tree

Events are debounced - rapid changes to the same path within 50ms are collapsed into a single event with the last operation.

overflow

Sent when the event queue overflows. This means some events may have been lost.

{
  "channel": "watcher",
  "type": "overflow",
  "watcher_id": "1",
  "message": "Event queue overflow, some events may have been lost"
}

If you receive an overflow event, re-sync the file tree by listing the directory to get the current state.


Listening for events

Connect to the same WebSocket used for terminal I/O. Text frames with "channel": "watcher" are file watcher events.

const socket = new WebSocket('wss://workspace.oblien.com/ws', {
  headers: { Authorization: `Bearer ${gatewayJwt}` },
});

socket.onmessage = (event) => {
  if (typeof event.data === 'string') {
    const msg = JSON.parse(event.data);

    if (msg.channel === 'watcher') {
      switch (msg.type) {
        case 'ready':
          console.log(`Watcher ${msg.watcher_id} ready: ${msg.root} (${msg.dirs} dirs)`);
          break;
        case 'change':
          console.log(`${msg.op}: ${msg.path}`);
          break;
        case 'overflow':
          console.log('Events may have been lost, re-syncing...');
          break;
      }
    }
  }
};

Limits

LimitValue
Max concurrent watchers5 per workspace
Debounce interval50ms per path
Auto-watch new subdirectoriesYes
ExcludesMerged with defaults, glob matching

Error responses

StatusMeaning
400Missing or invalid path
401Missing or invalid token
404Watcher not found
409Already at 5 watchers limit
500Failed to create file watcher

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

Ai Competitor Analyzer

提供AI驱动的竞争对手分析,支持批量自动处理,提升企业和专业团队分析效率与专业度。

Registry SourceRecently Updated
General

Ai Data Visualization

提供自动化AI分析与多格式批量处理,显著提升数据可视化效率,节省成本,适用企业和个人用户。

Registry SourceRecently Updated
General

Ai Cost Optimizer

提供基于预算和任务需求的AI模型成本优化方案,计算节省并指导OpenClaw配置与模型切换策略。

Registry SourceRecently Updated