MetorialDocs

Connect with the SDK

Run commands, stream output, manage files, and call ports from your code.

The SDK lets your app or agent drive a machine directly: run commands and read their output, keep long-running processes going, read and write files, and send HTTP requests to servers inside the machine. The SDK handles access tokens for you.

What you'll learn

  • How to run commands and stream their output
  • How to run background processes
  • How to read and write files
  • How to call a server running in the machine

Before you start

Each helper opens one connection that you can reuse for many calls. Close it when you're done. In Python, the helpers are async and work as async context managers.

Note

Connections need a running machine. Call machines.start() first if the machine is paused or hibernated.

Run commands

command() runs a command to completion and returns its output. A string runs through /bin/sh -c, so pipes and && work. Pass an array to run a program directly without a shell.

let shell = await metorial.sandbox.machines.shell(machineId);

let result = await shell.command('npm install && npm test', {
  cwd: '/app',
  env: { CI: 'true' },
  timeoutMs: 5 * 60_000
});

console.log(result.exitCode, result.stdout, result.stderr);

let list = await shell.command(['ls', '-la', '/app']);

Commands that exit with a non-zero code return normally, so check the exit code yourself. When the timeout hits, the command is stopped and timedOut is true.

Stream output and send input

Pass output handlers to see output as it arrives, and stdin to feed the command input. Handlers receive raw bytes.

let decoder = new TextDecoder();

await shell.command('python train.py', {
  onStdout: data => process.stdout.write(decoder.decode(data)),
  onStderr: data => process.stderr.write(decoder.decode(data))
});

let sorted = await shell.command('sort', { stdin: 'banana\napple\ncherry\n' });

Run background processes

spawn() starts a process and returns right away. Use it for dev servers, watchers, or interactive programs you write to over time. One connection can run many processes at once.

let server = await shell.spawn('npm run dev', { cwd: '/app' });
let pid = await server.started();

server.onStdout(data => console.log(new TextDecoder().decode(data)));

// later
await server.signal('TERM');
let exit = await server.wait();

To write to a process, spawn it with stdin: true, then call write() and finish with closeStdin() (close_stdin() in Python). kill() sends KILL.

Work with files

let files = await metorial.sandbox.machines.files(machineId);

await files.write('/app/config.json', JSON.stringify({ debug: true }), { parents: true });
let config = await files.readText('/app/config.json');

let { entries, truncated } = await files.list('/app', { recursive: true, maxDepth: 2 });
let info = await files.stat('/app/config.json');

if (await files.exists('/app/tmp')) {
  await files.delete('/app/tmp', { recursive: true });
}

await files.move('/app/out.log', '/app/logs/out.log');
files.close();

A few details:

  • write() accepts text or bytes and replaces existing files unless you pass overwrite: false. Set mode (such as '0755') for executables.
  • read() returns bytes and takes an offset and length to read part of a large file.
  • stat() returns the entry's type (file, directory, symlink, or other), size, mode, and modification time.
  • list() reports truncated when it hits its limit.

Call a server in the machine

connect() gives you an HTTP client for one port. It adds the access token to every request and refreshes it when it expires. Your server must listen on 0.0.0.0.

let port = await metorial.sandbox.machines.connect(machineId, 3000);

let health = await port.get('/health');
let created = await port.post('/items', { name: 'first' });

console.log(port.url, await created.json());

To call the port from somewhere else, such as a browser, read the URL from port.url and a token from getToken() (get_token() in Python), and send the token in the Metorial-Sandbox-Token header. Networking explains tokens and public ports.

Handle errors

Connection and protocol problems throw SandboxAccessError. Failures tied to a single command throw SandboxCommandError, a subclass. Both carry a code and a retryable flag. File operations add the path, with codes such as not_found, already_exists, and not_empty.

import { SandboxAccessError } from 'metorial';

try {
  await files.readText('/missing.txt');
} catch (error) {
  if (error instanceof SandboxAccessError && error.code == 'not_found') {
    // create it
  } else throw error;
}

Idle connections close after the machine's connection idle timeout. Pass onNotice (on_notice in Python) to shell() or files() to get a warning before that happens.