MoriMori
Mori

Terminal sessions

Named shells that stay alive across turns — the same session Mori drives is the one you can type into. Local, Docker, or SSH, with output kept bounded so a runaway command can't blow memory or context.

What a terminal session is

A terminal session is a long-lived shell that Mori keeps open across turns, keyed by a name you choose. cd into a directory, export an env var, start a build — all of it persists in that session until you close it. Reuse the same name and you pick up exactly where you left off; open a new name and you get a clean shell.

Five tools drive them: terminal_open starts (or reattaches to) a named session, terminal_run executes a command and waits for it to finish, terminal_read reads recent output without running anything, terminal_list enumerates every open session, and terminal_close reaps one. This is a real, stateful shell — not a one-shot run_terminal_command that forgets everything the moment it returns.

Named sessions shipped in 1.3.0 SOVEREIGN.

> terminal_open { name: "build", backend: "local" }
Opened terminal session "build" [local] at /Users/you/site. Run commands in it with terminal_run.

> terminal_run { name: "build", command: "cd apps/web && npm ci" }
[build] exit 0
…

> terminal_run { name: "build", command: "npm run build" }   # still in apps/web
[build] exit 0
…

Shared control — one shell, two drivers

The sessions Mori opens are the same sessions you see in the Mori Terminal panel. It is genuinely one shell with two drivers: the agent runs commands through the terminal_* tools, and you type into the same tab — same working directory, same environment, same running processes.

  • The terminal panel is tabbed, one tab per named session, with a backend badge (local / docker / ssh) and a live spinner while a command is running.
  • The panel polls the shared session list and the active tab's output a few times a second, so commands Mori runs — and their output — show up live as you watch.
  • terminal_read is the mirror image: it lets Mori catch up on output from a command you started by hand, so you can hand work back and forth in the same shell.

Everything the panel does routes through the exact same tool dispatch the agent uses, so there is no second, hidden code path — what you can do and what Mori can do are the same surface.

Three backends: local, Docker, SSH

You choose where a session's shell actually runs when you open it. A name is pinned to its backend for the life of the session — reopening the same name on a different backend is refused rather than silently swapping shells underneath you.

  • local (the default) — your own login shell: zsh or bash on macOS and Linux, PowerShell on Windows. It inherits the environment Mori is running in, so your PATH and tools are already there. Commands land on your real machine.
  • docker — a hardened, throwaway container. Good for running untrusted code, a clean build, or anything you'd rather not run against your home directory. Needs Docker installed.
  • ssh — a persistent shell on a remote host. Needs key-based auth to that host.

All three look identical to the model once open: the same commands, the same bounded output, the same exit codes.

The Docker backend is hardened by default

When you open a docker session, Mori starts a fresh container locked down before any command runs — you don't have to remember the flags. The container is started lazily on open and reaped with docker rm -f the moment you close the session, so nothing lingers.

  • `--cap-drop ALL` strips every Linux capability, and `--security-opt no-new-privileges` stops a process inside from escalating.
  • `--pids-limit 256` caps process count (a fork bomb hits a wall) and `--memory 2g` caps memory.
  • Your workspace is bind-mounted read-write at `/work`, and the shell starts there — so builds can touch your project files, but the container's own root filesystem is throwaway.
  • The default image is node:20-bookworm-slim; pass docker_image to pick another.

It degrades gracefully: if the docker binary isn't on your PATH or the container fails to start, terminal_open returns a clear error and tells you to install Docker or use the local backend — it never crashes the app.

docker run -d \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  --pids-limit 256 \
  --memory 2g \
  -v <your workspace>:/work -w /work \
  node:20-bookworm-slim sleep infinity

The SSH backend

An ssh session opens a persistent remote shell, multiplexed through an SSH ControlMaster socket so reconnecting to the same host is fast. It is built to fail fast rather than hang:

  • `BatchMode=yes` means Mori never blocks on an interactive password prompt. If key auth to the host isn't set up, the connection fails immediately with a clear error instead of freezing a task waiting for a password nobody will type.
  • A ServerAliveInterval keeps the connection healthy, and the shared control-master is torn down when you close the session so no dangling connection is left behind.
  • Like Docker, it degrades gracefully — a missing ssh binary or an unreachable host returns an error, never a crash.

Give it the destination as [user@]host when you open the session.

Output is always bounded

A shell that stays open forever could flood the model's context with the output of one chatty command. Every session is built to prevent that.

  • Sentinel completion. After each command Mori writes a unique marker line and reads output until that marker appears — so it knows exactly when a command finished and what exit code it returned, with no TTY and no guessing. The exit code comes back on every terminal_run.
  • Head + tail, middle elided. A single command's output is captured as a bounded head plus a rolling tail; if it runs long, the middle is dropped and replaced with a …N lines elided… marker. The result is flagged as bounded so you know it was trimmed.
  • Capped scrollback. Each session keeps a rolling window of its most recent output for terminal_read and the UI; older lines age out rather than accumulate forever.
  • Timeouts. A command waits up to 120 seconds by default (600 max). If it hasn't finished, terminal_run returns what it has so far and tells Mori the command may still be running — terminal_read catches up later, or terminal_close ends it. For servers that never exit, start_dev_server is the right tool instead.

Closing a session reaps its shell — and for Docker or SSH, the container or the connection too. Every open session is also reaped when you quit the app.

Commands still pass the safety gate

A persistent shell does not mean an ungoverned one. Every command sent through terminal_run passes the same command chokepoint as any other shell command Mori runs: an un-bypassable hardline floor refuses catastrophic patterns outright, and dangerous-but-legitimate commands are classified and gated according to your current autonomy level.

That means a terminal_run of rm -rf ~ is blocked no matter what, and a sudo or a git reset --hard prompts for approval under the default Guided level. How much runs without stopping to ask you is governed by the Autonomy ladder — see Autonomy for Guided / Trusted / Commander and the hardline floor.