> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Lightweight Terminal

> A terminal for agents inside your FastAPI or Express server, with no sandbox per agent.

An agent needs somewhere to run commands: Bash, Python, and CLIs such as
`git`. The usual answer is a sandbox, a container or microVM for each agent.
A sandbox reserves memory for every agent, takes time to start and needs a
service to manage it.

Most of what an agent runs does not need a machine. It reads files, calls
services and pipes text from one command to the next. A lightweight terminal
runs that work inside the server you already have. Each terminal is a Mirage
session with its own working directory, environment and history, and
[Monty](https://docs.mirage.strukto.ai/home/setup/runtime/monty.md) runs Python in-process. One server process
holds many terminals on its event loop.

| | Sandbox per agent | Lightweight terminal |
| - | - | - |
| Commands run in | a container or microVM | your server process |
| Memory per agent | 128 MB to 1 GB reserved ([1](https://modal.com/docs/guide/sandbox-resources), [2](https://developers.cloudflare.com/containers/platform/pricing/), [3](https://docs.e2b.dev/pricing), [4](https://www.daytona.io/docs/en/sandboxes/)) | tens of kilobytes when idle, plus the files it holds |
| Start | boots a container or microVM | adds a session to a running process |
| You operate | a sandbox service | nothing new |
| Runs | any program | Bash, Python on Monty, CLIs, MCP servers |

A command that needs more, such as `numpy` or a GPU, can still go to a machine:
[Runtime Routing](https://docs.mirage.strukto.ai/home/examples/runtime-routing.md) sends only those commands
there.

## What an agent can run

| | Example | Where it runs |
| - | - | - |
| Bash | `grep -c ERROR app.log`, pipes, redirects, variables, `curl`, `jq` | [Mirage's shell](https://docs.mirage.strukto.ai/home/core/shell.md) |
| Python | `python3 report.py` | [Monty](https://docs.mirage.strukto.ai/home/setup/runtime/monty.md), a subset of Python with no network or host access |
| CLIs | `git log --oneline -5` | a [CLI](https://docs.mirage.strukto.ai/home/setup/clis.md) the server registers, such as `git`, `gh` or `slack` |
| MCP servers | `deepwiki ask-wiki-question '{...}'` | any [MCP server](https://docs.mirage.strukto.ai/python/cli/mcp.md), installed as a CLI: each of its tools is a subcommand |

**The files they work on live in a virtual filesystem (VFS).** Each source of
files, such as memory, an S3 bucket or Google Drive, is a
[VFS](https://docs.mirage.strukto.ai/home/setup/vfs.md) mounted under its own path, and a server can mount
several side by side:

| Mount | For | Example |
| - | - | - |
| Memory | scratch files, gone when the server stops | `cat /work/notes.txt` |
| S3, Google Drive, a database | files that must outlast the server | `cp /work/report.csv /s3/reports/` |

This page mounts memory at `/work`, which needs no account.

## Installation

> [tab] FastAPI
>
> ```bash theme={null}
> pip install 'mirage-ai[monty]' fastapi uvicorn
> ```

> [tab] Express
>
> ```bash theme={null}
> npm install @struktoai/mirage-node @pydantic/monty express
> ```

## Server

One endpoint creates a terminal, another runs a command in it.

> [tab] FastAPI
>
> ```python app.py theme={null}
> from contextlib import asynccontextmanager
>
> from fastapi import FastAPI
> from pydantic import BaseModel
>
> from mirage import RAMVFS, MountMode, Workspace
> from mirage.runtime.python.monty import MontyRuntime
>
> ws = Workspace(
>     {"/work": (RAMVFS(), MountMode.EXEC)},
>     machines=[MontyRuntime(captures=["python", "python3"])],
> )
>
>
> @asynccontextmanager
> async def lifespan(app: FastAPI):
>     yield
>     await ws.close()
>
>
> app = FastAPI(lifespan=lifespan)
>
>
> class Run(BaseModel):
>     command: str
>
>
> @app.post("/sessions/{session_id}")
> async def create(session_id: str) -> None:
>     ws.create_session(session_id)
>
>
> @app.post("/sessions/{session_id}/run")
> async def run(session_id: str, body: Run) -> dict:
>     r = await ws.shell(body.command, session_id=session_id)
>     return {"stdout": await r.stdout_str(), "stderr": await r.stderr_str(), "exit_code": r.exit_code}
> ```
>
> ```bash theme={null}
> uvicorn app:app --port 3000
> ```

> [tab] Express
>
> ```typescript app.mjs theme={null}
> import express from 'express'
> import { MontyRuntime, MountMode, RAMVFS, Workspace } from '@struktoai/mirage-node'
>
> const ws = new Workspace(
>   { '/work': new RAMVFS() },
>   { mode: MountMode.EXEC, machines: [new MontyRuntime({ captures: ['python', 'python3'] })] },
> )
> const app = express()
> app.use(express.json())
>
> app.post('/sessions/:id', (req, res) => {
>   ws.createSession(req.params.id)
>   res.sendStatus(201)
> })
>
> app.post('/sessions/:id/run', async (req, res) => {
>   const r = await ws.shell(req.body.command, { sessionId: req.params.id })
>   res.json({ stdout: r.stdoutText, stderr: r.stderrText, exitCode: r.exitCode })
> })
>
> app.listen(3000, '127.0.0.1')
> ```
>
> ```bash theme={null}
> node app.mjs
> ```

## Usage

```bash theme={null}
curl -X POST localhost:3000/sessions/a
curl -X POST localhost:3000/sessions/a/run -H 'content-type: application/json' \
  -d '{"command": "cd /work && echo \"print(sum(range(10)))\" > s.py && python3 s.py"}'
```

The run prints `45`. A later call to session `a` starts in `/work`, where the
`cd` left it.

The examples below send several lines in one request. This shell function
reads the lines from standard input, sends them to session `a` as the
`command`, and prints what they print, errors included. It needs
[`jq`](https://jqlang.org):

```bash theme={null}
run() {
  jq -Rs '{command: .}' |
    curl -s localhost:3000/sessions/a/run -H 'content-type: application/json' -d @- |
    jq -j '.stdout, .stderr'
}

run <<'SH'
pwd
python3 s.py
SH
```

```text theme={null}
/work
45
```

The lines between `SH` and `SH` run in the terminal on the server, not in
the shell that calls `run`.

## Terminals per server

A sandbox is a machine, so it reserves memory for its agent whether the agent
is working or waiting. A terminal is not a machine. It is a record in your
server's memory: a working directory, variables and a history.

Agents

Busy

Each holds

A sandbox for each agent

machine,  reserved, busy or idle

Terminals in one server

1 server,  idle and  busy,

Each square is 128 MB, the least that the sandbox services linked above
reserve for one agent. The server is drawn at the same scale, four squares
for 512 MB, and then enlarged to show its terminals and its memory. Mirage
and its runtime take about 100 MB of it. An idle terminal adds about 20 KB,
plus any files it keeps in a memory mount. A busy terminal adds what its
command holds: files, a cache, the data in its pipes. Move the slider to add agents, and choose how
many are busy and how much each holds.

What a terminal costs depends on what it is doing:

| A terminal that is | Holds | Is limited by |
| - | - | - |
| idle, between an agent's turns | its variables and history | memory, tens of kilobytes each |
| waiting on a service, as in `curl` or `gh pr list` | the reply it is reading | memory, then CPU |
| working on data, as in `sort` or `grep` | the data in its pipes | the server's CPU, then memory |

* **Idle terminals are nearly free.** An idle terminal has no process or
  thread of its own, only a record of about 20 KB on Python and on
  TypeScript. One server holds thousands of them, so an agent can keep its
  terminal between turns.
* **Terminals waiting on a service run at once.** A command that waits for a
  reply uses no CPU while it waits, and the server runs other terminals'
  commands in the meantime. See [Async](#async).
* **Terminals working on data share the CPU.** Commands such as `sort` and
  `grep` take turns on it, so each takes longer as more run together. A
  command that needs a core to itself belongs on a machine: see
  [Runtime Routing](https://docs.mirage.strukto.ai/home/examples/runtime-routing.md).

Press each button to see four terminals run a command at the same moment:

Size the server as you would any web service: by how many terminals are busy
at the same moment, not by how many exist.

## Async

`ws.shell` is a coroutine in Python and a promise in TypeScript, and it never
blocks the event loop. The handlers above are `async`, so while one terminal
waits on a file read, a runtime or a slow command, the server keeps serving
every other terminal, with no thread or process per terminal.

> [tab] Python
>
> ```python theme={null}
> import asyncio
>
> for sid in ["a", "b", "c"]:
>     ws.create_session(sid)
> await asyncio.gather(*(ws.shell("sleep 1", session_id=sid) for sid in ["a", "b", "c"]))
> ```

> [tab] TypeScript
>
> ```typescript theme={null}
> for (const sid of ['a', 'b', 'c']) ws.createSession(sid)
> await Promise.all(['a', 'b', 'c'].map((sid) => ws.shell('sleep 1', { sessionId: sid })))
> ```

The three commands finish together in about a second; 20 sessions take the
same. Within one session, commands run one after another in order, as in a real
terminal, so three `sleep 1` commands on session `a` take three seconds.

## Sharing and security

* By default, terminals share the workspace's mounts, so a file one writes
  under `/work` is visible to the others.
* A [session profile](https://docs.mirage.strukto.ai/home/configuration/session.md) and a
  [policy](https://docs.mirage.strukto.ai/home/policy/overview.md) decide which commands a terminal may run
  and which files it sees, so each agent can have a folder the others cannot
  see. [Policy & Security](https://docs.mirage.strukto.ai/home/examples/policy-security.md) walks through one.
  To keep everything apart, give an agent its own workspace.

## Use cases

Each page adds a few lines to this server and shows what an agent does with
the terminal.

> [card] [Code Review](https://docs.mirage.strukto.ai/home/examples/lightweight-terminal/code-review.md)
>
> Review a pull request and a dependency bump with `gh`, `git`, a GitHub
> mount and an MCP server.

> [card] [Co-Work](https://docs.mirage.strukto.ai/home/examples/lightweight-terminal/co-work.md)
>
> Schedule candidate interviews from an inbox, a calendar and Slack.

> [card] [Release Notes](https://docs.mirage.strukto.ai/home/examples/lightweight-terminal/release-notes.md)
>
> Turn a week of merged work into release notes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.