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
24 changes: 24 additions & 0 deletions backend/app/services/project_device_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from sqlalchemy.orm import Session

from app.models.project import Project
from app.schemas.device import DeviceType
from app.schemas.project import (
ProjectConfig,
ProjectDeviceSessionResponse,
Expand All @@ -21,6 +22,7 @@
DeviceSessionNotFoundError,
local_device_session_service,
)
from app.services.device_service import device_service

ProjectSessionType = Literal["terminal", "code_server"]

Expand Down Expand Up @@ -50,6 +52,7 @@ async def start_project_device_session(

project_config = _parse_project_config(project.config)
device_id = _get_bound_device_id(project_config)
_ensure_device_supports_project_sessions(db, user_id, device_id)
path, create_if_missing = _get_project_path(
project_config, project.config, project_id
)
Expand Down Expand Up @@ -126,6 +129,27 @@ def _get_bound_device_id(config: ProjectConfig) -> str:
return config.execution.deviceId


def _ensure_device_supports_project_sessions(
db: Session,
user_id: int,
device_id: str,
) -> None:
"""Reject project terminal and code-server sessions for local devices."""

device_kind = device_service.get_device_by_device_id(db, user_id, device_id)
if not device_kind:
return

spec = getattr(device_kind, "json", None)
spec = spec.get("spec", {}) if isinstance(spec, dict) else {}
device_type = spec.get("deviceType")
if device_type == DeviceType.LOCAL.value:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Local devices do not support terminal or code-server sessions",
)
Comment on lines +132 to +150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add tracing on the new session-capability guard.

This new decision point should be instrumented with @trace_sync and span attributes/events (e.g., device_id, resolved deviceType, rejection reason) for production diagnostics and policy auditing. As per coding guidelines, "Use OpenTelemetry tracing decorators: @trace_async or @trace_sync for entire functions, and add_span_event() or set_span_attribute() for adding details to spans".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/project_device_session_service.py` around lines 132 -
150, Wrap the _ensure_device_supports_project_sessions function with the
OpenTelemetry sync tracer decorator (use `@trace_sync`) and inside the function
set span attributes for device_id and the resolved deviceType (use
set_span_attribute) and emit a span event when rejecting with the reason (e.g.,
"Local devices do not support terminal or code-server sessions") using
add_span_event; also record an event or attribute when device lookup returns no
result. Ensure you import the tracing helpers and call
set_span_attribute/add_span_event at the appropriate points in
_ensure_device_supports_project_sessions so telemetry captures device_id,
resolved deviceType, and rejection reason.

Comment on lines +145 to +150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed on deviceType to prevent capability-gate bypass.

This guard currently rejects only deviceType == "local". Missing/unknown values pass through and can still start project sessions. For a cloud-only tool gate, only explicit cloud should be allowed.

🔧 Proposed fix
-    device_type = spec.get("deviceType")
-    if device_type == DeviceType.LOCAL.value:
+    device_type = spec.get("deviceType")
+    if device_type != DeviceType.CLOUD.value:
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
-            detail="Local devices do not support terminal or code-server sessions",
+            detail="Only cloud devices support terminal or code-server sessions",
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/project_device_session_service.py` around lines 145 -
150, The guard currently only rejects device_type == DeviceType.LOCAL.value and
lets missing/unknown values through; change it to fail-closed by explicitly
allowing only the cloud value: read device_type = spec.get("deviceType") and if
device_type != DeviceType.CLOUD.value then raise the HTTPException (same status
and detail or update detail to indicate only cloud devices are supported).
Update the check near the current device_type handling in
project_device_session_service.py so unknown/missing values are rejected instead
of permitted.



