Pseudo Terminal (PTY)
Interact with pseudo terminal sessions in your sandboxes.
Daytona provides pseudo terminal (PTY) capabilities through the process module in sandboxes. A PTY is a process started with the PTY kind: it runs under a virtual terminal so programs behave as if connected to a real terminal, with terminal size, line discipline, and interactive prompts.
PTY processes are exposed through a sandbox's PTY namespace and share the handle API of other processes, described in process execution.
Use cases
PTY sessions are used when a program expects a real terminal.
-
Interactive development. REPLs, debuggers, and editors that read keystrokes and redraw the screen run in a PTY session. The virtual terminal gives them a terminal size and line discipline, as a real terminal does. The terminal starts at 80 columns by 24 rows unless the start request sets a size.
-
Terminals in web applications. A browser terminal connects to a PTY session, sends keystrokes, and resizes the session with the window. On each resize, the program in the terminal receives a window size change signal and can redraw. Browsers authenticate the WebSocket connection with the daytona_token query parameter.
-
Builds and tests. A PTY session runs compilers, test runners, and deploy tools that show progress or colors only when attached to a terminal. Waiting for the session returns its exit code, and the CLI exits with the same code. If the wait time elapses first, the result reports a timeout with no exit code, and the session keeps running.
-
Reconnects. A client that reconnects replays the output from a cursor and sees what it missed. Every output frame carries a cursor, and a read with a cursor returns only the frames after it. Retention can evict old output, and a cursor into evicted output fails.
-
Terminal agents. An agent works in one interactive shell that keeps its working directory and variables between commands. It reads the output as a terminal shows it, including escape sequences. When a session timeout elapses, the session is killed and reports that it timed out.
Start PTY session
Start a PTY session. Without a command, the session runs an interactive shell: the login shell of the user it runs as, or the first of bash, zsh, and sh found in the image. With a command, the session runs that command in the terminal instead. The terminal starts at 80 columns by 24 rows unless you set a size, and you can resize it later.
The SDKs and the CLI keep an exited session's record and output until deleted.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
# Without a command the PTY runs an interactive shell; the terminal defaults to 80x24
handle = sandbox.pty.start()
print(handle.id)from daytona import Daytona
from daytona.v2 import TerminalSize
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start("python3 -i", terminal=TerminalSize(cols=100, rows=40))| Parameter | Type | Description |
|---|---|---|
command | str | Sets the command to run in the PTY. |
terminal | TerminalSize | Sets the terminal size. cols and rows are each from 1 to 65535. The default is 80 columns by 24 rows. |
cwd | str | Sets the working directory of the command. |
env | Mapping[str, str] | Sets environment variables for the command. |
name | str | Sets the session name. See name PTY session. |
keep_logs | KeepLogs | Sets the log retention policy. With "on_exit_ttl", the record and output are removed 10 minutes after exit; with "none", the output is discarded at exit. See log retention. The default is "until_cleanup". |
Name PTY session
Name a PTY session. A name aliases the session ID, so another client can reach the session without knowing its ID. The name belongs to the session only while it runs: when the session exits, the name is released for reuse, and the session stays reachable by its ID only.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start(name="dev-shell")
# Names are unique among live processes and resolve wherever an ID is accepted
info = sandbox.process.get("dev-shell")Configure shell, user, and timeout
Configure the shell, the user, and the timeout of a PTY session. A shell that cannot be found or a user that does not exist fails the start. When the timeout elapses, the session is killed and reports that it timed out.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start(
shell="/bin/bash", # default: the user's login shell, then bash, zsh, sh
user="root", # run as another user
execution_timeout=600, # SIGKILL after 10 minutes; wait() reports reason "timed_out"
)| Parameter | Type | Description |
|---|---|---|
shell | str | Sets the shell, as a path or a name looked up on PATH. |
user | str | Sets the user the session runs as. Without shell, the session runs through the user's login shell, so a user whose shell is nologin cannot run commands. The user must exist in /etc/passwd of the sandbox. |
execution_timeout | float | Sets how many seconds the session can run before Daytona kills it. |
Connect to PTY session
Connect to an existing PTY session by its ID. Connecting creates a handle without contacting the sandbox; the handle sends input to the session, reads its output, resizes it, and stops it. A live connection streams output produced after it opens; to see earlier output, replay it from a cursor.
handle = sandbox.pty.connect(process_id)
handle.write_stdin('echo "Reconnected"\n')
# The Python SDK has no WebSocket attach. Use write_stdin() and
# stream_logs() instead.List PTY sessions
List PTY sessions, including retained exited ones; check each session's state.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
processes = sandbox.pty.list()
for info in processes.items:
print(f"PTY: {info.id} ({info.state})")
if info.terminal:
print(f" Size: {info.terminal.cols}x{info.terminal.rows}")
print(f" Working directory: {info.cwd}")Get PTY session info
info = handle.get()
print(f"PTY: {info.id}")
print(f"State: {info.state}")
print(f"PID: {info.pid}")
if info.terminal:
print(f"Size: {info.terminal.cols}x{info.terminal.rows}")
print(f"Working directory: {info.cwd}")
if info.state == "terminal":
print(f"Exit code: {info.exit_code}")Kill PTY session
Stop a PTY session and remove it. Stopping sends a signal to the session's processes; removing deletes the session record and its output. An interactive shell ignores the default termination signal, so a session started without a command keeps running unless the signal escalates to a kill. Removing a session that is still running kills it first.
# Sends SIGTERM, then SIGKILL after 5 seconds if the session is still running
handle.kill(escalate_after=5)
# Removes the process record and logs
handle.delete()| Parameter | Type | Description |
|---|---|---|
signal | SignalName | Sets the signal that kill() sends: TERM, KILL, INT, HUP, QUIT, USR1, or USR2. The default is "TERM". |
escalate_after | float | Sends escalate_to if the session is still running after this delay. The delay is in seconds, at most 600. |
escalate_to | SignalName | Sets the signal sent after escalate_after. The default is "KILL". |
Delete a PTY session
Delete a PTY session in one step: a running session gets a termination signal, then the session record and its output are removed. Deleting a non-PTY process fails and leaves it running.
sandbox.pty.delete(handle.id)Resize PTY session
Resize the terminal of a running PTY session. The program in the terminal receives a window size change signal and can redraw at the new size. The session record reports the new size.
handle.resize(150, 40)Send input
Send input to a PTY session. Input reaches the terminal as if typed, so end each command with a newline. Control keys are bytes: send Ctrl+C as 0x03 and Ctrl+D as 0x04. A terminal has no separate end of input; to end input, send Ctrl+D.
handle.write_stdin("ls -la\n")| Parameter | Type | Description |
|---|---|---|
data | Union[str, bytes] | Sets the input to send, as str or bytes. Bytes are sent base64 encoded. This parameter is required. |
Read output
Read one page of PTY output. A page holds up to 1,000 frames by default. To read past the first page, continue from a cursor as shown in replay output from a cursor.
page = handle.logs()
for frame in page.items:
print(frame.data, end="")| Parameter | Type | Description |
|---|---|---|
limit | int | Sets the page size. The daemon clamps the page size to 1 to 5000. The default is 1000. |
encoding | LogEncoding | Sets the output encoding. In text encoding, bytes that are not valid UTF-8 are replaced. Pass "base64" to get each frame's data as a standard base64 string of the raw bytes. The default is "text". |
Replay output from a cursor
Replay PTY output from a cursor. Every output frame carries a cursor, and a read with a cursor returns the frames after it, so a client that saved the last cursor it processed can resume without gaps or duplicates. Retention can evict old output; a cursor that points into evicted output fails, and the error carries the oldest cursor still available. See log retention.
# Every frame carries a cursor; reading resumes strictly after it. An empty page means the read has caught up
cursor = last_cursor
while True:
page = handle.logs(cursor=cursor)
if not page.items:
break
for frame in page.items:
print(frame.data, end="")
cursor = frame.cursorWait for completion
Wait for a PTY session to exit. The result holds the exit code, the ending signal if any, and a reason: exited, killed by a signal, timed out, failed to start, or lost. If the wait time elapses first, the result reports a timeout with no exit code, and the session keeps running.
result = handle.wait(timeout=30)
print(result.reason, result.exit_code)| Parameter | Type | Description |
|---|---|---|
timeout | float | Sets how long to wait, in seconds. Omit it or pass 0 to wait until the session exits. |
Interactive commands
Run several commands in one interactive shell. The shell keeps its state between commands, such as the working directory and variables. Read output while you send input; the output stream ends after the shell exits and all of its output is delivered.
import threading
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start()
# Print output in the background
def pump():
for event in handle.stream_logs():
if event.type == "log":
print(event.frame.data, end="")
reader = threading.Thread(target=pump, daemon=True)
reader.start()
for command in [
"pwd",
"ls -la",
"echo 'Hello from PTY'",
"python3 -c 'print(2 + 2)'",
"exit",
]:
handle.write_stdin(command + "\n")
result = handle.wait()
reader.join(timeout=5)
print(f"\nShell exited with code: {result.exit_code}")
handle.delete()Long-running processes
Interrupt a running command from the terminal. Ctrl+C sent as input goes through the terminal, which interrupts the foreground command; the shell itself keeps running. Signaling the session instead targets the whole session, including the shell. See kill PTY session.
import time
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start()
handle.write_stdin("for i in $(seq 1 100); do echo \"Line $i\"; sleep 1; done\n")
time.sleep(5)
handle.write_stdin("\x03") # Ctrl+C
handle.write_stdin("exit\n")
result = handle.wait()
for frame in handle.logs().items:
print(frame.data, end="")
print(f"\nExit code: {result.exit_code}")
handle.delete()Resource management
Remove a PTY session when you no longer need it. The SDKs keep an exited session's record and output until it is deleted, so delete it in a final block to remove it even when a step fails.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create()
handle = sandbox.pty.start()
try:
handle.write_stdin("echo hello\n")
handle.write_stdin("exit\n")
handle.wait(timeout=10)
finally:
handle.delete()Permissions
A PTY session runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an organization role or an API key.
A sandbox access token with the toolbox:exec operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access.
| Permission | Grants |
|---|---|
write:sandboxes | Start, connect to, resize, and kill PTY sessions |