Repository navigation
[Bug]: ExecSandboxInteractive ignores tty=false and always allocates a PTY #2228
Description
Agent Diagnostic
I traced the ExecSandboxInteractive request from the protobuf API through the gateway SSH relay.
The start message reuses ExecSandboxRequest, which contains a tty boolean described as requesting a pseudo-terminal. However, the gateway's interactive exec implementation currently calls request_pty() unconditionally and does not use the value of req.tty.
As a result, an interactive streaming request with tty=false still runs inside a PTY.
This prevents clients from using the bidirectional exec RPC for pipe-based protocols such as stdio MCP, which require:
- persistent streaming stdin;
- stdout and stderr to remain separate;
- no terminal echo or line discipline;
- no PTY newline or control-sequence processing.
Description
ExecSandboxInteractive should respect the tty field in its initial ExecSandboxRequest.
When tty=true, the gateway should request a PTY and forward window resize events.
When tty=false, the gateway should skip the SSH PTY request and ignore resize events while continuing to stream stdin, stdout, and stderr.
Reproduction Steps
The public Python SDK does not currently expose a bidirectional exec method. The following reproduction therefore invokes the generated gRPC client directly to test the gateway's public protobuf RPC.
This is not intended as an example of supported Python SDK usage. It demonstrates the server-side behavior of ExecSandboxInteractive when its start request contains tty=false.
- Create a ready sandbox.
- Invoke
ExecSandboxInteractivethrough a generated gRPC client:
import sys
import threading
from openshell import SandboxClient
from openshell._proto import openshell_pb2 as pb
PROBE = (
"[ -t 0 ] && echo STDIN_TTY || echo STDIN_NOTTY; "
"[ -t 1 ] && echo STDOUT_TTY || echo STDOUT_NOTTY; "
"[ -t 2 ] && echo STDERR_TTY || echo STDERR_NOTTY"
)
def run_interactive(client, sandbox_id, tty):
done = threading.Event()
request = pb.ExecSandboxRequest(
sandbox_id=sandbox_id,
command=["/bin/sh", "-c", PROBE],
tty=tty,
timeout_seconds=20,
)
def requests():
yield pb.ExecSandboxInput(start=request)
done.wait(timeout=30)
print(f"\nRPC: ExecSandboxInteractive")
print(f"Requested tty: {tty}")
print("Remote result:")
try:
responses = client._stub.ExecSandboxInteractive(
requests(),
timeout=40,
)
for event in responses:
payload = event.WhichOneof("payload")
if payload == "stdout":
sys.stdout.buffer.write(event.stdout.data)
sys.stdout.buffer.flush()
elif payload == "stderr":
sys.stderr.buffer.write(event.stderr.data)
sys.stderr.buffer.flush()
elif payload == "exit":
print(f"Exit code: {event.exit.exit_code}")
break
finally:
done.set()
def main():
if len(sys.argv) != 2:
raise SystemExit(
f"Usage: {sys.argv[0]} <sandbox-name>"
)
client = SandboxClient.from_active_cluster()
try:
sandbox = client.get(sys.argv[1])
run_interactive(
client,
sandbox.id,
tty=False,
)
run_interactive(
client,
sandbox.id,
tty=True,
)
finally:
client.close()
if __name__ == "__main__":
main()- Run the script and observe the output.
Current behavior
The gateway accepts the request but unconditionally sends an SSH PTY allocation request. The process therefore observes terminal-backed file descriptors despite tty=false. The result generated from the above script would be:
RPC: ExecSandboxInteractive
Requested tty: False
Remote result:
STDIN_TTY
STDOUT_TTY
STDERR_TTY
Exit code: 0
RPC: ExecSandboxInteractive
Requested tty: True
Remote result:
STDIN_TTY
STDOUT_TTY
STDERR_TTY
Exit code: 0
The result shows that whatever value tty is set, pty is always allocated, which may incur unexpected behaviors in some applications.
Expected contract
One of the following behaviors should be explicitly implemented:
- Honor
tty=falseby executing with ordinary pipes and preserving separate stdout and stderr streams; or - Reject
tty=falsewithINVALID_ARGUMENTand document thatExecSandboxInteractiveis PTY-only.
Silently accepting and ignoring the field makes the protobuf contract ambiguous.
Environment
- OS: Ubuntu 24.04.4 LTS
- Docker: Docker version 29.5.3
- OpenShell: 0.0.80
- Latest Release Checked: yes
- Possible duplicates checked: yes
Logs
Agent-First Checklist
- I pointed my agent at the repo and had it investigate this issue
- I loaded relevant skills (e.g.,
debug-openshell-cluster,debug-inference,openshell-cli) - I checked the latest OpenShell release and either reproduced the issue there or explained why I cannot upgrade/test it
- I searched existing issues for possible duplicates or explained why I could not
- My agent could not resolve this — the diagnostic above explains why
Activity
TaylorMutch commentedon Jul 13, 2026
letv1nnn commentedon Jul 14, 2026
hey @emonq , I'd like to work on this, unless you have already digged into it.