Skip to content
Raster
Esc
navigateopen⌘Jpreview
On this page

The terminal

One-shot commands as argv, interactive ptys, and why a non-zero exit code is a success.

Two ways to run something: a one-shot exec over HTTP, and a pty you attach to over the gateway. Neither is gated on the input lease — a shell is a session of its own, and two people holding separate terminals is normal. The lease governs the desktop, where there is one mouse and one keyboard.

exec

const result = await machine.terminal.exec(["git", "clone", url]);
result.stdout;
result.stderr;
result.exit_code;

command is argv, not a string. There is no shell and no tokenizing, so an argument containing a space, a quote or a semicolon is one argument. That is the whole defence against command injection, and it only works because the API refuses to split a string for you.

Field Meaning
command argv, required
user agent (default) or root
cwd working directory
stdin fed to the process
timeout_ms how long it may run

A non-zero exit code is a successful call

Only a failure to run the command at all is an error. A command that runs and returns 1 is a 200 with exit_code: 1 — the code is data, not a fault.

That distinction is worth holding onto in an agent loop. A model that sees an exception on every failed grep learns to avoid grep; a model that sees an exit code learns to read it.

Asking for a shell by name

exec deliberately does not tokenize, so pipes, globs and redirects need a shell, spelled out:

await machine.terminal.execShell("cd /tmp/site && python3 -m http.server 3000 &");
// which is exactly:
await machine.terminal.exec(["/bin/sh", "-lc", "cd /tmp/site && python3 -m http.server 3000 &"]);

The point is that a reader can see you asked for it. Guessing where a command ends is how injection happens.

Interactive terminals

Call What it does
POST /machines/{id}/terminals open a pty
GET /machines/{id}/terminals list open ptys
DELETE /machines/{id}/terminals/{term_id} kill one
const term = await machine.terminal.connect(); // opens one if you pass no id
term.write("ls -la\n");
term.resize(120, 32);
for await (const bytes of term) process.stdout.write(bytes);
term.close(); // closes the pty inside the machine

open takes command, cwd, user, cols and rows. Bytes go both ways unmodified over the terminal channel; a terminal already holding a client refuses a second one at the handshake with close code 4409.

The CLI’s raster shell is this, with your local terminal put into raw mode so ^C reaches the remote process rather than being handled locally.

Was this page helpful?