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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ Wegent is an open-source AI-native operating system for defining, organizing, an
- English version: [`docs/en/developer-guide/runtime-local-work.md`](docs/en/developer-guide/runtime-local-work.md)
- Wework local-first cloud connection: [`docs/zh/developer-guide/wework-cloud-connection.md`](docs/zh/developer-guide/wework-cloud-connection.md)
- English version: [`docs/en/developer-guide/wework-cloud-connection.md`](docs/en/developer-guide/wework-cloud-connection.md)
- Wework cloud sync architecture design: [`docs/zh/developer-guide/wework-cloud-sync-design.md`](docs/zh/developer-guide/wework-cloud-sync-design.md)
- English version: [`docs/en/developer-guide/wework-cloud-sync-design.md`](docs/en/developer-guide/wework-cloud-sync-design.md)
- Wework cloud sync development plan: [`docs/zh/developer-guide/wework-cloud-sync-development.md`](docs/zh/developer-guide/wework-cloud-sync-development.md)
- English version: [`docs/en/developer-guide/wework-cloud-sync-development.md`](docs/en/developer-guide/wework-cloud-sync-development.md)
- Wework performance diagnostics: [`docs/zh/developer-guide/wework-performance-diagnostics.md`](docs/zh/developer-guide/wework-performance-diagnostics.md)
- English version: [`docs/en/developer-guide/wework-performance-diagnostics.md`](docs/en/developer-guide/wework-performance-diagnostics.md)
- Wework E2E automation: [`docs/zh/developer-guide/wework-e2e-automation.md`](docs/zh/developer-guide/wework-e2e-automation.md)
Expand Down
74 changes: 70 additions & 4 deletions backend/app/api/ws/device_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
LocalTaskResponsesHandler,
emit_response_api_event,
)
from app.api.ws.wework_runtime_namespace import (
WEWORK_RUNTIME_EVENT,
WEWORK_RUNTIME_NAMESPACE,
wework_runtime_user_room,
)
from app.core.auth_utils import is_api_key, verify_api_key
from app.core.constants import get_wework_task_room, get_wework_user_room
from app.core.events import TaskCompletedEvent, get_event_bus
Expand Down Expand Up @@ -217,6 +222,7 @@ def _register_device(
device_type: Optional[str] = None,
bind_shell: Optional[str] = None,
runtime_transfer_host: Optional[str] = None,
runtime_instance_id: Optional[str] = None,
app_device_id: Optional[str] = None,
) -> tuple[bool, Optional[str], Optional[str]]:
"""
Expand All @@ -230,6 +236,7 @@ def _register_device(
device_type: Device type ('local', 'app', 'cloud', or 'remote')
bind_shell: Shell runtime binding ('claudecode' or 'openclaw')
runtime_transfer_host: Host peers should use for direct transfers
runtime_instance_id: Stable runtime installation ID shared by all routes
app_device_id: Desktop app IPC device ID for app registrations

Returns (success, persisted_display_name, error_message).
Expand All @@ -245,6 +252,7 @@ def _register_device(
device_type=device_type,
bind_shell=bind_shell,
runtime_transfer_host=runtime_transfer_host,
runtime_instance_id=runtime_instance_id,
app_device_id=app_device_id,
)
persisted_display_name = (
Expand Down Expand Up @@ -295,7 +303,10 @@ def _update_device_status(user_id: int, device_id: str, status: str) -> None:


def _match_cloud_device_sync(
user_id: int, client_ip: str, executor_device_id: str
user_id: int,
client_ip: str,
executor_device_id: str,
runtime_instance_id: Optional[str] = None,
) -> Optional[tuple[str, bool, Optional[dict]]]:
"""
Synchronous helper to match cloud device by device_id.
Expand All @@ -306,7 +317,10 @@ def _match_cloud_device_sync(
- needs_migration: True if legacy device needs migration
- device_data: Dict with device info for migration (device_id, etc.)
"""
import copy

from sqlalchemy import and_
from sqlalchemy.orm.attributes import flag_modified

from app.models.kind import Kind
from app.schemas.device import DeviceType
Expand Down Expand Up @@ -344,6 +358,15 @@ def _match_cloud_device_sync(
# New logic: verify server-generated device_id matches
if server_device_id:
if server_device_id == executor_device_id:
if runtime_instance_id:
device_json = copy.deepcopy(device.json)
device_json.setdefault("spec", {})[
"runtimeInstanceId"
] = runtime_instance_id
device.json = device_json
flag_modified(device, "json")
db.add(device)
db.commit()
logger.info(
f"[Device WS] Cloud device matched by device_id: "
f"sandbox_id={sandbox_id}, "
Expand Down Expand Up @@ -381,6 +404,7 @@ def _update_cloud_device_id_sync(
device_db_id: int,
executor_device_id: str,
sandbox_id: str,
runtime_instance_id: Optional[str] = None,
) -> str:
"""
Synchronous helper to update cloud device ID in CRD for backward compatibility.
Expand Down Expand Up @@ -412,6 +436,8 @@ def _update_cloud_device_id_sync(
old_device_id = device.name
device_json["metadata"]["name"] = executor_device_id
device_json["spec"]["deviceId"] = executor_device_id
if runtime_instance_id:
device_json["spec"]["runtimeInstanceId"] = runtime_instance_id

# Update cloudConfig with deviceId for future matching
if "cloudConfig" in device_json["spec"]:
Expand Down Expand Up @@ -584,6 +610,7 @@ def __init__(self, namespace: str = "/local-executor"):
"device:heartbeat": "on_device_heartbeat",
"device:status": "on_device_status",
"device:upgrade_status": "on_device_upgrade_status",
"runtime:event": "on_runtime_event",
"runtime.tasks.updated": "on_runtime_task_updated",
"terminal:output": "on_terminal_output",
"terminal:exit": "on_terminal_exit",
Expand Down Expand Up @@ -747,7 +774,11 @@ def _get_client_ip_log_context(self, environ: dict) -> tuple[Optional[str], str]
)

async def _match_cloud_device(
self, user_id: int, client_ip: str, executor_device_id: str
self,
user_id: int,
client_ip: str,
executor_device_id: str,
runtime_instance_id: Optional[str] = None,
) -> Optional[str]:
"""Match cloud device by verifying server-generated device_id.

Expand All @@ -769,7 +800,11 @@ async def _match_cloud_device(
try:
# Run database query in executor to avoid blocking event loop
result = await run_sync_in_executor(
_match_cloud_device_sync, user_id, client_ip, executor_device_id
_match_cloud_device_sync,
user_id,
client_ip,
executor_device_id,
runtime_instance_id,
)

if result is None:
Expand All @@ -785,6 +820,7 @@ async def _match_cloud_device(
device_data["device_id"],
executor_device_id,
sandbox_id,
runtime_instance_id,
)

return sandbox_id
Expand Down Expand Up @@ -1049,7 +1085,7 @@ async def on_device_register(self, sid: str, data: dict) -> dict:
is_cloud_device = False
if payload.device_type == DeviceType.CLOUD and client_ip:
cloud_device_id = await self._match_cloud_device(
user_id, client_ip, payload.device_id
user_id, client_ip, payload.device_id, payload.runtime_instance_id
)
if cloud_device_id:
is_cloud_device = True
Expand All @@ -1075,6 +1111,7 @@ async def on_device_register(self, sid: str, data: dict) -> dict:
payload.device_type.value,
payload.bind_shell.value,
runtime_transfer_host,
payload.runtime_instance_id,
payload.app_device_id,
)
if not success:
Expand Down Expand Up @@ -1759,6 +1796,35 @@ async def on_runtime_task_updated(self, sid: str, data: dict) -> dict:
)
return {"success": True, "notified": notified}

async def on_runtime_event(self, sid: str, data: dict) -> dict:
"""Forward native runtime app IPC events to Wework relay subscribers."""

session = await self.get_session(sid)
user_id = session.get("user_id") if session else None
device_id = str(session.get("device_id") or "") if session else ""
if not user_id or not device_id:
return {"error": "Device not authenticated"}
if not isinstance(data, dict):
return {"error": "Invalid runtime event payload"}

payload = dict(data)
nested_payload = payload.get("payload")
if isinstance(nested_payload, dict):
nested_payload = dict(nested_payload)
nested_payload.setdefault("deviceId", device_id)
nested_payload.setdefault("device_id", device_id)
payload["payload"] = nested_payload
else:
payload["payload"] = {"deviceId": device_id, "device_id": device_id}

await get_sio().emit(
WEWORK_RUNTIME_EVENT,
payload,
room=wework_runtime_user_room(int(user_id)),
namespace=WEWORK_RUNTIME_NAMESPACE,
)
return {"success": True}

async def _publish_task_completed_event(
self,
task_id: int,
Expand Down
Loading
Loading