Skip to content
Merged
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
124 changes: 124 additions & 0 deletions docs/design/ssh-workspaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# SSH workspaces without a remote Qwen service

[English](ssh-workspaces.md) | [简体中文](ssh-workspaces.zh-CN.md)

Status: implemented. Real SSH end-to-end tests passed against an isolated macOS
sshd with Python 3.9.6. The PR also contains a [Linux verification report](https://github.com/QwenLM/qwen-code/pull/12255#issuecomment-5755063787) for commit `a75c998`, reporting 121 passing checks on Debian 13 with Python 3.13.5.

## Problem and current state

Web Shell can select a remote HTTP daemon, but that requires Qwen on the remote
computer. Workspace registration, filesystem routes and agent tools currently
assume that a workspace path belongs to the local host. The existing execution
environment interface is used by container subagents, not main sessions.

## Scope

Support a Linux SSH target without installing Qwen or starting a remote service.
The initial target requires OpenSSH access, Python 3 for per-request filesystem
operations, Bash for shell commands, and the project's own tools, including Git when Git is used. Local
OpenSSH supplies SSH config, keys, agent authentication and ProxyJump. Unknown
host keys require a normal SSH connection before adding the workspace. Password
prompts are not handled in Web Shell.

The first version includes registration, local session persistence, agent file
reading/writing/editing/search, remote shell commands, Web Shell file operations,
an interactive SSH terminal and Git inspection. Unsupported workspace operations
must fail explicitly; they must never operate on the local anchor directory.
Remote hooks, skills, MCP/LSP, subagents, workflows, worktree creation and automatic
artifact discovery are outside this version.

## Design

### Identity and registration

Accept `ssh://user@host:port/absolute/project` alongside local workspace paths.
Use a connection descriptor and a deterministic, private local anchor directory
for each connection and remote directory. Existing local runtime and session
ownership remain keyed by this anchor; user-visible metadata identifies the SSH
host, explicit port and remote path. Resolve a selected root symlink once during registration and persist its canonical target; subsequent operations reject symlink traversal. Persisted registration restores the same descriptor.
An absent or malformed descriptor under the SSH anchor root is an error, never
an ordinary local workspace.

### SSH transport

Use system OpenSSH with batch authentication, strict host-key checks, bounded
connection time and output, and cancellation. Pass target and remote command as
separate arguments; quote all remote shell arguments. Filesystem requests invoke
a Python script over SSH and exchange JSON. Shell output uses framed stdout/stderr records followed by an explicit exit status, distinguishing a completed command exit 255 from a failed SSH connection; no remote helper file or listener is
installed. Validate paths on the remote host, prevent symlink escapes, preserve
file modes, and use temporary files plus rename for writes. Conditional edits
detect stale content. Connection failures are returned without replaying writes
or commands. Shell commands execute as `bash -c`, including when the local daemon runs on Windows; missing Bash is an error, with no fallback to a different shell. Execute requests send one JSON line and keep SSH stdin open. The remote executor watches for EOF while the command or its output streams remain active, including after output redirection. Cancellation, timeout or output failure terminates the command's process group with SIGTERM followed by SIGKILL after a two-second grace period. Network partitions can delay disconnect detection, and descendants that deliberately leave the process group are outside this cleanup. Cancellation therefore cannot promise that every disconnected remote command has stopped, and the result must say so.

Agent edits compare CRLF and LF consistently while keeping freshness hashes over the original bytes. Existing-file writes, edits and manually revised proposals retain UTF-8 BOM and line-ending format. New files preserve the supplied content.

### Local agent runtime

Keep model credentials, approval decisions, session history and output storage
local. Load the descriptor for the exact anchor when constructing the ACP
session Config. Install an SSH execution environment for the main session and
reuse the existing tool schemas and confirmation wrapper. Implement tool actions
through SSH rather than invoking local tool implementations. Direct the agent to read remote QWEN.md and AGENTS.md through the file
tools; do not import remote executable configuration into the local runtime. Disable local hooks and services that cannot honor remote paths. Main-session structured output, Goal tools, web fetch/search and the opt-in todo tool retain their ordinary configuration gates and local ownership. Override code-mode-only for SSH sessions so the tools execute through SSH. Shell deadlines and output thresholds honor the local settings; truncated shell output preserves both its beginning and its end.

### Daemon and Web Shell

Provide an SSH-aware workspace filesystem factory and terminal launch command.
Classify registration as process-global, descriptor persistence as
persisted-workspace scoped, file/Git operations as selected-runtime scoped and
session operations as live-session-owner scoped. Resolve the selected runtime
before choosing SSH, enforce its trust and generation guards, and explicitly
reject unsupported routes. Read intents remain available before trust; writes, commands and Git inspection require trust. Voice status, settings and transcription use the selected runtime and local model service. A workflow setting can be persisted but never activates workflow execution in an SSH session. No unknown, removed, blocked or disconnected target
may fall back to the primary runtime or local filesystem.

The add-workspace form accepts an SSH address and explains its prerequisites.
Remote workspaces remain in the local daemon's catalog beside local workspaces.
Keep the existing workspace identity used for navigation and session ownership.

## Affected areas

- Core SSH transport and execution environment, main Config wiring and terminal
launch options.
- CLI workspace descriptor storage, registration, filesystem adapter, route
dispatch and ACP Config construction.
- SDK workspace metadata and Web Shell add-workspace presentation.
- Focused tests for transport, remote tools, registration, route ownership and UI.

## Validation and acceptance

Dry-run the registration request against the globally installed CLI first. Then
test the local bundle against an isolated SSH server with a temporary project.
Verify remote-only file changes and command markers, local session persistence,
separate identities for different connections, stale-edit rejection, invalid
paths and authentication failures, cancellation and terminal cleanup. Test that
unsupported routes and missing descriptors cannot touch the local anchor or
primary workspace. Run the full build, typecheck, bundle and focused package
tests, followed by self-audit and independent review.

The feature is complete when an added SSH workspace can run an agent that reads,
edits and tests the remote project, while its Web Shell files, Git inspection and
terminal address that same project, without running Qwen on the remote host.

## Usage and limits

Start `qwen serve` from a local workspace and choose **Add workspace** in Web
Shell. Enter `ssh://user@host:2222/absolute/project`; SSH config aliases work as
`ssh://build-box/absolute/project`. Use `%20` for spaces in paths. The existing
trust dialog applies to the remote project. Enable persistence to restore this
connection after restarting the local daemon. The daemon's primary workspace
must remain local.

The remote host needs Python 3 and the tools required by the project. Agent shell commands require Bash; file operations do not. File tools accept UTF-8 text; binary previews/uploads use the byte API.
Whole text reads and remote writes are limited to 16 MiB, with the existing smaller Web Shell text read/write limits retained. SSH binary uploads also have a 16 MiB limit; larger uploads fail with HTTP 413. Large text reads use line/limit windows. Byte reads seek directly to the requested offset, including in files larger than 16 MiB, and omit a full-file hash for partial windows. Search respects `.gitignore` and `.qwenignore` in
Git repositories, together with the effective `context.fileFiltering.customIgnoreFiles` (default `.agentignore` and `.aiignore`). An explicit empty custom list retains `.qwenignore`. Non-Git projects containing these ignore files fail explicitly, including configured relative ignore-file paths.
Search reports incomplete results when entries are unreadable, a file exceeds the text scan cap, Git warns of skipped entries or a checked-out submodule is omitted. Submodule content can be inspected through the SSH shell. Dubious Git ownership is an explicit error and never changes Git trust configuration. Git inspection includes status and working-tree diffs. Above 500 changed files, the overview returns counts without file details, matching the local fast path. Large untracked previews retain the
1,000,000-byte/400-line limits and report truncation; their line counts cover the bounded
preview. Run other Git commands in the SSH terminal
or through the agent shell tool.

Background shell jobs, local shortcut commands that operate on the project,
worktrees, hooks, skills, MCP/LSP, subagents, workflows, channels, automatic memory and artifact
discovery are unavailable for SSH sessions. Session history, model selection
and approvals remain local. Compound shell approvals persist per-command rules and retain shell-substitution warnings alongside the SSH warning. SSH workspaces are excluded from channel startup restoration and channel ownership selection. This version uses one SSH process per operation;
it does not install a remote agent or synchronize a local project copy. Writes clean up temporary files after ordinary failures; abrupt remote process termination can leave a temporary file and cannot promise cleanup.
102 changes: 102 additions & 0 deletions docs/design/ssh-workspaces.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 无需远程 Qwen 服务的 SSH 工作区

[English](ssh-workspaces.md) | [简体中文](ssh-workspaces.zh-CN.md)

状态:已实现。真实 SSH 端到端测试已在隔离的 macOS sshd 和 Python 3.9.6
环境通过。PR 中还附有针对提交 `a75c998` 的 [Linux 验证报告](https://github.com/QwenLM/qwen-code/pull/12255#issuecomment-5755063787),报告在 Debian 13、Python 3.13.5 上有 121 项检查通过。

## 问题与现状

Web Shell 可以选择远程 HTTP daemon,但远程计算机必须安装 Qwen。
工作区注册、文件接口和 agent 工具目前都假设工作区路径属于本地主机。
现有执行环境接口用于容器子代理,尚未接入主会话。

## 范围

支持 Linux SSH 目标,远端无需安装 Qwen 或启动服务。第一版要求远端可通过
OpenSSH 访问,具备处理单次文件操作的 Python 3、执行 shell 命令的 Bash,以及项目自身的工具;使用 Git
时需要 Git。本地 OpenSSH 提供 SSH config、密钥、agent 认证和 ProxyJump。
未知主机密钥需要先通过普通 SSH 连接确认。Web Shell 不处理密码输入提示。

第一版包含工作区注册、本地会话持久化、agent 文件读写编辑与搜索、远程命令、
Web Shell 文件操作、交互式 SSH 终端和 Git 查看。不支持的工作区操作必须明确
报错,绝不能操作本地占位目录。本版不包含远程 hooks、skills、MCP/LSP、子代理、
工作流、worktree 创建和自动产物发现。

## 设计

### 身份与注册

除本地路径外,接受 `ssh://user@host:port/absolute/project`。
为每个连接和远程目录保存连接描述,并建立确定性的私有本地占位目录。
现有本地运行时和会话归属仍以占位目录为键;面向用户的元数据显示 SSH 主机、
显式端口与远程路径。持久化注册恢复相同的连接描述。SSH 占位根目录下缺失或损坏的
连接描述必须报错,不能被当作普通本地工作区。

注册时允许所选根路径经过符号链接,并保存其解析后的规范目标;后续操作拒绝符号链接穿越。

### SSH 传输

使用系统 OpenSSH,启用批量认证、严格主机密钥检查、连接超时、输出限制和取消。
目标与远程命令使用独立参数传入;所有远程 shell 参数均正确转义。
文件请求通过 SSH 调用 Python 脚本并交换 JSON,不安装远程 helper 文件或监听
服务。在远程主机校验路径、防止符号链接逃逸、保留文件权限,并使用临时文件
加重命名写入。条件编辑检测过期内容。连接失败直接返回,不重放写入或命令。
Shell 命令通过 `bash -c` 执行,即使本地 daemon 运行在 Windows 上也保持一致;缺少 Bash 时明确报错,不回退到其他 shell。执行请求发送一行 JSON 后保持 SSH stdin 打开。远端在命令仍运行或输出流仍打开时监测 EOF,包括输出重定向之后。取消、超时或输出失败时,先向命令进程组发送 SIGTERM,等待两秒后再发送 SIGKILL。网络分区可能延迟断连检测,主动脱离进程组的后代进程也不在清理范围内。因此取消不能保证每个已断连的远程命令都停止,结果必须说明这一点。Shell 输出通过分帧 stdout/stderr 记录和明确的最终退出状态传输,从而区分远端命令退出码 255 与 SSH 连接失败。

Agent 编辑时统一比较 CRLF 和 LF,过期检查仍对原始字节计算哈希。已有文件的写入、编辑和手工修改提案保留 UTF-8 BOM 及换行格式;新文件保留传入内容。

### 本地 agent 运行时

模型凭据、权限决定、会话历史和输出存储保留在本地。创建 ACP 会话 Config 时,
读取精确占位目录对应的连接描述。为主会话安装 SSH 执行环境,复用现有工具
schema 和确认包装器。工具动作通过 SSH 实现,不调用本地工具实现。
提示 agent 通过文件工具读取远程 QWEN.md 和 AGENTS.md,不把远端可执行配置导入本地运行时。
禁用不能正确处理远程路径的本地 hooks 和服务。主会话的结构化输出、Goal 工具、网页获取与搜索,以及显式启用的 todo 工具继续遵守原有配置条件,并由本地会话拥有。SSH 会话覆盖 code-mode-only 设置,让工具通过 SSH 执行。Shell 超时和输出阈值遵守本地设置,截断时保留输出的开头与末尾。

### Daemon 与 Web Shell

提供支持 SSH 的工作区文件系统工厂和终端启动命令。
注册属于进程全局操作,连接描述持久化属于已保存工作区,文件与 Git 操作属于
所选运行时,会话操作属于存活会话的拥有者。先解析所选运行时,再选择 SSH,
执行其信任与代际检查,并明确拒绝未支持的接口。信任前仍可执行读取操作;写入、命令与 Git 查看要求信任。语音状态、设置和转录使用所选运行时及本地模型服务。工作流设置可以保存,但不会启用 SSH 会话中的工作流执行。未知、已移除、阻塞或断线的
目标都不能回退到主运行时或本地文件系统。

添加工作区表单接受 SSH 地址并说明前置条件。远程工作区与本地工作区一起
出现在本地 daemon 的目录中。导航与会话归属继续使用现有工作区身份。

## 影响范围

- Core SSH 传输与执行环境、主 Config 接线和终端启动参数。
- CLI 工作区连接描述存储、注册、文件适配器、路由分发与 ACP Config 创建。
- SDK 工作区元数据与 Web Shell 添加工作区界面。
- 传输、远程工具、注册、路由归属和界面的定向测试。

## 验证与验收

首先对全局安装的 CLI 执行注册请求,确认基线。然后使用本地 bundle、隔离的
SSH 服务和临时项目测试。验证文件改动与命令标记仅发生在远端、本地会话
持久化、不同连接身份隔离、过期编辑拒绝、非法路径与认证失败、取消和终端
清理。验证不支持的接口和缺失连接描述无法触碰本地占位目录或主工作区。
执行完整 build、typecheck、bundle 和定向包测试,再进行自审和独立审查。

添加 SSH 工作区后,agent 能读取、编辑和测试远程项目,Web Shell 文件、Git
查看和终端均指向同一个远程项目,且远程主机不运行 Qwen,即满足功能验收。

## 使用方式与限制

在本地工作区启动 `qwen serve`,然后在 Web Shell 选择 **添加工作区**。
输入 `ssh://user@host:2222/absolute/project`;SSH config 别名可以写为
`ssh://build-box/absolute/project`。路径中的空格使用 `%20`。现有信任对话框
同样适用于远程项目。启用持久化后,重启本地 daemon 会恢复这个连接。
daemon 的主工作区必须保留为本地目录。

远端需要 Python 3 和项目所需的工具。Agent shell 命令需要 Bash,文件操作不依赖 Bash。文件工具接受 UTF-8 文本,
二进制预览和上传使用字节接口。完整文本读取与远程写入上限为 16 MiB,同时保留 Web Shell 原有的更小文本读写限制。SSH 二进制上传也限制为 16 MiB,超限返回 HTTP 413。大文本使用 line/limit 分段读取。字节读取直接定位指定偏移,支持超过 16 MiB 的文件,部分窗口不返回全文件哈希。
Git 仓库内的搜索遵守 `.gitignore`、`.qwenignore` 与生效的 `context.fileFiltering.customIgnoreFiles`(默认 `.agentignore` 和 `.aiignore`)。自定义列表显式为空时仍保留 `.qwenignore`。包含这些忽略文件的非 Git 项目会明确报错,包括配置为相对路径的忽略文件。遇到不可读条目、超出文本扫描上限的文件、Git 跳过条目的警告或省略已检出的子模块时,搜索标记结果不完整。子模块内容可通过 SSH shell 查看。Git 所有权可疑时明确报错,不修改 Git 信任配置。Git 查看包括状态和工作区差异;改动文件超过 500 个时返回计数并省略文件详情,与本地快速路径一致。大文件的未跟踪预览保留 1,000,000 字节/400 行限制并标记截断,行数统计只覆盖
限制内的预览。其他 Git 命令可以通过 SSH 终端或 agent 的 shell 工具执行。

SSH 会话暂不支持后台 shell 任务、操作项目的本地快捷命令、worktree、hooks、skills、
MCP/LSP、子代理、工作流、频道、自动记忆和自动产物发现。会话历史、模型选择和权限
确认保留在本地。复合 shell 命令按子命令持久化授权规则,同时保留命令替换警告和 SSH 警告。SSH 工作区不参与频道启动恢复和频道归属选择。本版每次操作使用一个 SSH 进程,不安装远程 agent,也不同步
一份本地项目副本。普通写入失败会清理临时文件;远端进程被突然终止时可能留下临时文件,无法保证清理。
Loading
Loading