Sign Up

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))
ParameterTypeDescription
commandstrSets the command to run in the PTY.
terminalTerminalSizeSets the terminal size. cols and rows are each from 1 to 65535. The default is 80 columns by 24 rows.
cwdstrSets the working directory of the command.
envMapping[str, str]Sets environment variables for the command.
namestrSets the session name. See name PTY session.
keep_logsKeepLogsSets 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"
)
ParameterTypeDescription
shellstrSets the shell, as a path or a name looked up on PATH.
userstrSets 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_timeoutfloatSets 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()
ParameterTypeDescription
signalSignalNameSets the signal that kill() sends: TERM, KILL, INT, HUP, QUIT, USR1, or USR2. The default is "TERM".
escalate_afterfloatSends escalate_to if the session is still running after this delay. The delay is in seconds, at most 600.
escalate_toSignalNameSets 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")
ParameterTypeDescription
dataUnion[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="")
ParameterTypeDescription
limitintSets the page size. The daemon clamps the page size to 1 to 5000. The default is 1000.
encodingLogEncodingSets 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.cursor

Wait 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)
ParameterTypeDescription
timeoutfloatSets 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.

PermissionGrants
write:sandboxesStart, connect to, resize, and kill PTY sessions