Skip to content

Commit 41c646d

Browse files
authored
fix: restore UnixLocal PTY terminal signal defaults (#3082)
1 parent 4b2881c commit 41c646d

2 files changed

Lines changed: 23 additions & 10 deletions

File tree

src/agents/sandbox/sandboxes/unix_local.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
_DEFAULT_WORKSPACE_PREFIX = "sandbox-local-"
7070
_DEFAULT_MANIFEST_ROOT = cast(str, Manifest.model_fields["root"].default)
7171
_PTY_READ_CHUNK_BYTES = 16_384
72+
_PTY_CHILD_SIGNAL_DEFAULTS = (signal.SIGINT, signal.SIGQUIT)
7273

7374
logger = logging.getLogger(__name__)
7475

@@ -78,6 +79,11 @@ def _close_fd_quietly(fd: int) -> None:
7879
os.close(fd)
7980

8081

82+
def _restore_pty_child_signal_defaults() -> None:
83+
for signum in _PTY_CHILD_SIGNAL_DEFAULTS:
84+
signal.signal(signum, signal.SIG_DFL)
85+
86+
8187
class UnixLocalSandboxSessionState(SandboxSessionState):
8288
type: Literal["unix_local"] = "unix_local"
8389
workspace_root_owned: bool = False
@@ -283,9 +289,9 @@ async def pty_exec_start(
283289
def _preexec() -> None:
284290
os.setsid()
285291
fcntl.ioctl(secondary_fd, termios.TIOCSCTTY, 0)
286-
# PTY children should always treat Ctrl-C as an interrupt even if the parent
287-
# process temporarily ignores SIGINT under the test runner.
288-
signal.signal(signal.SIGINT, signal.SIG_DFL)
292+
# PTY children should use default terminal signal behavior even if the parent
293+
# process temporarily ignores signals under the test runner.
294+
_restore_pty_child_signal_defaults()
289295

290296
try:
291297
process = await asyncio.create_subprocess_exec(

tests/sandbox/test_unix_local.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,22 @@ async def test_pty_ctrl_c_interrupts_long_running_process(self, tmp_path: Path)
107107
with pytest.raises(PtySessionNotFoundError):
108108
await session.pty_write_stdin(session_id=started.process_id, chars="")
109109

110+
@pytest.mark.parametrize(
111+
("signum", "chars"),
112+
[
113+
pytest.param(signal.SIGINT, "\x03", id="sigint"),
114+
pytest.param(signal.SIGQUIT, "\x1c", id="sigquit"),
115+
],
116+
)
110117
@pytest.mark.asyncio
111-
async def test_pty_ctrl_c_interrupts_even_if_parent_ignores_sigint(
112-
self, tmp_path: Path
118+
async def test_pty_terminal_signals_interrupt_even_if_parent_ignores_signal(
119+
self, tmp_path: Path, signum: signal.Signals, chars: str
113120
) -> None:
114121
client = UnixLocalSandboxClient()
115122
manifest = Manifest(root=str(tmp_path / "workspace"))
116-
previous_handler = signal.getsignal(signal.SIGINT)
123+
previous_handler = signal.getsignal(signum)
117124

118-
signal.signal(signal.SIGINT, signal.SIG_IGN)
125+
signal.signal(signum, signal.SIG_IGN)
119126
try:
120127
async with await client.create(
121128
manifest=manifest, snapshot=None, options=None
@@ -131,14 +138,14 @@ async def test_pty_ctrl_c_interrupts_even_if_parent_ignores_sigint(
131138

132139
interrupted = await session.pty_write_stdin(
133140
session_id=started.process_id,
134-
chars="\x03",
141+
chars=chars,
135142
yield_time_s=5.5,
136143
)
137144

138145
assert interrupted.process_id is None
139-
assert interrupted.exit_code == -2
146+
assert interrupted.exit_code == -signum
140147
finally:
141-
signal.signal(signal.SIGINT, previous_handler)
148+
signal.signal(signum, previous_handler)
142149

143150
@pytest.mark.asyncio
144151
async def test_non_tty_pty_session_rejects_stdin_and_can_still_be_polled(

0 commit comments

Comments
 (0)