# Connect with the SDK
URL: https://metorial.com/docs/build/sandboxes/connect-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.

<PageIntro>
  <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
  </Learn>

  <Reading title="Before you start">
    * A running machine ([quickstart](/docs/build/sandboxes/quickstart))
    * The [TypeScript or Python SDK](/docs/build/sandboxes#install-the-tools)
  </Reading>
</PageIntro>

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.
</Note>

## Run commands [#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.

<CodeBlockTabs defaultValue="TypeScript" groupId="language" mode="code">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="TypeScript">
    ```typescript
    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']);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    async with await metorial.sandbox.machines.shell(machine_id) as shell:
        result = await shell.command(
            "npm install && npm test",
            cwd="/app",
            env={"CI": "true"},
            timeout_ms=5 * 60_000,
        )

        print(result.exit_code, result.stdout, result.stderr)

        listing = await shell.command(["ls", "-la", "/app"])
    ```
  </CodeBlockTab>
</CodeBlockTabs>

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 [#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.

<CodeBlockTabs defaultValue="TypeScript" groupId="language" mode="code">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="TypeScript">
    ```typescript
    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' });
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import sys

    await shell.command(
        "python train.py",
        on_stdout=lambda data: sys.stdout.write(data.decode()),
        on_stderr=lambda data: sys.stderr.write(data.decode()),
    )

    sorted_result = await shell.command("sort", stdin="banana\napple\ncherry\n")
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Run background processes [#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.

<CodeBlockTabs defaultValue="TypeScript" groupId="language" mode="code">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="TypeScript">
    ```typescript
    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();
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    server = await shell.spawn("npm run dev", cwd="/app")
    pid = await server.started()

    server.on_stdout(lambda data: print(data.decode(), end=""))

    # later
    await server.signal("TERM")
    exit_info = await server.wait()
    ```
  </CodeBlockTab>
</CodeBlockTabs>

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 [#work-with-files]

<CodeBlockTabs defaultValue="TypeScript" groupId="language" mode="code">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="TypeScript">
    ```typescript
    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();
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import json

    async with await metorial.sandbox.machines.files(machine_id) as files:
        await files.write("/app/config.json", json.dumps({"debug": True}), parents=True)
        config = await files.read_text("/app/config.json")

        listing = await files.list("/app", recursive=True, max_depth=2)
        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")
    ```
  </CodeBlockTab>
</CodeBlockTabs>

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 [#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`.

<CodeBlockTabs defaultValue="TypeScript" groupId="language" mode="code">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="TypeScript">
    ```typescript
    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());
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    async with await metorial.sandbox.machines.connect(machine_id, 3000) as port:
        health = await port.get("/health")
        created = await port.post("/items", json={"name": "first"})

        print(port.url, created.json())
    ```
  </CodeBlockTab>
</CodeBlockTabs>

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](/docs/build/sandboxes/networking) explains tokens and public ports.

## Handle errors [#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`.

<CodeBlockTabs defaultValue="TypeScript" groupId="language" mode="code">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="TypeScript">
    ```typescript
    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;
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    from metorial import SandboxAccessError

    try:
        await files.read_text("/missing.txt")
    except SandboxAccessError as error:
        if error.code != "not_found":
            raise
    ```
  </CodeBlockTab>
</CodeBlockTabs>

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.