From c695fc504dda9285f6bb9fe2a90997dcc67b59b6 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:46:02 +0200 Subject: [PATCH 1/2] fix(sdk): support sending stdin through the PTY command handle Wire up `handle_send_stdin`/`handleSendStdin` on the handle returned by `pty.create()`/`pty.connect()` across the JS and Python (sync + async) SDKs, so `handle.send_stdin(...)`/`handle.sendStdin(...)` works the same as for regular command handles instead of raising "Sending stdin is not supported for this command handle." PTY input methods now also accept `str`, encoding it as UTF-8. In the JS SDK, `pty.sendStdin()` is added as the canonical PTY input method to mirror `commands.sendStdin()`, with `pty.sendInput()` kept as a deprecated alias. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/pty-handle-send-stdin.md | 8 ++++ packages/js-sdk/src/sandbox/commands/pty.ts | 30 +++++++++++--- .../tests/sandbox/pty/sendInput.test.ts | 39 ++++++++++++++++++- .../e2b/sandbox_async/commands/pty.py | 22 +++++++---- .../e2b/sandbox_sync/commands/pty.py | 22 +++++++---- .../sandbox_async/pty/test_send_input.py | 18 +++++++++ .../sync/sandbox_sync/pty/test_send_input.py | 14 +++++++ 7 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 .changeset/pty-handle-send-stdin.md diff --git a/.changeset/pty-handle-send-stdin.md b/.changeset/pty-handle-send-stdin.md new file mode 100644 index 0000000000..34af0f28d1 --- /dev/null +++ b/.changeset/pty-handle-send-stdin.md @@ -0,0 +1,8 @@ +--- +"@e2b/python-sdk": patch +"e2b": patch +--- + +Support sending input to a PTY directly through the command handle (`handle.send_stdin` / `handle.sendStdin`), matching the regular command handle. Previously stdin could only be sent via the PID-keyed `sandbox.pty.send_stdin` / `sandbox.pty.sendInput`, and calling `handle.send_stdin` raised "Sending stdin is not supported for this command handle." PTY input methods now also accept `str` input, encoding it as UTF-8. + +In the JS SDK, `sandbox.pty.sendStdin()` is added as the canonical PTY input method to mirror `sandbox.commands.sendStdin()`; the existing `sandbox.pty.sendInput()` remains as a deprecated alias. diff --git a/packages/js-sdk/src/sandbox/commands/pty.ts b/packages/js-sdk/src/sandbox/commands/pty.ts index 0fb685a615..d719507b50 100644 --- a/packages/js-sdk/src/sandbox/commands/pty.ts +++ b/packages/js-sdk/src/sandbox/commands/pty.ts @@ -153,7 +153,7 @@ export class Pty { undefined, undefined, opts.onData, - undefined, + (data, stdinOpts) => this.sendStdin(pid, data, stdinOpts), undefined, this.checkHealth ) @@ -210,7 +210,7 @@ export class Pty { undefined, undefined, opts?.onData, - undefined, + (data, stdinOpts) => this.sendStdin(pid, data, stdinOpts), undefined, this.checkHealth ) @@ -227,9 +227,9 @@ export class Pty { * @param data input data to send to the PTY. * @param opts connection options. */ - async sendInput( + async sendStdin( pid: number, - data: Uint8Array, + data: string | Uint8Array, opts?: Pick ): Promise { try { @@ -238,7 +238,10 @@ export class Pty { input: { input: { case: 'pty', - value: data, + value: + typeof data === 'string' + ? new TextEncoder().encode(data) + : data, }, }, process: { @@ -260,6 +263,23 @@ export class Pty { } } + /** + * Send input to a PTY. + * + * @deprecated Use {@link Pty.sendStdin} instead. + * + * @param pid process ID of the PTY. + * @param data input data to send to the PTY. + * @param opts connection options. + */ + async sendInput( + pid: number, + data: string | Uint8Array, + opts?: Pick + ): Promise { + return this.sendStdin(pid, data, opts) + } + /** * Resize PTY. * Call this when the terminal window is resized and the number of columns and rows has changed. diff --git a/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts b/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts index b32d7f23dd..9e5286c955 100644 --- a/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts @@ -1,7 +1,23 @@ import { expect } from 'vitest' import { sandboxTest } from '../../setup' -sandboxTest('send input', async ({ sandbox }) => { +sandboxTest('send stdin', async ({ sandbox }) => { + const terminal = await sandbox.pty.create({ + cols: 80, + rows: 24, + onData: () => null, + }) + + await sandbox.pty.sendStdin( + terminal.pid, + new Uint8Array(Buffer.from('exit\n')) + ) + + await terminal.wait() + expect(terminal.exitCode).toBe(0) +}) + +sandboxTest('send input (deprecated alias)', async ({ sandbox }) => { const terminal = await sandbox.pty.create({ cols: 80, rows: 24, @@ -16,3 +32,24 @@ sandboxTest('send input', async ({ sandbox }) => { await terminal.wait() expect(terminal.exitCode).toBe(0) }) + +sandboxTest('send stdin through handle', async ({ sandbox }) => { + let output = '' + + const terminal = await sandbox.pty.create({ + cols: 80, + rows: 24, + envs: { ABC: '123' }, + onData: (data) => { + output += new TextDecoder().decode(data) + }, + }) + + // Send input directly through the handle instead of the PID-keyed module method. + await terminal.sendStdin(new Uint8Array(Buffer.from('echo $ABC\n'))) + await terminal.sendStdin('exit\n') + + await terminal.wait() + expect(terminal.exitCode).toBe(0) + expect(output).toContain('123') +}) diff --git a/packages/python-sdk/e2b/sandbox_async/commands/pty.py b/packages/python-sdk/e2b/sandbox_async/commands/pty.py index db793f2d0e..7734ed334b 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/pty.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/pty.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Dict, Optional, Union import e2b_connect import httpcore @@ -82,7 +82,7 @@ async def kill( async def send_stdin( self, pid: int, - data: bytes, + data: Union[str, bytes], request_timeout: Optional[float] = None, ) -> None: """ @@ -97,7 +97,7 @@ async def send_stdin( process_pb2.SendInputRequest( process=process_pb2.ProcessSelector(pid=pid), input=process_pb2.ProcessInput( - pty=data, + pty=data.encode() if isinstance(data, str) else data, ), ), request_timeout=self._connection_config.get_request_timeout( @@ -164,11 +164,15 @@ async def create( f"Failed to start process: expected start event, got {start_event}" ) + pid = start_event.event.start.pid return AsyncCommandHandle( - pid=start_event.event.start.pid, - handle_kill=lambda: self.kill(start_event.event.start.pid), + pid=pid, + handle_kill=lambda: self.kill(pid), events=events, on_pty=on_data, + handle_send_stdin=lambda data, request_timeout=None: self.send_stdin( + pid, data, request_timeout + ), check_health=self._check_health, ) except Exception as e: @@ -216,11 +220,15 @@ async def connect( f"Failed to connect to process: expected start event, got {start_event}" ) + pid = start_event.event.start.pid return AsyncCommandHandle( - pid=start_event.event.start.pid, - handle_kill=lambda: self.kill(start_event.event.start.pid), + pid=pid, + handle_kill=lambda: self.kill(pid), events=events, on_pty=on_data, + handle_send_stdin=lambda data, request_timeout=None: self.send_stdin( + pid, data, request_timeout + ), check_health=self._check_health, ) except Exception as e: diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/pty.py b/packages/python-sdk/e2b/sandbox_sync/commands/pty.py index f0de8c0eeb..fbfa43c4f4 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/pty.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/pty.py @@ -2,7 +2,7 @@ import httpx import threading -from typing import Dict, Optional +from typing import Dict, Optional, Union from packaging.version import Version from e2b.api import make_logging_event_hooks @@ -110,7 +110,7 @@ def kill( def send_stdin( self, pid: int, - data: bytes, + data: Union[str, bytes], request_timeout: Optional[float] = None, ) -> None: """ @@ -125,7 +125,7 @@ def send_stdin( process_pb2.SendInputRequest( process=process_pb2.ProcessSelector(pid=pid), input=process_pb2.ProcessInput( - pty=data, + pty=data.encode() if isinstance(data, str) else data, ), ), request_timeout=self._connection_config.get_request_timeout( @@ -190,10 +190,14 @@ def create( f"Failed to start process: expected start event, got {start_event}" ) + pid = start_event.event.start.pid return CommandHandle( - pid=start_event.event.start.pid, - handle_kill=lambda: self.kill(start_event.event.start.pid), + pid=pid, + handle_kill=lambda: self.kill(pid), events=events, + handle_send_stdin=lambda data, request_timeout=None: self.send_stdin( + pid, data, request_timeout + ), check_health=self._check_health, ) except Exception as e: @@ -239,10 +243,14 @@ def connect( f"Failed to connect to process: expected start event, got {start_event}" ) + pid = start_event.event.start.pid return CommandHandle( - pid=start_event.event.start.pid, - handle_kill=lambda: self.kill(start_event.event.start.pid), + pid=pid, + handle_kill=lambda: self.kill(pid), events=events, + handle_send_stdin=lambda data, request_timeout=None: self.send_stdin( + pid, data, request_timeout + ), check_health=self._check_health, ) except Exception as e: diff --git a/packages/python-sdk/tests/async/sandbox_async/pty/test_send_input.py b/packages/python-sdk/tests/async/sandbox_async/pty/test_send_input.py index d54eeaa09f..9663bd3e43 100644 --- a/packages/python-sdk/tests/async/sandbox_async/pty/test_send_input.py +++ b/packages/python-sdk/tests/async/sandbox_async/pty/test_send_input.py @@ -9,3 +9,21 @@ async def test_send_input(async_sandbox: AsyncSandbox): await async_sandbox.pty.send_stdin(terminal.pid, b"exit\n") await terminal.wait() assert terminal.exit_code == 0 + + +async def test_handle_send_stdin(async_sandbox: AsyncSandbox): + output = [] + + terminal = await async_sandbox.pty.create( + PtySize(cols=80, rows=24), + on_data=lambda x: output.append(x.decode("utf-8")), + envs={"ABC": "123"}, + ) + + # Send input directly through the handle instead of the PID-keyed module method. + await terminal.send_stdin(b"echo $ABC\n") + await terminal.send_stdin("exit\n") + + await terminal.wait() + assert terminal.exit_code == 0 + assert "123" in "".join(output) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/pty/test_send_input.py b/packages/python-sdk/tests/sync/sandbox_sync/pty/test_send_input.py index 15c5df0344..0755d3d3ce 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/pty/test_send_input.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/pty/test_send_input.py @@ -7,3 +7,17 @@ def test_send_input(sandbox: Sandbox): sandbox.pty.send_stdin(terminal.pid, b"exit\n") result = terminal.wait() assert result.exit_code == 0 + + +def test_handle_send_stdin(sandbox: Sandbox): + output = [] + + terminal = sandbox.pty.create(PtySize(cols=80, rows=24), envs={"ABC": "123"}) + + # Send input directly through the handle instead of the PID-keyed module method. + terminal.send_stdin(b"echo $ABC\n") + terminal.send_stdin("exit\n") + + result = terminal.wait(on_pty=lambda x: output.append(x.decode("utf-8"))) + assert result.exit_code == 0 + assert "123" in "".join(output) From 342824c4b1c8d432afb64e57ab2a8918f01ec55c Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:11:46 +0200 Subject: [PATCH 2/2] fix(cli): migrate PTY input off the deprecated sendInput alias Addresses PR review: the CLI was the one internal consumer still calling the now-deprecated `pty.sendInput`, which would emit a self-inflicted deprecation warning. Switch it to the canonical `pty.sendStdin` (identical behavior, the alias just delegates). Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/pty-handle-send-stdin.md | 3 ++- packages/cli/src/terminal.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/pty-handle-send-stdin.md b/.changeset/pty-handle-send-stdin.md index 34af0f28d1..5bd67b5ca7 100644 --- a/.changeset/pty-handle-send-stdin.md +++ b/.changeset/pty-handle-send-stdin.md @@ -1,8 +1,9 @@ --- "@e2b/python-sdk": patch +"@e2b/cli": patch "e2b": patch --- Support sending input to a PTY directly through the command handle (`handle.send_stdin` / `handle.sendStdin`), matching the regular command handle. Previously stdin could only be sent via the PID-keyed `sandbox.pty.send_stdin` / `sandbox.pty.sendInput`, and calling `handle.send_stdin` raised "Sending stdin is not supported for this command handle." PTY input methods now also accept `str` input, encoding it as UTF-8. -In the JS SDK, `sandbox.pty.sendStdin()` is added as the canonical PTY input method to mirror `sandbox.commands.sendStdin()`; the existing `sandbox.pty.sendInput()` remains as a deprecated alias. +In the JS SDK, `sandbox.pty.sendStdin()` is added as the canonical PTY input method to mirror `sandbox.commands.sendStdin()`; the existing `sandbox.pty.sendInput()` remains as a deprecated alias. The CLI is migrated onto `sendStdin()` internally (no behavior change). diff --git a/packages/cli/src/terminal.ts b/packages/cli/src/terminal.ts index 713314e638..28218fc3c1 100644 --- a/packages/cli/src/terminal.ts +++ b/packages/cli/src/terminal.ts @@ -26,7 +26,7 @@ export async function spawnConnectedTerminal(sandbox: e2b.Sandbox) { const inputQueue = new BatchedQueue(async (batch) => { const combined = Buffer.concat(batch) - await sandbox.pty.sendInput(terminalSession.pid, combined) + await sandbox.pty.sendStdin(terminalSession.pid, combined) }, FLUSH_INPUT_INTERVAL_MS) const resizeListener = process.stdout.on('resize', () =>