InteractiveBox— the wrapper that handles raw mode and bidirectional forwarding for you — ships in the Python and Node SDKs. PTY itself works from every SDK: callexec(..., tty=True)to get anExecutionand forward stdin/stdout yourself (see “Lower-level PTY” at the end).
- You must run this in a real terminal. Interactive forwarding is enabled only when stdin is a TTY; in CI, pipes, Jupyter, or an IDE’s “Run” button, stdin is usually not a TTY, so keyboard forwarding is disabled automatically (see the
ttyparameter under Parameters and Returns).
Quick Example (happy path)
Python
Save the whole block below asinteractive.py, run python interactive.py in a terminal, and you drop straight into the box’s shell. Type exit or press Ctrl-D to quit.
Node
Save asinteractive.mjs (@boxlite-ai/boxlite is ESM-only) and run node interactive.mjs in a terminal.
Parameters and Returns
InteractiveBox(...) constructor parameters (Python)
Source: sdks/python/boxlite/interactivebox.py:43. InteractiveBox extends SimpleBox; keyword arguments not listed (such as working_dir, volumes, ports) pass through **kwargs to SimpleBox.
Leavingmemory_mib/cpusunset means “let the runtime decide”. Compute resources → Defaults is the single source of truth for those numbers.
Naming difference: inSimpleBox.exec(env=...)the env is a dict; butInteractiveBox’s constructorenvis fed directly to nativeBox.exec, so it must be a list[tuple] (see_start_interactive_shellininteractivebox.py).
InteractiveBoxOptions (Node)
Source: sdks/node/lib/interactivebox.ts. Extends SimpleBoxOptions; the constructor requires an options object.
Methods and return values
Context-manager type:InteractiveBoxis an async context manager (async with). The runtime handleBoxliteis a synchronouswith— don’t mix them up.
Troubleshooting
No interaction, keyboard does nothing (running in an IDE/CI/pipe)
Symptom: the script runs, but keystrokes get no response, or it blocks atwait().
Cause: interactive forwarding is enabled only when stdin is a TTY. tty=None (Python) / undefined (Node) auto-detects; under an IDE “Run” button, CI, python x.py < file, or | pipe, stdin is not a TTY, so forwarding is off.
Fix: run it in a real terminal; if you must force forwarding, pass tty=True (Python) / tty: true (Node) explicitly. Note: when you force tty=True in a non-TTY environment, Python calls termios.tcgetattr / tty.setraw on stdin in __aenter__ and raises termios.error. The exact errno varies by platform / stdin type (commonly (25, 'Inappropriate ioctl for device') under a pipe, and (19, 'Operation not supported by device') on macOS without a TTY). Whatever the errno, it is the same termios.error class; catch it with except termios.error (or a broad except Exception).
env passed with the wrong type
InteractiveBox’s env is passed to the lower-level Box.exec, so it must be list[tuple[str, str]]:
InteractiveBoxOptions.env uses a Record<string, string> object.)
Startup failure / no virtualization
BoxLite requires hardware virtualization: Linux needs KVM (/dev/kvm available, user in the kvm group); macOS uses Apple’s Hypervisor.framework (no KVM needed); WSL2 needs KVM enabled. Without virtualization, the box fails to start — the exception can be caught with try/except (Python) / try/catch (Node), and the process does not crash. macOS Intel is not supported.
Image pull failure raises RuntimeError, not BoxliteError
When an image pull fails due to a network hiccup, Python raises the built-in RuntimeError (Node a bare Error), not a BoxliteError subclass. Catch it with a broad except Exception / catch (err) and retry; don’t catch only BoxliteError.
Shell does not exist
Ifshell points to a path that the image does not have (such as /bin/bash on plain alpine), it raises a RuntimeError when entering async with (the __aenter__ that launches the PTY shell), with a message such as:
async with in try/except RuntimeError to catch it. Make sure the target image has that shell (alpine has /bin/sh by default, but not bash).
Lower-level PTY (C / Go / Rust, no high-level wrapper)
From C / Go / Rust, drive the PTY throughexec directly and forward I/O yourself:
- Rust:
BoxCommand::new(shell).tty(true), thenbox.exec(cmd)to get anExecution, and forward fromexecution.stdin()/stdout()/stderr()(all&mut);execution.resize_tty(rows, cols)resizes the window. The execution method isexec(there is nolitebox.run). - C:
BoxliteCommand.tty = 1,boxlite_box_exec(handle, &cmd, &execution, &error)to get the execution; output via theboxlite_execution_on_stdout/on_stderrcallbacks, stdin viaboxlite_execution_stdin_write, window size viaboxlite_execution_tty_resize. This is a post-and-drain model — you must callboxlite_runtime_drainin a loop to drive the callbacks. - Go:
box.StartExecution(...)withExecutionOptions{TTY: true, OnStdout: ..., OnStderr: ...}, writing stdin yourself.