def _get_project_path(
config: ProjectConfig,
raw_config: object,
Expand Down
44 changes: 44 additions & 0 deletions backend/tests/services/test_project_device_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,50 @@ async def test_start_project_device_session_rejects_project_without_bound_device
assert "local device" in exc_info.value.detail.lower()


@pytest.mark.asyncio
async def test_start_project_device_session_rejects_local_device_type(monkeypatch):
"""Project terminal and code-server sessions are cloud-device-only tools."""
from fastapi import HTTPException

from app.services import project_device_session_service as service

project = SimpleNamespace(
id=123,
user_id=7,
is_active=True,
config=_project_config(path="/workspace/project", device_id="local-device"),
)
device_kind = SimpleNamespace(json={"spec": {"deviceType": "local"}})
query = SimpleNamespace(filter=lambda *args: SimpleNamespace(first=lambda: project))
db = SimpleNamespace(query=lambda model: query)
execute_mock = AsyncMock()
monkeypatch.setattr(
service.local_device_session_service,
"start_session",
execute_mock,
)
monkeypatch.setattr(
service,
"device_service",
SimpleNamespace(
get_device_by_device_id=lambda db, user_id, device_id: device_kind
),
raising=False,
)

with pytest.raises(HTTPException) as exc_info:
await service.start_project_device_session(
db=db,
user_id=7,
project_id=123,
session_type="terminal",
)

assert exc_info.value.status_code == 400
assert "local devices do not support" in exc_info.value.detail.lower()
execute_mock.assert_not_awaited()


@pytest.mark.asyncio
async def test_local_device_session_service_calls_device_start_session(monkeypatch):
"""Device session service should send a start_session RPC to the online device."""
Expand Down
29 changes: 23 additions & 6 deletions docs/en/user-guide/ai-devices/local-device-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,12 @@ docker run -d --platform linux/amd64 \
wegent-device:linux-amd64
```

By default, the device image only starts `wegent-executor` and the interactive session gateway. Backend resolves the project-bound local device and project path from the project ID, then calls:
By default, the device image only starts `wegent-executor` and the interactive session gateway. Note that Wework currently exposes project connection tools only for cloud devices. Local devices can be bound to projects and execute AI tasks, but they do not support the project toolbar Terminal, IDE/code-server, or Desktop VNC/VPN entries.

- `POST /api/projects/{project_id}/terminal`: starts a writable ttyd in the project path and returns a short-token URL.
- `POST /api/projects/{project_id}/code-server`: returns a short-token code-server URL. The code-server process inside the device image runs with a fixed password, and the session gateway logs in server-side so the browser does not see the code-server login page or password.

Both entrypoints are exposed under the `/s/{session_id}/` path on `DEVICE_PUBLIC_BASE_URL`. Each terminal or code-server session has an isolated path, so a user can open multiple projects at the same time or open multiple terminal/code-server sessions for one project. Terminal sessions are created dynamically on the device and the matching ttyd process is cleaned up when the browser disconnects. Code-server is a persistent in-container process, and the gateway opens the requested project path through it. To keep the legacy fixed `8080` code-server and `7681` ttyd entrypoints, add `-e START_DEVICE_UI=1` and map those ports at container runtime.
These project session APIs are for cloud-device project connections. If the project is bound to a local device, Backend rejects terminal and code-server session startup. Cloud-device URLs include a short-lived session token and are exposed through the device-side session gateway. Each terminal or code-server session has an isolated path, so a user can open multiple projects at the same time or open multiple terminal/code-server sessions for one project. Terminal sessions are created dynamically on the device and the matching ttyd process is cleaned up when the browser disconnects. Code-server is a persistent in-container process, and the gateway opens the requested project path through it. To keep the legacy fixed `8080` code-server and `7681` ttyd entrypoints, add `-e START_DEVICE_UI=1` and map those ports at container runtime.

When a project configures `workspace.localPath` or `workspace.checkoutPath`, the device creates that directory before starting terminal or code-server.

Expand Down Expand Up @@ -241,6 +241,21 @@ You can dynamically choose execution location:

Simply change the device selection before sending each message.

### Using Local Devices in Projects

When creating a project, you can select an online or busy ClaudeCode local device. After the project is created, AI tasks execute on that local device and use the project's configured local path or checkout path.

Local devices do not support cloud connection capabilities in the project toolbar:

| Feature | Local Device Support |
|---------|----------------------|
| **Terminal** | Not supported |
| **IDE/code-server** | Not supported |
| **Desktop VNC/VPN** | Not supported |
| **CPU/MEM/Disk monitoring** | Not supported |

When a project is bound to a local device, the workspace toolbar hides Terminal, IDE, and Desktop entries and shows a local-device capability notice. Choose a cloud device when you need those connection and monitoring capabilities.

### Setting Default Device

1. Open device list in the selector
Expand All @@ -259,11 +274,13 @@ Access your devices through:
2. **Settings Page**: Go to **Settings** → **Connections** to view connectable devices
3. **API**: `GET /devices` for programmatic access

### Managing Cloud Devices
### Managing Connection Devices

The **Settings** → **Connections** page lists ClaudeCode devices that the current account can connect to, including cloud devices and local devices. It only shows devices with `bind_shell=claudecode`, grouped by cloud devices and local devices.

The **Settings** → **Connections** page lists Claude Code cloud devices that the current account can connect to. It only shows devices with `device_type=cloud` and `bind_shell=claudecode`, and displays online status, executor version, CPU, memory, and disk usage.
Cloud devices display online status, executor version, CPU, memory, and disk usage. When no cloud device exists, click **Add** to create one. After the create request returns, the page keeps a "cloud device creating" notice visible. Initialization usually takes 2-3 minutes, and the device appears in the list automatically when it comes online.

When no cloud device exists, click **Add** to create one. After the create request returns, the page keeps a "cloud device creating" notice visible. Initialization usually takes 2-3 minutes, and the device appears in the list automatically when it comes online.
Local devices display device name, online status, and executor version. They do not show CPU, MEM, or disk monitoring data or the resource monitoring note, and they do not show cloud-only actions such as Terminal, IDE, Desktop VNC/VPN, restart, or cloud-resource deletion. Offline local devices show a delete entry for removing the device registration. If the device reconnects, it automatically registers again.

Online cloud devices can open interactive sessions directly:

Expand Down Expand Up @@ -291,7 +308,7 @@ Each device shows:
| **Name** | Device hostname (e.g., "Darwin - MacBook-Pro.local") |
| **Status** | Online/Offline indicator |
| **Version** | Executor version, when available |
| **Resource Usage** | CPU, memory, and disk usage, when reported by the device |
| **Resource Usage** | CPU, memory, and disk usage for cloud devices only |
| **Slots** | Concurrent task capacity (X/5) |
| **Default** | Star indicator if set as default |

Expand Down
29 changes: 23 additions & 6 deletions docs/zh/user-guide/ai-devices/local-device-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,12 @@ docker run -d --platform linux/amd64 \
wegent-device:linux-amd64
```

设备镜像默认只启动 `wegent-executor` 和交互式 session gateway。Backend 根据项目 ID 找到项目绑定的本地设备与项目路径后,可以调用:
设备镜像默认只启动 `wegent-executor` 和交互式 session gateway。需要注意的是,Wework 当前只对云设备开放项目连接工具;本地设备可以绑定到项目并执行 AI 任务,但不支持项目工具栏中的终端、IDE/code-server、桌面 VNC/VPN 入口。

- `POST /api/projects/{project_id}/terminal`:在项目路径中启动可写 ttyd,返回带短期 token 的访问 URL。
- `POST /api/projects/{project_id}/code-server`:返回带短期 token 的 code-server 访问 URL。设备镜像内的 code-server 使用固定密码运行,session gateway 会在服务端自动登录,浏览器不会看到 code-server 登录页或固定密码。

两个入口都通过 `DEVICE_PUBLIC_BASE_URL` 下的 `/s/{session_id}/` 路径对外暴露;每个 terminal 或 code-server session 都有独立路径,因此同一用户可以同时打开多个项目,或在同一项目中打开多个 terminal/code-server。terminal 会话由设备侧动态创建,浏览器连接关闭后会销毁对应 ttyd 进程;code-server 是容器内持久进程,通过 gateway 按项目路径打开目录。若需要保留旧的固定 `8080` code-server 和 `7681` ttyd 入口,可在运行容器时添加 `-e START_DEVICE_UI=1` 并额外映射对应端口。
上述项目会话接口用于云设备项目连接;如果项目绑定的是本地设备,Backend 会拒绝启动 terminal 或 code-server 会话。云设备返回的访问地址带有短期 session token,并通过设备侧 session gateway 暴露。每个 terminal 或 code-server session 都有独立路径,因此同一用户可以同时打开多个项目,或在同一项目中打开多个 terminal/code-server。terminal 会话由设备侧动态创建,浏览器连接关闭后会销毁对应 ttyd 进程;code-server 是容器内持久进程,通过 gateway 按项目路径打开目录。若需要保留旧的固定 `8080` code-server 和 `7681` ttyd 入口,可在运行容器时添加 `-e START_DEVICE_UI=1` 并额外映射对应端口。

如果项目配置了 `workspace.localPath` 或 `workspace.checkoutPath`,设备会在启动 terminal 或 code-server 前自动创建该目录。

Expand Down Expand Up @@ -243,6 +243,21 @@ EXECUTOR_MODE=local wegent-executor

只需在发送每条消息之前更改设备选择即可。

### 项目中使用本地设备

创建项目时可以选择在线或繁忙的 ClaudeCode 本地设备。项目创建后,AI 任务会在该本地设备上执行,并使用项目配置中的本地路径或检出路径。

本地设备不支持项目工具栏中的云端连接能力:

| 功能 | 本地设备支持 |
|------|--------------|
| **终端** | 不支持 |
| **IDE/code-server** | 不支持 |
| **桌面 VNC/VPN** | 不支持 |
| **CPU/MEM/磁盘监控** | 不支持 |

如果项目绑定本地设备,工作区工具栏会隐藏终端、IDE 和桌面入口,并显示本地设备能力限制提示。需要这些连接和监控能力时,请选择云设备创建项目。

### 设置默认设备

1. 在选择器中打开设备列表
Expand All @@ -261,11 +276,13 @@ EXECUTOR_MODE=local wegent-executor
2. **设置页**:进入 **设置** → **连接** 查看可连接设备
3. **API**:`GET /devices` 用于程序化访问

### 管理云设备
### 管理连接页设备

**设置** → **连接** 页面会列出当前账号可连接的 ClaudeCode 设备,包括云设备和本地设备。页面仅展示 `bind_shell=claudecode` 的设备,并按云设备、本地设备分组。

**设置** → **连接** 页面会列出当前账号可连接的 Claude Code 云设备。页面仅展示 `device_type=cloud` 且 `bind_shell=claudecode` 的设备,并显示在线状态、executor 版本、CPU、内存和磁盘使用率。
云设备会显示在线状态、executor 版本、CPU、内存和磁盘使用率。当没有云设备时,点击 **添加** 可以创建一台新的云设备。创建请求返回后,页面会保留“云设备创建中”的提示;初始化通常需要 2-3 分钟,设备上线后会自动出现在列表中。

当没有云设备时,点击 **添加** 可以创建一台新的云设备。创建请求返回后,页面会保留“云设备创建中”的提示;初始化通常需要 2-3 分钟,设备上线后会自动出现在列表中。
本地设备会显示设备名称、在线状态和 executor 版本,但不会展示 CPU、MEM、磁盘监控数据和资源监控说明,也不会展示终端、IDE、桌面 VNC/VPN、重启或删除云资源等云设备专属操作。离线本地设备会显示删除入口,用于移除该设备的注册记录;如果设备重新连接,它会自动重新注册。

在线云设备支持直接打开交互式会话:

Expand Down Expand Up @@ -293,7 +310,7 @@ EXECUTOR_MODE=local wegent-executor
| **名称** | 设备主机名(如 "Darwin - MacBook-Pro.local") |
| **状态** | 在线/离线指示器 |
| **版本** | executor 版本(如适用) |
| **资源使用率** | CPU、内存、磁盘使用率(如设备上报) |
| **资源使用率** | CPU、内存、磁盘使用率(仅云设备) |
| **槽位** | 并发任务容量(X/5) |
| **默认** | 如果设为默认则显示星号 |

Expand Down
Loading
Loading