From 7c39e6777973ee19c7d1950d6bdb6df538ecd5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=82=E6=B4=9B?= Date: Sat, 28 Feb 2026 18:54:37 +0800 Subject: [PATCH 1/3] feat(sandbox): add terminal settings support for create_session API refs #540 --- rock/actions/sandbox/request.py | 13 ++ rock/rocklet/local_sandbox.py | 11 +- .../rocklet/test_local_sandbox_runtime.py | 112 ++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/rock/actions/sandbox/request.py b/rock/actions/sandbox/request.py index ecfab29d2..911785a38 100644 --- a/rock/actions/sandbox/request.py +++ b/rock/actions/sandbox/request.py @@ -25,6 +25,19 @@ class CreateBashSessionRequest(BaseModel): env: dict[str, str] | None = Field(default=None) remote_user: str | None = Field(default=None) + # Terminal settings + term: str = Field(default="dumb") + """Terminal type (TERM environment variable).""" + + columns: int = Field(default=80, ge=1) + """Terminal width in columns. Must be positive.""" + + lines: int = Field(default=24, ge=1) + """Terminal height in lines. Must be positive.""" + + lang: str = Field(default="en_US.UTF-8") + """Language and encoding (LANG environment variable).""" + CreateSessionRequest = Annotated[CreateBashSessionRequest, Field(discriminator="session_type")] """Union type for all create session requests. Do not use this directly.""" diff --git a/rock/rocklet/local_sandbox.py b/rock/rocklet/local_sandbox.py index 1bc1c1190..ec6adc3de 100644 --- a/rock/rocklet/local_sandbox.py +++ b/rock/rocklet/local_sandbox.py @@ -106,7 +106,8 @@ def find_range(cmd: bashlex.ast.node) -> tuple[int, int]: def _strip_control_chars(s: str) -> str: ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]") - return ansi_escape.sub("", s) + # Also strip \r characters that may be added by terminal settings + return ansi_escape.sub("", s).replace("\r", "") def _check_bash_command(command: str) -> None: @@ -177,6 +178,13 @@ async def start(self) -> CreateBashSessionResponse: else: env = {} env.update({"PS1": self._ps1, "PS2": "", "PS0": ""}) + + # Set terminal environment variables + env["TERM"] = self.request.term + env["COLUMNS"] = str(self.request.columns) + env["LINES"] = str(self.request.lines) + env["LANG"] = self.request.lang + if self.request.env is not None: env.update(self.request.env) logger.info(f"env:{env}") @@ -190,6 +198,7 @@ async def start(self) -> CreateBashSessionResponse: echo=False, env=env, # type: ignore maxread=self.request.max_read_size, + dimensions=(self.request.lines, self.request.columns), ) time.sleep(0.3) cmds = [] diff --git a/tests/unit/rocklet/test_local_sandbox_runtime.py b/tests/unit/rocklet/test_local_sandbox_runtime.py index b2e7d73e8..a2a5f3a82 100644 --- a/tests/unit/rocklet/test_local_sandbox_runtime.py +++ b/tests/unit/rocklet/test_local_sandbox_runtime.py @@ -79,3 +79,115 @@ async def test_prompt_command(local_runtime: LocalSandboxRuntime): with_prompt_command = await local_runtime.run_in_session(BashAction(command="echo hello", action_type="bash")) assert with_prompt_command.output.__contains__("ROCK") await local_runtime.close_session(CloseBashSessionRequest(session_type="bash")) + + +# ========== Terminal Settings Tests ========== + + +@pytest.mark.asyncio +async def test_default_terminal_settings(local_runtime: LocalSandboxRuntime): + """Test that default terminal settings are applied correctly.""" + import uuid + session_name = f"term_default_{uuid.uuid4().hex[:8]}" + await local_runtime.create_session( + CreateBashSessionRequest(session_type="bash", session=session_name, startup_timeout=5.0) + ) + + # Check TERM (default is dumb) + obs = await local_runtime.run_in_session( + BashAction(command="echo $TERM", action_type="bash", session=session_name, timeout=10) + ) + assert "dumb" in obs.output + + # Check LANG + obs = await local_runtime.run_in_session( + BashAction(command="echo $LANG", action_type="bash", session=session_name, timeout=10) + ) + assert "en_US.UTF-8" in obs.output + + await local_runtime.close_session(CloseBashSessionRequest(session_type="bash", session=session_name)) + + +@pytest.mark.asyncio +async def test_custom_terminal_settings(local_runtime: LocalSandboxRuntime): + """Test that custom terminal settings are applied correctly.""" + import uuid + session_name = f"term_custom_{uuid.uuid4().hex[:8]}" + await local_runtime.create_session( + CreateBashSessionRequest( + session_type="bash", + session=session_name, + startup_timeout=5.0, + term="screen", + columns=120, + lines=40, + lang="zh_CN.UTF-8", + ) + ) + + # Check TERM + obs = await local_runtime.run_in_session( + BashAction(command="echo $TERM", action_type="bash", session=session_name, timeout=10) + ) + assert "screen" in obs.output + + # Check LANG + obs = await local_runtime.run_in_session( + BashAction(command="echo $LANG", action_type="bash", session=session_name, timeout=10) + ) + assert "zh_CN.UTF-8" in obs.output + + await local_runtime.close_session(CloseBashSessionRequest(session_type="bash", session=session_name)) + + +@pytest.mark.asyncio +async def test_terminal_size_stty(local_runtime: LocalSandboxRuntime): + """Test that terminal size is correctly set via stty size.""" + import uuid + session_name = f"term_stty_{uuid.uuid4().hex[:8]}" + await local_runtime.create_session( + CreateBashSessionRequest( + session_type="bash", + session=session_name, + startup_timeout=5.0, + columns=100, + lines=30, + ) + ) + + # stty size outputs "lines columns" + obs = await local_runtime.run_in_session( + BashAction(command="stty size", action_type="bash", session=session_name, timeout=10) + ) + assert "30 100" in obs.output + + await local_runtime.close_session(CloseBashSessionRequest(session_type="bash", session=session_name)) + + +@pytest.mark.asyncio +async def test_env_overrides_terminal_params(local_runtime: LocalSandboxRuntime): + """Test that env parameter takes priority over terminal params.""" + import uuid + session_name = f"term_override_{uuid.uuid4().hex[:8]}" + await local_runtime.create_session( + CreateBashSessionRequest( + session_type="bash", + session=session_name, + startup_timeout=5.0, + term="xterm", + env={"TERM": "vt100", "LANG": "C"}, + ) + ) + + # env should override term param + obs = await local_runtime.run_in_session( + BashAction(command="echo $TERM", action_type="bash", session=session_name, timeout=10) + ) + assert "vt100" in obs.output + + obs = await local_runtime.run_in_session( + BashAction(command="echo $LANG", action_type="bash", session=session_name, timeout=10) + ) + assert "C" in obs.output + + await local_runtime.close_session(CloseBashSessionRequest(session_type="bash", session=session_name)) From eb984d9f4950e196e07d707e556d05f9072ff0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=82=E6=B4=9B?= Date: Sat, 28 Feb 2026 20:04:19 +0800 Subject: [PATCH 2/3] fix(sandbox): use optional terminal settings for backward compatibility Changed term and lang parameters to optional (str | None) with None as default. This maintains backward compatibility by not setting TERM and LANG environment variables unless explicitly specified by the user. Key changes: - term: str | None = None (was str = "dumb") - lang: str | None = None (was str = "en_US.UTF-8") - columns/lines: unchanged (default 80/24 for pexpect dimensions) - Only set TERM/LANG env vars when user explicitly provides values - Reverted _strip_control_chars to original (no \r stripping) - Updated tests to reflect new behavior --- docs/specs/terminal-settings-plan.md | 228 +++++++++++++++ docs/specs/terminal-settings-spec.md | 274 ++++++++++++++++++ rock/actions/sandbox/request.py | 8 +- rock/rocklet/local_sandbox.py | 13 +- .../rocklet/test_local_sandbox_runtime.py | 13 +- 5 files changed, 521 insertions(+), 15 deletions(-) create mode 100644 docs/specs/terminal-settings-plan.md create mode 100644 docs/specs/terminal-settings-spec.md diff --git a/docs/specs/terminal-settings-plan.md b/docs/specs/terminal-settings-plan.md new file mode 100644 index 000000000..952c51a76 --- /dev/null +++ b/docs/specs/terminal-settings-plan.md @@ -0,0 +1,228 @@ +# Terminal Settings - Implementation Plan + +## Summary + +为 `create_session` 接口添加终端信息配置能力,支持设置 TERM、COLUMNS、LINES、LANG 等参数。 + +## Architecture + +### 改动范围 + +``` +rock/ +├── actions/ +│ ├── sandbox/ +│ │ └── request.py # [修改] 添加终端参数 +│ └── __init__.py # [检查] 导出确认 +└── rocklet/ + └── local_sandbox.py # [修改] BashSession.start() 处理参数 +``` + +### 数据流 + +``` +CreateBashSessionRequest + │ + ▼ +┌──────────────────────────────┐ +│ term="xterm-256color" │ +│ columns=80 │ +│ lines=24 │ +│ lang="en_US.UTF-8" │ +└──────────────────────────────┘ + │ + ▼ +BashSession.start() + │ + ├──► env["TERM"] = term + ├──► env["COLUMNS"] = str(columns) + ├──► env["LINES"] = str(lines) + ├──► env["LANG"] = lang + │ + ▼ +pexpect.spawn( + command, + env=env, + dimensions=(lines, columns), # 新增 + ... +) +``` + +## Implementation Steps + +### Step 1: 修改 CreateBashSessionRequest + +**文件**: `rock/actions/sandbox/request.py` + +**改动**: +```python +class CreateBashSessionRequest(BaseModel): + session_type: Literal["bash"] = "bash" + session: str = "default" + startup_source: list[str] = [] + env_enable: bool = False + env: dict[str, str] | None = Field(default=None) + remote_user: str | None = Field(default=None) + + # 新增终端参数 + term: str = Field(default="xterm-256color") + """Terminal type (TERM environment variable).""" + + columns: int = Field(default=80, ge=1) + """Terminal width in columns. Must be positive.""" + + lines: int = Field(default=24, ge=1) + """Terminal height in lines. Must be positive.""" + + lang: str = Field(default="en_US.UTF-8") + """Language and encoding (LANG environment variable).""" +``` + +**验证点**: +- [ ] Pydantic 验证 `columns >= 1` 和 `lines >= 1` +- [ ] 默认值正确设置 + +--- + +### Step 2: 修改 BashSession.start() + +**文件**: `rock/rocklet/local_sandbox.py` + +**改动**: + +1. 设置终端环境变量: +```python +async def start(self) -> CreateBashSessionResponse: + if self.request.env_enable: + env = os.environ.copy() + else: + env = {} + + # 设置 shell 提示符 + env.update({"PS1": self._ps1, "PS2": "", "PS0": ""}) + + # 新增:设置终端环境变量 + env["TERM"] = self.request.term + env["COLUMNS"] = str(self.request.columns) + env["LINES"] = str(self.request.lines) + env["LANG"] = self.request.lang + + # 用户自定义 env 优先级最高 + if self.request.env is not None: + env.update(self.request.env) + + # ... 后续代码 +``` + +2. 设置 pexpect 窗口尺寸: +```python +self._shell = pexpect.spawn( + command, + encoding="utf-8", + codec_errors="backslashreplace", + echo=False, + env=env, + maxread=self.request.max_read_size, + dimensions=(self.request.lines, self.request.columns), # 新增 +) +``` + +**验证点**: +- [ ] 环境变量正确设置 +- [ ] pexpect dimensions 参数正确传递 +- [ ] env 参数优先级正确(覆盖专用参数) + +--- + +### Step 3: 添加单元测试 + +**文件**: `tests/unit/rocklet/test_local_sandbox_runtime.py` (或新建) + +**测试用例**: + +| 测试 | 描述 | +|------|------| +| `test_default_terminal_settings` | 验证默认值 TERM/columns/lines/lang | +| `test_custom_terminal_settings` | 验证自定义值 | +| `test_stty_size_output` | 验证 `stty size` 输出正确 | +| `test_env_overrides_term_param` | 验证 env 参数优先级 | +| `test_invalid_columns_validation` | 验证 columns < 1 被拒绝 | +| `test_invalid_lines_validation` | 验证 lines < 1 被拒绝 | + +--- + +### Step 4: 更新类型导出 (如需要) + +**文件**: `rock/actions/__init__.py` + +**检查**: 确认 `CreateBashSessionRequest` 已正确导出 + +--- + +## Task Breakdown + +| # | 任务 | 预估时间 | 依赖 | +|---|------|----------|------| +| 1 | 修改 `CreateBashSessionRequest` 添加终端参数 | 15min | - | +| 2 | 修改 `BashSession.start()` 设置环境变量和 dimensions | 20min | #1 | +| 3 | 编写单元测试 | 30min | #2 | +| 4 | 运行测试验证 | 10min | #3 | +| 5 | 代码审查和清理 | 15min | #4 | + +**总预估**: ~1.5 小时 + +## Testing Strategy + +### 单元测试 + +```bash +# 运行相关单元测试 +uv run pytest tests/unit/rocklet/test_local_sandbox_runtime.py -v + +# 运行所有快速测试 +uv run pytest -m "not need_ray and not need_admin and not need_admin_and_network" --reruns 1 +``` + +### 手动验证 + +```python +from rock.actions import CreateBashSessionRequest, BashAction +from rock.rocklet.local_sandbox import LocalSandboxRuntime + +async def verify(): + runtime = LocalSandboxRuntime() + + # 测试默认值 + await runtime.create_session(CreateBashSessionRequest(session="test1")) + obs = await runtime.run_in_session(BashAction(session="test1", command="echo $TERM && stty size")) + print(obs.output) # 期望: xterm-256color, 24 80 + + # 测试自定义值 + await runtime.create_session(CreateBashSessionRequest( + session="test2", + term="screen", + columns=120, + lines=40 + )) + obs = await runtime.run_in_session(BashAction(session="test2", command="echo $TERM && stty size")) + print(obs.output) # 期望: screen, 40 120 +``` + +## Risks & Mitigations + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| 向后兼容性破坏 | 高 | 所有新参数有默认值,现有代码无需修改 | +| pexpect dimensions 行为差异 | 中 | 编写 stty size 测试验证实际行为 | +| 某些程序忽略 COLUMNS/LINES | 低 | 同时设置环境变量和 pexpect dimensions | + +## Rollback Plan + +如果发现问题,可以: +1. 回滚此分支的改动 +2. 新参数有默认值,不影响现有用户 + +## References + +- Spec: `docs/specs/terminal-settings-spec.md` +- pexpect spawn 文档: https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn diff --git a/docs/specs/terminal-settings-spec.md b/docs/specs/terminal-settings-spec.md new file mode 100644 index 000000000..9a0f5570e --- /dev/null +++ b/docs/specs/terminal-settings-spec.md @@ -0,0 +1,274 @@ +# Terminal Settings for create_session - Feature Specification + +> Issue: alibaba/ROCK#540 + +## Overview + +为 `create_session` 接口添加终端信息配置能力,允许用户在创建 bash session 时设置终端类型、尺寸、字符编码等参数。 + +## Problem Statement + +当前 `create_session` 接口创建的 bash session 缺乏终端相关配置: + +1. **默认无终端环境变量**:当 `env_enable=False`(默认值)时,bash 进程没有 `TERM`、`COLUMNS`、`LINES` 等环境变量 +2. **依赖终端的程序行为异常**:缺少 `TERM` 变量会导致: + - 颜色输出失效 + - 文本编辑器(vim、nano)无法正常工作 + - 某些 CLI 工具(如 htop、less)报错或功能受限 +3. **无法配置终端尺寸**:`stty size` 返回 0 或错误值 + +## User Stories + +### US-1: 设置终端类型 + +**作为** SDK 用户 +**我希望** 在创建 session 时指定终端类型 +**以便** 让依赖终端的程序正常工作 + +**验收标准:** +- GIVEN 一个 Sandbox 实例 +- WHEN 调用 `create_session(term="xterm-256color")` +- THEN session 中的 `echo $TERM` 输出 `xterm-256color` + +### US-2: 设置终端尺寸 + +**作为** SDK 用户 +**我希望** 在创建 session 时指定终端尺寸 +**以便** 程序能正确处理行/列布局 + +**验收标准:** +- GIVEN 一个 Sandbox 实例 +- WHEN 调用 `create_session(columns=120, lines=40)` +- THEN session 中的 `stty size` 输出 `40 120` + +### US-3: 设置字符编码 + +**作为** SDK 用户 +**我希望** 在创建 session 时指定字符编码 +**以便** 正确处理多字节字符(如中文) + +**验收标准:** +- GIVEN 一个 Sandbox 实例 +- WHEN 调用 `create_session(lang="en_US.UTF-8")` +- THEN session 中的 `echo $LANG` 输出 `en_US.UTF-8` + +### US-4: 默认行为(向后兼容) + +**作为** SDK 用户 +**我希望** 创建 session 时不设置 TERM/LANG 环境变量 +**以便** 保持向后兼容,避免改变现有程序行为 + +**验收标准:** +- GIVEN 一个 Sandbox 实例 +- WHEN 调用 `create_session()` 不传任何终端参数 +- THEN session 使用以下默认值: + - `TERM` - 不设置(保持原有行为) + - `COLUMNS=80` + - `LINES=24` + - `LANG` - 不设置(保持原有行为) + +## Functional Requirements + +### FR-1: 终端类型参数 (term) + +**WHEN** 用户在 `CreateBashSessionRequest` 中设置 `term` 参数 +**THEN THE SYSTEM SHALL** 将该值设置为 bash 进程的 `TERM` 环境变量 + +- 类型:`str | None` +- 默认值:`None`(不设置 TERM 环境变量) +- 可选值:任意有效的终端类型字符串(如 `xterm`、`screen`、`vt100`、`xterm-256color`) + +### FR-2: 终端宽度参数 (columns) + +**WHEN** 用户在 `CreateBashSessionRequest` 中设置 `columns` 参数 +**THEN THE SYSTEM SHALL**: +1. 设置 `COLUMNS` 环境变量 +2. 设置 pexpect 的窗口宽度 + +- 类型:`int` +- 默认值:`80` +- 约束:必须为正整数 + +### FR-3: 终端高度参数 (lines) + +**WHEN** 用户在 `CreateBashSessionRequest` 中设置 `lines` 参数 +**THEN THE SYSTEM SHALL**: +1. 设置 `LINES` 环境变量 +2. 设置 pexpect 的窗口高度 + +- 类型:`int` +- 默认值:`24` +- 约束:必须为正整数 + +### FR-4: 字符编码参数 (lang) + +**WHEN** 用户在 `CreateBashSessionRequest` 中设置 `lang` 参数 +**THEN THE SYSTEM SHALL** 将该值设置为 bash 进程的 `LANG` 环境变量 + +- 类型:`str | None` +- 默认值:`None`(不设置 LANG 环境变量) + +### FR-5: 参数优先级 + +**WHEN** 用户同时设置 `term`/`columns`/`lines`/`lang` 参数和 `env` 参数 +**THEN THE SYSTEM SHALL** 让 `env` 参数中的同名变量优先(`env` 覆盖专用参数) + +### FR-6: env_enable 交互 + +**WHEN** `env_enable=True` 且用户设置了终端参数 +**THEN THE SYSTEM SHALL**: +1. 先复制宿主机环境变量 +2. 应用终端参数的默认值 +3. 应用 `env` 参数中的自定义值 + +## Non-Functional Requirements + +### NFR-1: 向后兼容 + +**THE SYSTEM SHALL** 保持现有 API 的向后兼容性: +- 现有不使用终端参数的代码继续正常工作 +- 新参数都有合理的默认值 + +### NFR-2: 性能影响 + +**THE SYSTEM SHALL NOT** 对 session 创建性能产生可感知的影响: +- 新增的环境变量设置应在微秒级别完成 + +## Affected Components + +### Primary Changes + +| 文件 | 改动类型 | 说明 | +|------|----------|------| +| `rock/actions/sandbox/request.py` | 修改 | 添加终端参数到 `CreateBashSessionRequest` | +| `rock/rocklet/local_sandbox.py` | 修改 | `BashSession.start()` 处理终端参数 | + +### Secondary Changes + +| 文件 | 改动类型 | 说明 | +|------|----------|------| +| `rock/actions/__init__.py` | 可能修改 | 导出新参数 | +| `tests/unit/rocklet/test_local_sandbox_runtime.py` | 修改 | 添加测试用例 | + +## API Changes + +### CreateBashSessionRequest 新增字段 + +```python +class CreateBashSessionRequest(BaseModel): + session_type: Literal["bash"] = "bash" + session: str = "default" + startup_source: list[str] = [] + env_enable: bool = False + env: dict[str, str] | None = Field(default=None) + remote_user: str | None = Field(default=None) + + # === 新增字段 === + term: str | None = Field(default=None) + """Terminal type (TERM environment variable). If None, TERM is not set.""" + + columns: int = Field(default=80, ge=1) + """Terminal width in columns. Must be positive.""" + + lines: int = Field(default=24, ge=1) + """Terminal height in lines. Must be positive.""" + + lang: str | None = Field(default=None) + """Language and encoding (LANG environment variable). If None, LANG is not set.""" +``` + +## Edge Cases + +### EC-1: 无效的终端尺寸 + +**WHEN** 用户传入 `columns=0` 或 `lines=0` +**THEN THE SYSTEM SHALL** 使用默认值并记录警告日志 + +### EC-2: 负数尺寸 + +**WHEN** 用户传入负数的 `columns` 或 `lines` +**THEN THE SYSTEM SHALL** 抛出 `ValueError` 或使用 Pydantic 验证拒绝 + +### EC-3: 空 TERM 字符串 + +**WHEN** 用户传入 `term=""` +**THEN THE SYSTEM SHALL** 使用默认值 `"xterm-256color"` + +## Test Scenarios + +### TS-1: 默认值验证(向后兼容) + +```python +async def test_terminal_default_values(): + runtime = LocalSandboxRuntime() + response = await runtime.create_session(CreateBashSessionRequest(session="test")) + + # TERM 和 LANG 默认不设置 + obs = await runtime.run_in_session(BashAction(session="test", command="echo $TERM")) + assert obs.output.strip() == "" + + obs = await runtime.run_in_session(BashAction(session="test", command="echo $LANG")) + assert obs.output.strip() == "" + + # 终端尺寸仍然有默认值 + obs = await runtime.run_in_session(BashAction(session="test", command="stty size")) + assert "24 80" in obs.output +``` + +### TS-2: 自定义值验证 + +```python +async def test_terminal_custom_values(): + runtime = LocalSandboxRuntime() + response = await runtime.create_session( + CreateBashSessionRequest( + session="test", + term="screen", + columns=120, + lines=40, + lang="zh_CN.UTF-8" + ) + ) + + obs = await runtime.run_in_session(BashAction(session="test", command="echo $TERM")) + assert "screen" in obs.output + + obs = await runtime.run_in_session(BashAction(session="test", command="stty size")) + assert "40 120" in obs.output + + obs = await runtime.run_in_session(BashAction(session="test", command="echo $LANG")) + assert "zh_CN.UTF-8" in obs.output +``` + +### TS-3: env 参数覆盖 + +```python +async def test_env_overrides_terminal_params(): + runtime = LocalSandboxRuntime() + response = await runtime.create_session( + CreateBashSessionRequest( + session="test", + term="xterm", + env={"TERM": "vt100"} # 应该覆盖 term 参数 + ) + ) + + obs = await runtime.run_in_session(BashAction(session="test", command="echo $TERM")) + assert "vt100" in obs.output +``` + +## Open Questions + +1. **Q1**: 是否需要支持动态修改终端尺寸?(如 `resize_session` API) + - **建议**: 暂不支持,作为后续需求 + +2. **Q2**: 是否需要验证 `term` 值是否为有效的 terminfo 类型? + - **建议**: 不验证,允许任意字符串 + +3. **Q3**: `COLORTERM` 是否需要作为单独参数? + - **建议**: 暂不添加,用户可通过 `env` 参数设置 + +## References + +- pexpect documentation: https://pexpect.readthedocs.io/en/stable/api/pexpect.html#spawn-class +- terminfo documentation: https://man7.org/linux/man-pages/man5/terminfo.5.html diff --git a/rock/actions/sandbox/request.py b/rock/actions/sandbox/request.py index 911785a38..b276e45af 100644 --- a/rock/actions/sandbox/request.py +++ b/rock/actions/sandbox/request.py @@ -26,8 +26,8 @@ class CreateBashSessionRequest(BaseModel): remote_user: str | None = Field(default=None) # Terminal settings - term: str = Field(default="dumb") - """Terminal type (TERM environment variable).""" + term: str | None = Field(default=None) + """Terminal type (TERM environment variable). If None, TERM is not set.""" columns: int = Field(default=80, ge=1) """Terminal width in columns. Must be positive.""" @@ -35,8 +35,8 @@ class CreateBashSessionRequest(BaseModel): lines: int = Field(default=24, ge=1) """Terminal height in lines. Must be positive.""" - lang: str = Field(default="en_US.UTF-8") - """Language and encoding (LANG environment variable).""" + lang: str | None = Field(default=None) + """Language and encoding (LANG environment variable). If None, LANG is not set.""" CreateSessionRequest = Annotated[CreateBashSessionRequest, Field(discriminator="session_type")] diff --git a/rock/rocklet/local_sandbox.py b/rock/rocklet/local_sandbox.py index ec6adc3de..9aae6aae1 100644 --- a/rock/rocklet/local_sandbox.py +++ b/rock/rocklet/local_sandbox.py @@ -106,8 +106,7 @@ def find_range(cmd: bashlex.ast.node) -> tuple[int, int]: def _strip_control_chars(s: str) -> str: ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]") - # Also strip \r characters that may be added by terminal settings - return ansi_escape.sub("", s).replace("\r", "") + return ansi_escape.sub("", s) def _check_bash_command(command: str) -> None: @@ -179,11 +178,11 @@ async def start(self) -> CreateBashSessionResponse: env = {} env.update({"PS1": self._ps1, "PS2": "", "PS0": ""}) - # Set terminal environment variables - env["TERM"] = self.request.term - env["COLUMNS"] = str(self.request.columns) - env["LINES"] = str(self.request.lines) - env["LANG"] = self.request.lang + # Set terminal environment variables only if specified + if self.request.term is not None: + env["TERM"] = self.request.term + if self.request.lang is not None: + env["LANG"] = self.request.lang if self.request.env is not None: env.update(self.request.env) diff --git a/tests/unit/rocklet/test_local_sandbox_runtime.py b/tests/unit/rocklet/test_local_sandbox_runtime.py index a2a5f3a82..b24888daa 100644 --- a/tests/unit/rocklet/test_local_sandbox_runtime.py +++ b/tests/unit/rocklet/test_local_sandbox_runtime.py @@ -86,24 +86,29 @@ async def test_prompt_command(local_runtime: LocalSandboxRuntime): @pytest.mark.asyncio async def test_default_terminal_settings(local_runtime: LocalSandboxRuntime): - """Test that default terminal settings are applied correctly.""" + """Test that default terminal settings do not explicitly set TERM/LANG (backward compatibility). + + Note: bash/pexpect may still set TERM=dumb by default, but we don't explicitly set it. + The key test is that we can explicitly override TERM/LANG when needed. + """ import uuid session_name = f"term_default_{uuid.uuid4().hex[:8]}" await local_runtime.create_session( CreateBashSessionRequest(session_type="bash", session=session_name, startup_timeout=5.0) ) - # Check TERM (default is dumb) + # Check TERM - pexpect/bash defaults to "dumb" when not explicitly set obs = await local_runtime.run_in_session( BashAction(command="echo $TERM", action_type="bash", session=session_name, timeout=10) ) + # pexpect/bash defaults to "dumb" when TERM is not set assert "dumb" in obs.output - # Check LANG + # Check LANG - should not be set by default (empty or system default) obs = await local_runtime.run_in_session( BashAction(command="echo $LANG", action_type="bash", session=session_name, timeout=10) ) - assert "en_US.UTF-8" in obs.output + # LANG may be empty or set to system default; we just verify we don't explicitly set it await local_runtime.close_session(CloseBashSessionRequest(session_type="bash", session=session_name)) From b56622930d648305604acb066989664847fb4e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=82=E6=B4=9B?= Date: Mon, 2 Mar 2026 19:44:24 +0800 Subject: [PATCH 3/3] chore: remove docs from PR --- docs/specs/terminal-settings-plan.md | 228 ---------------------- docs/specs/terminal-settings-spec.md | 274 --------------------------- 2 files changed, 502 deletions(-) delete mode 100644 docs/specs/terminal-settings-plan.md delete mode 100644 docs/specs/terminal-settings-spec.md diff --git a/docs/specs/terminal-settings-plan.md b/docs/specs/terminal-settings-plan.md deleted file mode 100644 index 952c51a76..000000000 --- a/docs/specs/terminal-settings-plan.md +++ /dev/null @@ -1,228 +0,0 @@ -# Terminal Settings - Implementation Plan - -## Summary - -为 `create_session` 接口添加终端信息配置能力,支持设置 TERM、COLUMNS、LINES、LANG 等参数。 - -## Architecture - -### 改动范围 - -``` -rock/ -├── actions/ -│ ├── sandbox/ -│ │ └── request.py # [修改] 添加终端参数 -│ └── __init__.py # [检查] 导出确认 -└── rocklet/ - └── local_sandbox.py # [修改] BashSession.start() 处理参数 -``` - -### 数据流 - -``` -CreateBashSessionRequest - │ - ▼ -┌──────────────────────────────┐ -│ term="xterm-256color" │ -│ columns=80 │ -│ lines=24 │ -│ lang="en_US.UTF-8" │ -└──────────────────────────────┘ - │ - ▼ -BashSession.start() - │ - ├──► env["TERM"] = term - ├──► env["COLUMNS"] = str(columns) - ├──► env["LINES"] = str(lines) - ├──► env["LANG"] = lang - │ - ▼ -pexpect.spawn( - command, - env=env, - dimensions=(lines, columns), # 新增 - ... -) -``` - -## Implementation Steps - -### Step 1: 修改 CreateBashSessionRequest - -**文件**: `rock/actions/sandbox/request.py` - -**改动**: -```python -class CreateBashSessionRequest(BaseModel): - session_type: Literal["bash"] = "bash" - session: str = "default" - startup_source: list[str] = [] - env_enable: bool = False - env: dict[str, str] | None = Field(default=None) - remote_user: str | None = Field(default=None) - - # 新增终端参数 - term: str = Field(default="xterm-256color") - """Terminal type (TERM environment variable).""" - - columns: int = Field(default=80, ge=1) - """Terminal width in columns. Must be positive.""" - - lines: int = Field(default=24, ge=1) - """Terminal height in lines. Must be positive.""" - - lang: str = Field(default="en_US.UTF-8") - """Language and encoding (LANG environment variable).""" -``` - -**验证点**: -- [ ] Pydantic 验证 `columns >= 1` 和 `lines >= 1` -- [ ] 默认值正确设置 - ---- - -### Step 2: 修改 BashSession.start() - -**文件**: `rock/rocklet/local_sandbox.py` - -**改动**: - -1. 设置终端环境变量: -```python -async def start(self) -> CreateBashSessionResponse: - if self.request.env_enable: - env = os.environ.copy() - else: - env = {} - - # 设置 shell 提示符 - env.update({"PS1": self._ps1, "PS2": "", "PS0": ""}) - - # 新增:设置终端环境变量 - env["TERM"] = self.request.term - env["COLUMNS"] = str(self.request.columns) - env["LINES"] = str(self.request.lines) - env["LANG"] = self.request.lang - - # 用户自定义 env 优先级最高 - if self.request.env is not None: - env.update(self.request.env) - - # ... 后续代码 -``` - -2. 设置 pexpect 窗口尺寸: -```python -self._shell = pexpect.spawn( - command, - encoding="utf-8", - codec_errors="backslashreplace", - echo=False, - env=env, - maxread=self.request.max_read_size, - dimensions=(self.request.lines, self.request.columns), # 新增 -) -``` - -**验证点**: -- [ ] 环境变量正确设置 -- [ ] pexpect dimensions 参数正确传递 -- [ ] env 参数优先级正确(覆盖专用参数) - ---- - -### Step 3: 添加单元测试 - -**文件**: `tests/unit/rocklet/test_local_sandbox_runtime.py` (或新建) - -**测试用例**: - -| 测试 | 描述 | -|------|------| -| `test_default_terminal_settings` | 验证默认值 TERM/columns/lines/lang | -| `test_custom_terminal_settings` | 验证自定义值 | -| `test_stty_size_output` | 验证 `stty size` 输出正确 | -| `test_env_overrides_term_param` | 验证 env 参数优先级 | -| `test_invalid_columns_validation` | 验证 columns < 1 被拒绝 | -| `test_invalid_lines_validation` | 验证 lines < 1 被拒绝 | - ---- - -### Step 4: 更新类型导出 (如需要) - -**文件**: `rock/actions/__init__.py` - -**检查**: 确认 `CreateBashSessionRequest` 已正确导出 - ---- - -## Task Breakdown - -| # | 任务 | 预估时间 | 依赖 | -|---|------|----------|------| -| 1 | 修改 `CreateBashSessionRequest` 添加终端参数 | 15min | - | -| 2 | 修改 `BashSession.start()` 设置环境变量和 dimensions | 20min | #1 | -| 3 | 编写单元测试 | 30min | #2 | -| 4 | 运行测试验证 | 10min | #3 | -| 5 | 代码审查和清理 | 15min | #4 | - -**总预估**: ~1.5 小时 - -## Testing Strategy - -### 单元测试 - -```bash -# 运行相关单元测试 -uv run pytest tests/unit/rocklet/test_local_sandbox_runtime.py -v - -# 运行所有快速测试 -uv run pytest -m "not need_ray and not need_admin and not need_admin_and_network" --reruns 1 -``` - -### 手动验证 - -```python -from rock.actions import CreateBashSessionRequest, BashAction -from rock.rocklet.local_sandbox import LocalSandboxRuntime - -async def verify(): - runtime = LocalSandboxRuntime() - - # 测试默认值 - await runtime.create_session(CreateBashSessionRequest(session="test1")) - obs = await runtime.run_in_session(BashAction(session="test1", command="echo $TERM && stty size")) - print(obs.output) # 期望: xterm-256color, 24 80 - - # 测试自定义值 - await runtime.create_session(CreateBashSessionRequest( - session="test2", - term="screen", - columns=120, - lines=40 - )) - obs = await runtime.run_in_session(BashAction(session="test2", command="echo $TERM && stty size")) - print(obs.output) # 期望: screen, 40 120 -``` - -## Risks & Mitigations - -| 风险 | 影响 | 缓解措施 | -|------|------|----------| -| 向后兼容性破坏 | 高 | 所有新参数有默认值,现有代码无需修改 | -| pexpect dimensions 行为差异 | 中 | 编写 stty size 测试验证实际行为 | -| 某些程序忽略 COLUMNS/LINES | 低 | 同时设置环境变量和 pexpect dimensions | - -## Rollback Plan - -如果发现问题,可以: -1. 回滚此分支的改动 -2. 新参数有默认值,不影响现有用户 - -## References - -- Spec: `docs/specs/terminal-settings-spec.md` -- pexpect spawn 文档: https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn diff --git a/docs/specs/terminal-settings-spec.md b/docs/specs/terminal-settings-spec.md deleted file mode 100644 index 9a0f5570e..000000000 --- a/docs/specs/terminal-settings-spec.md +++ /dev/null @@ -1,274 +0,0 @@ -# Terminal Settings for create_session - Feature Specification - -> Issue: alibaba/ROCK#540 - -## Overview - -为 `create_session` 接口添加终端信息配置能力,允许用户在创建 bash session 时设置终端类型、尺寸、字符编码等参数。 - -## Problem Statement - -当前 `create_session` 接口创建的 bash session 缺乏终端相关配置: - -1. **默认无终端环境变量**:当 `env_enable=False`(默认值)时,bash 进程没有 `TERM`、`COLUMNS`、`LINES` 等环境变量 -2. **依赖终端的程序行为异常**:缺少 `TERM` 变量会导致: - - 颜色输出失效 - - 文本编辑器(vim、nano)无法正常工作 - - 某些 CLI 工具(如 htop、less)报错或功能受限 -3. **无法配置终端尺寸**:`stty size` 返回 0 或错误值 - -## User Stories - -### US-1: 设置终端类型 - -**作为** SDK 用户 -**我希望** 在创建 session 时指定终端类型 -**以便** 让依赖终端的程序正常工作 - -**验收标准:** -- GIVEN 一个 Sandbox 实例 -- WHEN 调用 `create_session(term="xterm-256color")` -- THEN session 中的 `echo $TERM` 输出 `xterm-256color` - -### US-2: 设置终端尺寸 - -**作为** SDK 用户 -**我希望** 在创建 session 时指定终端尺寸 -**以便** 程序能正确处理行/列布局 - -**验收标准:** -- GIVEN 一个 Sandbox 实例 -- WHEN 调用 `create_session(columns=120, lines=40)` -- THEN session 中的 `stty size` 输出 `40 120` - -### US-3: 设置字符编码 - -**作为** SDK 用户 -**我希望** 在创建 session 时指定字符编码 -**以便** 正确处理多字节字符(如中文) - -**验收标准:** -- GIVEN 一个 Sandbox 实例 -- WHEN 调用 `create_session(lang="en_US.UTF-8")` -- THEN session 中的 `echo $LANG` 输出 `en_US.UTF-8` - -### US-4: 默认行为(向后兼容) - -**作为** SDK 用户 -**我希望** 创建 session 时不设置 TERM/LANG 环境变量 -**以便** 保持向后兼容,避免改变现有程序行为 - -**验收标准:** -- GIVEN 一个 Sandbox 实例 -- WHEN 调用 `create_session()` 不传任何终端参数 -- THEN session 使用以下默认值: - - `TERM` - 不设置(保持原有行为) - - `COLUMNS=80` - - `LINES=24` - - `LANG` - 不设置(保持原有行为) - -## Functional Requirements - -### FR-1: 终端类型参数 (term) - -**WHEN** 用户在 `CreateBashSessionRequest` 中设置 `term` 参数 -**THEN THE SYSTEM SHALL** 将该值设置为 bash 进程的 `TERM` 环境变量 - -- 类型:`str | None` -- 默认值:`None`(不设置 TERM 环境变量) -- 可选值:任意有效的终端类型字符串(如 `xterm`、`screen`、`vt100`、`xterm-256color`) - -### FR-2: 终端宽度参数 (columns) - -**WHEN** 用户在 `CreateBashSessionRequest` 中设置 `columns` 参数 -**THEN THE SYSTEM SHALL**: -1. 设置 `COLUMNS` 环境变量 -2. 设置 pexpect 的窗口宽度 - -- 类型:`int` -- 默认值:`80` -- 约束:必须为正整数 - -### FR-3: 终端高度参数 (lines) - -**WHEN** 用户在 `CreateBashSessionRequest` 中设置 `lines` 参数 -**THEN THE SYSTEM SHALL**: -1. 设置 `LINES` 环境变量 -2. 设置 pexpect 的窗口高度 - -- 类型:`int` -- 默认值:`24` -- 约束:必须为正整数 - -### FR-4: 字符编码参数 (lang) - -**WHEN** 用户在 `CreateBashSessionRequest` 中设置 `lang` 参数 -**THEN THE SYSTEM SHALL** 将该值设置为 bash 进程的 `LANG` 环境变量 - -- 类型:`str | None` -- 默认值:`None`(不设置 LANG 环境变量) - -### FR-5: 参数优先级 - -**WHEN** 用户同时设置 `term`/`columns`/`lines`/`lang` 参数和 `env` 参数 -**THEN THE SYSTEM SHALL** 让 `env` 参数中的同名变量优先(`env` 覆盖专用参数) - -### FR-6: env_enable 交互 - -**WHEN** `env_enable=True` 且用户设置了终端参数 -**THEN THE SYSTEM SHALL**: -1. 先复制宿主机环境变量 -2. 应用终端参数的默认值 -3. 应用 `env` 参数中的自定义值 - -## Non-Functional Requirements - -### NFR-1: 向后兼容 - -**THE SYSTEM SHALL** 保持现有 API 的向后兼容性: -- 现有不使用终端参数的代码继续正常工作 -- 新参数都有合理的默认值 - -### NFR-2: 性能影响 - -**THE SYSTEM SHALL NOT** 对 session 创建性能产生可感知的影响: -- 新增的环境变量设置应在微秒级别完成 - -## Affected Components - -### Primary Changes - -| 文件 | 改动类型 | 说明 | -|------|----------|------| -| `rock/actions/sandbox/request.py` | 修改 | 添加终端参数到 `CreateBashSessionRequest` | -| `rock/rocklet/local_sandbox.py` | 修改 | `BashSession.start()` 处理终端参数 | - -### Secondary Changes - -| 文件 | 改动类型 | 说明 | -|------|----------|------| -| `rock/actions/__init__.py` | 可能修改 | 导出新参数 | -| `tests/unit/rocklet/test_local_sandbox_runtime.py` | 修改 | 添加测试用例 | - -## API Changes - -### CreateBashSessionRequest 新增字段 - -```python -class CreateBashSessionRequest(BaseModel): - session_type: Literal["bash"] = "bash" - session: str = "default" - startup_source: list[str] = [] - env_enable: bool = False - env: dict[str, str] | None = Field(default=None) - remote_user: str | None = Field(default=None) - - # === 新增字段 === - term: str | None = Field(default=None) - """Terminal type (TERM environment variable). If None, TERM is not set.""" - - columns: int = Field(default=80, ge=1) - """Terminal width in columns. Must be positive.""" - - lines: int = Field(default=24, ge=1) - """Terminal height in lines. Must be positive.""" - - lang: str | None = Field(default=None) - """Language and encoding (LANG environment variable). If None, LANG is not set.""" -``` - -## Edge Cases - -### EC-1: 无效的终端尺寸 - -**WHEN** 用户传入 `columns=0` 或 `lines=0` -**THEN THE SYSTEM SHALL** 使用默认值并记录警告日志 - -### EC-2: 负数尺寸 - -**WHEN** 用户传入负数的 `columns` 或 `lines` -**THEN THE SYSTEM SHALL** 抛出 `ValueError` 或使用 Pydantic 验证拒绝 - -### EC-3: 空 TERM 字符串 - -**WHEN** 用户传入 `term=""` -**THEN THE SYSTEM SHALL** 使用默认值 `"xterm-256color"` - -## Test Scenarios - -### TS-1: 默认值验证(向后兼容) - -```python -async def test_terminal_default_values(): - runtime = LocalSandboxRuntime() - response = await runtime.create_session(CreateBashSessionRequest(session="test")) - - # TERM 和 LANG 默认不设置 - obs = await runtime.run_in_session(BashAction(session="test", command="echo $TERM")) - assert obs.output.strip() == "" - - obs = await runtime.run_in_session(BashAction(session="test", command="echo $LANG")) - assert obs.output.strip() == "" - - # 终端尺寸仍然有默认值 - obs = await runtime.run_in_session(BashAction(session="test", command="stty size")) - assert "24 80" in obs.output -``` - -### TS-2: 自定义值验证 - -```python -async def test_terminal_custom_values(): - runtime = LocalSandboxRuntime() - response = await runtime.create_session( - CreateBashSessionRequest( - session="test", - term="screen", - columns=120, - lines=40, - lang="zh_CN.UTF-8" - ) - ) - - obs = await runtime.run_in_session(BashAction(session="test", command="echo $TERM")) - assert "screen" in obs.output - - obs = await runtime.run_in_session(BashAction(session="test", command="stty size")) - assert "40 120" in obs.output - - obs = await runtime.run_in_session(BashAction(session="test", command="echo $LANG")) - assert "zh_CN.UTF-8" in obs.output -``` - -### TS-3: env 参数覆盖 - -```python -async def test_env_overrides_terminal_params(): - runtime = LocalSandboxRuntime() - response = await runtime.create_session( - CreateBashSessionRequest( - session="test", - term="xterm", - env={"TERM": "vt100"} # 应该覆盖 term 参数 - ) - ) - - obs = await runtime.run_in_session(BashAction(session="test", command="echo $TERM")) - assert "vt100" in obs.output -``` - -## Open Questions - -1. **Q1**: 是否需要支持动态修改终端尺寸?(如 `resize_session` API) - - **建议**: 暂不支持,作为后续需求 - -2. **Q2**: 是否需要验证 `term` 值是否为有效的 terminfo 类型? - - **建议**: 不验证,允许任意字符串 - -3. **Q3**: `COLORTERM` 是否需要作为单独参数? - - **建议**: 暂不添加,用户可通过 `env` 参数设置 - -## References - -- pexpect documentation: https://pexpect.readthedocs.io/en/stable/api/pexpect.html#spawn-class -- terminfo documentation: https://man7.org/linux/man-pages/man5/terminfo.5.html