Run commands
Run a shell command inside a sandbox and read its result. You can also stream its output as it arrives, keep it running in the background, feed it stdin, and handle a non-zero exit.
Commands run over the sandbox's own connection, not the public REST API, so
there is no REST endpoint and no curl example here. Use the TypeScript or
Python SDK. Every example reads IMPELLO_API_KEY from the environment; get a
key at dashboard.impello.ai/keys.
Run a command
sandbox.commands.run() starts the command, waits for it to exit and returns
its stdout, stderr and exit code. Every command runs through /bin/bash -l -c,
so pipes, globs, && and shell variables work as they do in a terminal.
import { Sandbox } from '@impello/sdk'
const sandbox = await Sandbox.create('base')
const result = await sandbox.commands.run('echo hello && uname -a')
console.log(result.stdout)
console.log(result.exitCode) // 0
await sandbox.kill()The result has four fields: stdout, stderr, exitCode (exit_code in
Python) and an optional error message. Both streams are decoded as UTF-8 and
returned as strings.
Options
| TypeScript | Python | Default | What it does |
|---|---|---|---|
cwd | cwd | the user's home | Working directory for the command. |
user | user | the template's user | Linux user to run as — any user that exists in the template. base has user, with home /home/user, and root. |
envs | envs | {} | Environment variables for this command. They override the ones given to the sandbox. |
onStdout, onStderr | on_stdout, on_stderr | none | Callbacks that receive output chunks as they arrive. See Stream output. |
stdin | stdin | false | Keep stdin open so you can write to it. See Send input. |
background | background | false | Return a handle at once instead of waiting. See Background commands. |
timeoutMs | timeout | 60000 ms / 60 s | How long the SDK waits for the command. See Long-running commands. |
Stream output
Pass onStdout and onStderr to receive output while the command is still
running. The callbacks get each chunk as a string; the full output is still
collected in the returned result.
In the Python sync SDK a background handle is also iterable. Each item is a
(stdout, stderr, pty) tuple with one field set and the other two None:
handle = sandbox.commands.run("ls -la /", background=True)
for stdout, stderr, _ in handle:
if stdout is not None:
print(stdout, end="")
In the sync SDK the on_stdout and on_stderr arguments to run() apply to
foreground commands only. For a background command, iterate the handle or pass
them to handle.wait(on_stdout=..., on_stderr=...).
With AsyncSandbox, pass on_stdout and on_stderr to run() for both
foreground and background commands; AsyncCommandHandle.wait() takes no
callbacks.
Background commands
Set background: true and run() returns a CommandHandle immediately. Use
the handle to wait for the exit, read the output so far, or kill the process.
import { Sandbox } from '@impello/sdk'
const sandbox = await Sandbox.create('base')
const handle = await sandbox.commands.run('sleep 2 && echo done', {
background: true,
})
console.log(handle.pid)
console.log(handle.exitCode) // undefined while running
const result = await handle.wait()
console.log(result.stdout) // "done\n"
await sandbox.kill()In TypeScript the handle exposes pid, stdout, stderr, exitCode and
error; exitCode is undefined until the command exits. The Python sync
handle exposes pid and the methods below; the async handle adds stdout,
stderr, exit_code and error, with exit_code None while running.
handle.disconnect() stops the SDK listening without killing the command. To
pick it up again, call sandbox.commands.connect(pid) from any client
connected to the same sandbox; the new handle's wait() returns the result
when the command exits.
const handle = await sandbox.commands.run('sleep 30 && echo done', {
background: true,
})
await handle.disconnect()
const again = await sandbox.commands.connect(handle.pid, {
onStdout: (data) => console.log(data),
})
await again.wait()Long-running commands
The SDK stays attached to a command for 60 seconds by default. When the limit
passes it throws TimeoutError (TimeoutException in Python) from run() or
wait(). That stops the SDK waiting; find the process with
sandbox.commands.list(), then re-attach with sandbox.commands.connect(pid)
to see whether it is still running, or stop it with
sandbox.commands.kill(pid). Raise the limit with timeoutMs (timeout in
Python, in seconds) to cover the longest run you expect. background: true
does not change this: the handle is bounded by the same value, so pass the
option there too.
const result = await sandbox.commands.run('npm install', {
timeoutMs: 10 * 60 * 1000,
})connect() has the same 60 second default and takes the same option.
The command timeout is separate from the sandbox timeout. A sandbox that
reaches its own limit is killed with everything running inside it, unless its
lifecycle pauses it instead. A wait() still pending on one of its commands
then throws TimeoutError. See Timeouts.
Send input
Start the command with stdin: true, write to it with sendStdin(), and call
closeStdin() to send EOF when you are done. Both methods also exist on
sandbox.commands taking a pid, for a command you did not start from this
handle.
import { Sandbox } from '@impello/sdk'
const sandbox = await Sandbox.create('base')
const handle = await sandbox.commands.run('cat', {
background: true,
stdin: true,
})
await handle.sendStdin('hello from stdin\n')
await handle.closeStdin()
const result = await handle.wait()
console.log(result.stdout) // "hello from stdin\n"
await sandbox.kill()The data may be a string or raw bytes — a Uint8Array in TypeScript, bytes
in Python. A handle for a pseudo-terminal supports neither method; use the
Interactive terminal module for that instead.
List and kill processes
sandbox.commands.list() returns every running command and PTY session as a
ProcessInfo with pid, cmd, args, envs, and optional cwd and tag.
Because every command runs through bash, cmd is /bin/bash and your command
line is the last element of args.
sandbox.commands.kill(pid) sends SIGKILL. It returns true when the signal
was sent and false when no process had that pid. handle.kill() does the
same for the handle's own process.
import { Sandbox } from '@impello/sdk'
const sandbox = await Sandbox.create('base')
await sandbox.commands.run('sleep 600', { background: true })
for (const proc of await sandbox.commands.list()) {
console.log(proc.pid, proc.args.at(-1))
if (proc.args.at(-1) === 'sleep 600') {
console.log(await sandbox.commands.kill(proc.pid)) // true
}
}
await sandbox.kill()Non-zero exit codes
A command that exits with anything other than 0 makes run() and wait()
throw CommandExitError (CommandExitException in Python). The error carries
the same exitCode, stdout and stderr fields as a result, so you can read
what went wrong.
import { CommandExitError, Sandbox } from '@impello/sdk'
const sandbox = await Sandbox.create('base')
try {
await sandbox.commands.run('ls /does-not-exist')
} catch (err) {
if (err instanceof CommandExitError) {
console.log(err.exitCode) // 2
console.log(err.stderr)
} else {
throw err
}
}
await sandbox.kill()In Python, str(e) reads Command exited with code 2 and error: followed by
stderr. The other classes a command can raise are listed on the
Errors page.
Next: Interactive terminal.