Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion agent/box_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def start(
`window_id` is the tmux session name. The cloud picks it (default
`bux-w1`); we sanitize defensively. First attach to a window
creates it via `tmux new-session -A -d` and seeds the launch
command (claude / bash). Subsequent attaches just open another
command (claude / codex / bash). Subsequent attaches just open another
client onto the same tmux session.

`launch` and `dsp_enabled` are only honored when we're CREATING
Expand Down Expand Up @@ -411,6 +411,7 @@ def _ensure_tmux_window(
session. Splitting create-vs-attach this way avoids "session
not found" races and lets us seed `launch` only on first create.
"""
import shlex
import subprocess

def _run(args: list[str]) -> int:
Expand Down Expand Up @@ -442,6 +443,10 @@ def _run(args: list[str]) -> int:
# `; exec bash -l` so when claude quits the user lands in a
# bash prompt instead of the tmux session ending.
cmd_str = f'{claude_cmd}; exec bash -l'
elif launch == 'codex':
# Same tmux lifecycle as Claude: Codex owns the first command,
# and quitting drops the user to a normal login shell.
cmd_str = f'{shlex.quote(CODEX_BIN)}; exec bash -l'
else:
cmd_str = 'exec bash -l'

Expand Down Expand Up @@ -884,6 +889,11 @@ async def _handle(self, raw: str | bytes) -> None:
if new != self._dsp_enabled:
LOG.info('dsp_enabled %s → %s', self._dsp_enabled, new)
self._dsp_enabled = new
elif cmd == 'update_default_agent':
# Cloud owns the persisted default. The launch choice is carried
# on each shell_attach, so the box-agent only ACKs this command to
# avoid warning noise on older/no-op setting pushes.
await self._send({'type': 'ack', 'cmd': cmd, 'ok': True})
elif cmd == 'shell_input':
import base64 as _b64

Expand Down
36 changes: 36 additions & 0 deletions agent/test_box_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import unittest
import sys
import types
from unittest import mock

sys.modules.setdefault('websockets', types.ModuleType('websockets'))

from agent import box_agent


class ShellSessionLaunchTest(unittest.TestCase):
def test_codex_launch_seeds_tmux_window_with_codex_cli(self) -> None:
calls: list[list[str]] = []

def fake_run(args: list[str], **_kwargs: object) -> mock.Mock:
calls.append(args)
# First call is has-session: report missing so creation path runs.
return mock.Mock(returncode=1 if len(calls) == 1 else 0)

with (
mock.patch.object(box_agent, 'CODEX_BIN', '/usr/local/bin/codex'),
mock.patch('subprocess.run', side_effect=fake_run),
):
box_agent.ShellSession._ensure_tmux_window(
'bux-w1',
launch='codex',
dsp_enabled=True,
)

self.assertEqual(calls[0], ['/usr/bin/tmux', 'has-session', '-t', 'bux-w1'])
self.assertEqual(calls[1][-3:], ['/bin/bash', '-lc', '/usr/local/bin/codex; exec bash -l'])
self.assertEqual(calls[2], ['/usr/bin/tmux', 'set-window-option', '-t', 'bux-w1', 'aggressive-resize', 'on'])


if __name__ == '__main__':
unittest.main()
Loading