Skip to content

feat(wework): unify cloud runtime IPC - #1797

Merged
qdaxb merged 5 commits into
mainfrom
agent/unify-wework-runtime-ipc
Jul 8, 2026
Merged

qdaxb merged 5 commits into
mainfrom
agent/unify-wework-runtime-ipc

Conversation

@qdaxb

@qdaxb qdaxb commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Unifies Wework cloud runtime execution with the local socket protocol by carrying app IPC requests and events through backend WebSocket transport.

Changes

  • Add a backend /wework-runtime Socket.IO namespace that authenticates users and relays app IPC requests to the selected device runtime executor.
  • Forward executor runtime events through the device namespace as runtime:event without translating them into chat events.
  • Add a Wework cloud runtime IPC client and route cloud runtime work APIs through the same local runtime service shape used by local mode.
  • Update executor event forwarding and add backend/Wework coverage for the unified path.
  • Document the cloud runtime IPC flow in Chinese and English developer docs.

Validation

  • uv run pytest tests/api/ws/test_wework_runtime_namespace.py
  • uv run python -m py_compile app/api/ws/wework_runtime_namespace.py app/api/ws/device_namespace.py app/main.py
  • cargo check -p wegent-executor
  • pnpm --filter wework typecheck
  • pnpm --filter wework test -- src/api/hybrid/hybridServices.test.ts src/api/local/localServices.test.ts src/api/local/localChatStream.test.ts
  • pre-push hook: Wework ESLint/TypeScript/unit tests, backend Black/isort/syntax/settings checks, executor cargo fmt/test/clippy, Alembic multi-head check

Summary by CodeRabbit

  • New Features

    • Added cloud runtime IPC support for connecting, requesting work, and receiving runtime events.
    • Improved device and project handling with runtime identifiers, route-aware labels, and better remote project creation behavior.
    • Added support for preserving failed runtime tasks in the UI instead of dropping them.
  • Bug Fixes

    • Prevented duplicate or empty cloud/local work entries from appearing after background sync.
    • Kept device and runtime status visible during temporary sync gaps and offline scenarios.
  • Documentation

    • Expanded cloud sync and runtime connection docs in English and Chinese.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a stable runtime_instance_id propagated across backend device schemas/CRDs, executor config, and app IPC sidecar; adds a new backend /wework-runtime Socket.IO namespace relaying IPC requests to executors; and reworks frontend hybrid/local runtime services with a cloud runtime IPC client and a new cloud runtime sync/merge state model.

Changes

Backend runtime_instance_id and device schema

Layer / File(s) Summary
Schema and provider updates
backend/app/schemas/device.py, backend/app/services/device_service.py, backend/app/services/device/*.py, backend/tests/services/*
Adds runtime_instance_id/socket_device_id fields and persists/returns runtimeInstanceId in CRD upserts and device provider responses.
WebSocket device registration, matching, migration
backend/app/api/ws/device_namespace.py, backend/tests/api/ws/test_device_reconnect_storm.py
Threads runtime_instance_id through registration, cloud-device matching, and legacy migration flows, persisting it into device CRD JSON.
runtime:event relay handler
backend/app/api/ws/device_namespace.py
Adds on_runtime_event routing that normalizes payload and forwards to the Wework runtime relay room.
Wework runtime namespace and IPC relay
backend/app/api/ws/wework_runtime_namespace.py, backend/app/main.py, backend/tests/api/ws/test_wework_runtime_namespace.py, docs/*/wework-cloud-connection.md
Adds a new /wework-runtime namespace authenticating clients and relaying runtime:request to executor IPC/RPC, registered at startup, with docs and tests.
Docs links
AGENTS.md
Adds design doc links for Wework cloud sync.

Estimated code review effort: 4 (Complex) | ~60 minutes

Executor and Tauri runtime_instance_id plumbing

Layer / File(s) Summary
DeviceConfig generation
executor/src/config/device.rs, executor/tests/config_contract.rs
Adds runtime_instance_id field, env override, and generalized generate_prefixed_id helper.
App IPC sidecar and local backend wiring
executor/src/local/app_ipc.rs, executor/src/local/backend.rs, executor/src/local/backend/*.rs, executor/src/app/mod.rs, executor/tests/local_backend_*.rs
Propagates runtime_instance_id through IPC server, ready event, registration payload, and adds runtime event forwarding via broadcast channel.
Tauri local executor status
wework/src-tauri/src/local_executor.rs, wework/src/tauri/localExecutor.ts
Exposes runtimeInstanceId in local executor status parsed from ready events.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Frontend cloud runtime IPC client and device/work merging

Layer / File(s) Summary
Types
wework/src/types/api.ts, wework/src/types/devices.ts, wework/src/types/workbench.ts
Adds runtime routing and cloud sync state type definitions.
Cloud runtime IPC client
wework/src/api/backend/runtimeIpc.ts
Adds createCloudRuntimeIpcClient for request/subscribe over /wework-runtime.
Local runtime work via IPC
wework/src/api/local/localServices.ts, wework/src/api/local/localServices.test.ts
Replaces internal runtime work API with createRuntimeWorkApiFromIpc, adding device-id resolution and duplicate-workspace filtering.
Hybrid services cloud routing
wework/src/api/hybrid/hybridServices.ts, wework/src/api/hybrid/hybridServices.test.ts, wework/src/api/executorAccess.ts, wework/src/api/executorAccess.test.ts
Routes cloud runtime work/chat per-device through IPC, tracks remembered cloud devices, and renames offline executor error.
Cloud runtime sync/merge logic
wework/src/features/workbench/workbenchCloudStatus.ts, wework/src/features/workbench/workbenchCloudStatus.test.ts
Replaces device/runtime-work merge helpers with identity-aware merging and adds sync lifecycle/selectors.
Workbench refresh and messaging integration
wework/src/features/workbench/useWorkbenchDataRefresh.ts, useWorkbenchRuntimeMessaging.ts, workbenchReducer.ts, related tests
Switches to cloudRuntimeState-based refresh and preserves failed optimistic runtime tasks.
Device route UI and dialogs
wework/src/components/layout/DesktopSidebar.tsx, DesktopWorkbenchLayout.test.tsx, wework/src/components/projects/ProjectCreateDialog.tsx, StandaloneProjectDialogs.tsx, wework/src/features/cloud-connection/CloudConnectionSidebarButton.tsx, wework/src/lib/device-capabilities.ts
Adds route-based device labels, snapshot-merging in project dialog, and remote project creation eligibility filtering.

Estimated code review effort: 4 (Complex) | ~75 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: unifying Wework cloud runtime IPC.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/unify-wework-runtime-ipc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

qdaxb and others added 4 commits July 8, 2026 10:28
* feat(wework): 修复云端同步功能

* feat(wework): 修复云端同步功能

* feat(wework): 修复云端同步功能

* feat(wework): 修复云端同步功能

---------

Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>
…ntime-ipc

# Conflicts:
#	wework/src/features/workbench/WorkbenchProvider.test.tsx
#	wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts
@Micro66
Micro66 marked this pull request as ready for review July 8, 2026 10:08

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🧹 Nitpick comments (4)
wework/src/api/backend/runtimeIpc.ts (1)

53-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant wrapper function in subscribe.

runtimeHandler wraps handler without adding behavior. Pass handler directly to reduce indirection.

♻️ Proposed refactor
     async subscribe(handler: (event: LocalExecutorEvent) => void): Promise<() => void> {
       await client.ensureConnected()
-      const runtimeHandler = (event: LocalExecutorEvent) => {
-        handler(event)
-      }
-      client.socket.on(RUNTIME_EVENT, runtimeHandler)
-      return () => client.socket.off(RUNTIME_EVENT, runtimeHandler)
+      client.socket.on(RUNTIME_EVENT, handler)
+      return () => client.socket.off(RUNTIME_EVENT, handler)
     },
🤖 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 `@wework/src/api/backend/runtimeIpc.ts` around lines 53 - 60, `subscribe` in
`runtimeIpc` uses a redundant `runtimeHandler` wrapper that only forwards to
`handler` without adding behavior. Simplify this by registering `handler`
directly with `client.socket.on(RUNTIME_EVENT, ...)` and removing the extra
local function, while keeping the unsubscribe callback paired with the same
function reference.
wework/src/api/local/localServices.ts (3)

242-247: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider exporting RuntimeWorkIpcOptions.

createRuntimeWorkApiFromIpc is exported and now consumed cross-module (hybridServices.ts passes a resolveDeviceId override), but the options type itself isn't exported, so external callers can't reference it for strongly-typed option objects.

🤖 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 `@wework/src/api/local/localServices.ts` around lines 242 - 247, The
RuntimeWorkIpcOptions type used by createRuntimeWorkApiFromIpc is currently
internal, which prevents cross-module consumers like hybridServices from typing
their option objects. Export RuntimeWorkIpcOptions alongside
createRuntimeWorkApiFromIpc so external callers can reference resolveDeviceId,
normalizeDeviceRecord, and adaptListResponse with proper TypeScript support.

1410-1561: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

normalizeDeviceRecord/resolveDeviceId hooks are inconsistently applied across methods.

requestWithLocalDevice correctly funnels through the configurable normalizeDeviceRecord/resolveDeviceId (Lines 1418-1431), but several other exported methods bypass this entirely:

  • guideRuntimeTask (Lines 1546-1550) hardcodes normalizeLocalDeviceRecord instead of the configured normalizeDeviceRecord.
  • sendRuntimeMessage/rollbackRuntimeTask/createRuntimeTask call resolveDeviceId but never apply normalizeDeviceRecord at all, relying on createLocalRuntimeSendPayload/createLocalRuntimeTaskPayload instead.

Since hybridServices.ts currently only overrides resolveDeviceId (not normalizeDeviceRecord), there's no active bug today, but the abstraction is misleading: a future override of normalizeDeviceRecord for cloud routing would silently not apply to these methods.

Separately, requestWithLocalDevice calls resolveDeviceId twice — once inside normalizeRequest (Line 1423) and again directly (Line 1431) — which is redundant.

♻️ Avoid the redundant resolveDeviceId call
   const requestWithLocalDevice = async <TResponse, TRequest extends object>(
     method: string,
     data: TRequest
   ): Promise<TResponse> => {
-    const normalizedData = await normalizeRequest(data)
-    const deviceId = await resolveDeviceId(normalizedData)
+    const deviceId = await resolveDeviceId(data as unknown as Record<string, unknown>)
+    const normalizedData = normalizeDeviceRecord(data as Record<string, unknown>, deviceId) as TRequest &
+      Record<string, unknown>
🤖 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 `@wework/src/api/local/localServices.ts` around lines 1410 - 1561, The device
normalization hooks are applied inconsistently in createRuntimeWorkApiFromIpc,
and requestWithLocalDevice also resolves the device twice. Update
sendRuntimeMessage, rollbackRuntimeTask, createRuntimeTask, and guideRuntimeTask
to route through the configurable normalizeDeviceRecord and resolveDeviceId
behavior used by normalizeRequest/requestWithLocalDevice, instead of hardcoding
normalizeLocalDeviceRecord or bypassing normalization via the payload builders.
Also remove the extra resolveDeviceId call in requestWithLocalDevice by reusing
the device ID from normalizeRequest so the hook flow is consistent across all
exported methods.

Source: Learnings


1304-1408: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Label-based dedup heuristic can hide unrelated empty remote workspaces; duplicated workspace-path parsing logic.

Two concerns in adaptRuntimeWorkListResponse:

  1. The new first pass (Lines 1324-1339) collects labels from all non-remote workspaces and later (Lines 1369-1373) drops any zero-task remote workspace whose label collides with one of those. Labels default to the last path segment (workspaceLabel), so two unrelated workspaces from different hosts sharing a common folder name (e.g. project, src) would cause a legitimate, still-empty remote workspace to be silently hidden from the UI.
  2. The workspacePath derivation logic (Lines 1327-1333 and 1343-1349) is duplicated verbatim between the label-collection loop and the main loop.
♻️ Suggested extraction to remove duplication
+function resolveWorkspacePath(workspace: Record<string, unknown>): string | undefined {
+  return (
+    stringValue(workspace.workspacePath) ??
+    stringValue(workspace.workspace_path) ??
+    stringValue(workspace.projectWorkspacePath) ??
+    stringValue(workspace.project_workspace_path) ??
+    stringValue(workspace.cwd) ??
+    stringValue(workspace.path)
+  )
+}

Based on learnings, "avoid duplicate logic/copy-paste" and "extract common logic into utilities or modules".

🤖 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 `@wework/src/api/local/localServices.ts` around lines 1304 - 1408,
`adaptRuntimeWorkListResponse` is using a label-based dedup rule that can
incorrectly hide unrelated empty remote workspaces, and it repeats the same
workspace-path parsing in both loops. Extract the workspacePath resolution into
a shared helper used by both passes, then replace the `localWorkspaceLabels`
heuristic with a safer dedup key based on a more specific identifier such as
workspacePath plus source/host context (or remove the filter if that context is
unavailable) so only true duplicates are skipped.

Source: Learnings

🤖 Prompt for all review comments with 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.

Inline comments:
In `@backend/app/api/ws/wework_runtime_namespace.py`:
- Line 178: The `max_output_bytes` argument parsing in `on_runtime_request` can
throw on malformed client input, bypassing the existing
`RuntimeRpcError`/`DeviceCommandError` handling. Update the
`int(params.get("max_output_bytes") or 1024 * 1024)` logic to parse defensively
inside `wework_runtime_namespace.py`, preferably by introducing a safe helper or
inline validation before constructing the request payload, and fall back to the
default value when the input is missing or invalid. If appropriate, also enforce
an upper bound so `max_output_bytes` cannot be set excessively high.
- Around line 111-133: `on_runtime_request` needs a top-level type check before
any field extraction so non-dict payloads don’t trigger uncaught AttributeError.
Add the same `isinstance(data, dict)` guard used by `on_runtime_event` in
`wework_runtime_namespace`/`device_namespace` logic before calling
`request_id_from`, `string_field`, or `data.get`, and return a bad_request IPC
error when the payload is not an object.

In `@wework/src/api/backend/runtimeIpc.ts`:
- Around line 95-106: The ack handling in the runtime IPC callback is too
permissive and can also hide missing data. Update the success check in
runtimeIpc’s acknowledgement handler to require an explicit true value on ok
instead of treating a missing field as success, and avoid casting a missing
result to T when resolving. Use the existing formatRuntimeIpcError path for
failures, and make sure the resolve branch only returns a real result from the
ack payload so malformed or incomplete acknowledgements do not masquerade as
successful responses.

In `@wework/src/api/hybrid/hybridServices.ts`:
- Around line 480-497: The `searchRuntimeWork` cloud fan-out in
`hybridServices.ts` is bypassing the same device filtering used by
`listCloudRuntimeWork`, so it can query offline devices and duplicate results
for the same `runtime_instance_id`. Reuse the existing `isUsableDevice` plus
local-runtime deduplication logic before building the `cloudDevicesPromise`, and
apply it consistently to the `listCloudDevices()` result before calling
`cloudRuntimeApi(...).searchRuntimeWork(data)`. Keep the behavior aligned with
`listCloudRuntimeWork` so both paths query only reachable, non-duplicate cloud
devices.

---

Nitpick comments:
In `@wework/src/api/backend/runtimeIpc.ts`:
- Around line 53-60: `subscribe` in `runtimeIpc` uses a redundant
`runtimeHandler` wrapper that only forwards to `handler` without adding
behavior. Simplify this by registering `handler` directly with
`client.socket.on(RUNTIME_EVENT, ...)` and removing the extra local function,
while keeping the unsubscribe callback paired with the same function reference.

In `@wework/src/api/local/localServices.ts`:
- Around line 242-247: The RuntimeWorkIpcOptions type used by
createRuntimeWorkApiFromIpc is currently internal, which prevents cross-module
consumers like hybridServices from typing their option objects. Export
RuntimeWorkIpcOptions alongside createRuntimeWorkApiFromIpc so external callers
can reference resolveDeviceId, normalizeDeviceRecord, and adaptListResponse with
proper TypeScript support.
- Around line 1410-1561: The device normalization hooks are applied
inconsistently in createRuntimeWorkApiFromIpc, and requestWithLocalDevice also
resolves the device twice. Update sendRuntimeMessage, rollbackRuntimeTask,
createRuntimeTask, and guideRuntimeTask to route through the configurable
normalizeDeviceRecord and resolveDeviceId behavior used by
normalizeRequest/requestWithLocalDevice, instead of hardcoding
normalizeLocalDeviceRecord or bypassing normalization via the payload builders.
Also remove the extra resolveDeviceId call in requestWithLocalDevice by reusing
the device ID from normalizeRequest so the hook flow is consistent across all
exported methods.
- Around line 1304-1408: `adaptRuntimeWorkListResponse` is using a label-based
dedup rule that can incorrectly hide unrelated empty remote workspaces, and it
repeats the same workspace-path parsing in both loops. Extract the workspacePath
resolution into a shared helper used by both passes, then replace the
`localWorkspaceLabels` heuristic with a safer dedup key based on a more specific
identifier such as workspacePath plus source/host context (or remove the filter
if that context is unavailable) so only true duplicates are skipped.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f499d382-107a-40f8-833a-752e41aad52c

📥 Commits

Reviewing files that changed from the base of the PR and between be7ed8c and d00366b.

📒 Files selected for processing (53)
  • AGENTS.md
  • backend/app/api/ws/device_namespace.py
  • backend/app/api/ws/wework_runtime_namespace.py
  • backend/app/main.py
  • backend/app/schemas/device.py
  • backend/app/services/device/cloud_provider.py
  • backend/app/services/device/local_provider.py
  • backend/app/services/device/remote_provider.py
  • backend/app/services/device_service.py
  • backend/tests/api/ws/test_device_reconnect_storm.py
  • backend/tests/api/ws/test_wework_runtime_namespace.py
  • backend/tests/services/test_device_alias_preservation.py
  • backend/tests/services/test_local_device_provider.py
  • docs/en/developer-guide/wework-cloud-connection.md
  • docs/zh/developer-guide/wework-cloud-connection.md
  • executor/src/app/mod.rs
  • executor/src/config/device.rs
  • executor/src/local/app_ipc.rs
  • executor/src/local/backend.rs
  • executor/src/local/backend/client.rs
  • executor/src/local/backend/config.rs
  • executor/tests/config_contract.rs
  • executor/tests/local_backend_contract.rs
  • executor/tests/local_backend_device_migration_contract.rs
  • executor/tests/local_backend_dispatch_contract.rs
  • executor/tests/local_backend_resilience_contract.rs
  • wework/src-tauri/src/local_executor.rs
  • wework/src/api/backend/runtimeIpc.ts
  • wework/src/api/executorAccess.test.ts
  • wework/src/api/executorAccess.ts
  • wework/src/api/hybrid/hybridServices.test.ts
  • wework/src/api/hybrid/hybridServices.ts
  • wework/src/api/local/localServices.test.ts
  • wework/src/api/local/localServices.ts
  • wework/src/components/layout/DesktopSidebar.test.tsx
  • wework/src/components/layout/DesktopSidebar.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/projects/ProjectCreateDialog.test.tsx
  • wework/src/components/projects/ProjectCreateDialog.tsx
  • wework/src/components/projects/StandaloneProjectDialogs.tsx
  • wework/src/features/cloud-connection/CloudConnectionSidebarButton.tsx
  • wework/src/features/workbench/WorkbenchProvider.test.tsx
  • wework/src/features/workbench/useWorkbenchDataRefresh.ts
  • wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts
  • wework/src/features/workbench/workbenchCloudStatus.test.ts
  • wework/src/features/workbench/workbenchCloudStatus.ts
  • wework/src/features/workbench/workbenchReducer.test.ts
  • wework/src/features/workbench/workbenchReducer.ts
  • wework/src/lib/device-capabilities.ts
  • wework/src/tauri/localExecutor.ts
  • wework/src/types/api.ts
  • wework/src/types/devices.ts
  • wework/src/types/workbench.ts

Comment on lines +111 to +133
async def on_runtime_request(self, sid: str, data: dict) -> dict:
"""Relay one app IPC-style runtime request to an online executor."""

session = await self.get_session(sid)
user_id = session.get("user_id") if session else None
if not user_id:
return ipc_error(data, "unauthorized", "Not authenticated")

request_id = request_id_from(data)
method = string_field(data, "method")
device_id = string_field(data, "device_id") or string_field(data, "deviceId")
params = data.get("params")
if params is None:
params = data.get("payload", {})

if not method:
return ipc_error(data, "bad_request", "method is required", request_id)
if not device_id:
return ipc_error(data, "bad_request", "device_id is required", request_id)
if not isinstance(params, dict):
return ipc_error(
data, "bad_request", "params must be an object", request_id
)

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard data as a dict before field extraction.

on_runtime_request never validates that data is a dict. If a client sends a non-dict payload, string_field(data, "method") (and data.get("params")) call .get on a non-dict and raise AttributeError, which isn't caught by the RuntimeRpcError/DeviceCommandError handler — the client gets no error ACK. Note that on_runtime_event in device_namespace.py already performs this guard.

🛡️ Proposed guard
         session = await self.get_session(sid)
         user_id = session.get("user_id") if session else None
         if not user_id:
             return ipc_error(data, "unauthorized", "Not authenticated")
+        if not isinstance(data, dict):
+            return ipc_error(data, "bad_request", "payload must be an object")
 
         request_id = request_id_from(data)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def on_runtime_request(self, sid: str, data: dict) -> dict:
"""Relay one app IPC-style runtime request to an online executor."""
session = await self.get_session(sid)
user_id = session.get("user_id") if session else None
if not user_id:
return ipc_error(data, "unauthorized", "Not authenticated")
request_id = request_id_from(data)
method = string_field(data, "method")
device_id = string_field(data, "device_id") or string_field(data, "deviceId")
params = data.get("params")
if params is None:
params = data.get("payload", {})
if not method:
return ipc_error(data, "bad_request", "method is required", request_id)
if not device_id:
return ipc_error(data, "bad_request", "device_id is required", request_id)
if not isinstance(params, dict):
return ipc_error(
data, "bad_request", "params must be an object", request_id
)
async def on_runtime_request(self, sid: str, data: dict) -> dict:
"""Relay one app IPC-style runtime request to an online executor."""
session = await self.get_session(sid)
user_id = session.get("user_id") if session else None
if not user_id:
return ipc_error(data, "unauthorized", "Not authenticated")
if not isinstance(data, dict):
return ipc_error(data, "bad_request", "payload must be an object")
request_id = request_id_from(data)
method = string_field(data, "method")
device_id = string_field(data, "device_id") or string_field(data, "deviceId")
params = data.get("params")
if params is None:
params = data.get("payload", {})
if not method:
return ipc_error(data, "bad_request", "method is required", request_id)
if not device_id:
return ipc_error(data, "bad_request", "device_id is required", request_id)
if not isinstance(params, dict):
return ipc_error(
data, "bad_request", "params must be an object", request_id
)
🤖 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/api/ws/wework_runtime_namespace.py` around lines 111 - 133,
`on_runtime_request` needs a top-level type check before any field extraction so
non-dict payloads don’t trigger uncaught AttributeError. Add the same
`isinstance(data, dict)` guard used by `on_runtime_event` in
`wework_runtime_namespace`/`device_namespace` logic before calling
`request_id_from`, `string_field`, or `data.get`, and return a bad_request IPC
error when the payload is not an object.

args=params.get("args") if isinstance(params.get("args"), list) else [],
env=params.get("env") if isinstance(params.get("env"), dict) else {},
timeout_seconds=timeout_seconds,
max_output_bytes=int(params.get("max_output_bytes") or 1024 * 1024),

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

max_output_bytes parsing can crash on malformed client input.

int(params.get("max_output_bytes") or 1024 * 1024) raises ValueError/TypeError for client-supplied values like "abc" or a list. That exception escapes the except (RuntimeRpcError, DeviceCommandError) in on_runtime_request, so the request fails without a structured error ACK. Consider parsing defensively (and optionally clamping to an upper bound).

🛡️ Proposed defensive parse
-        return await local_device_command_service.execute_command(
+        try:
+            max_output_bytes = int(params.get("max_output_bytes") or 1024 * 1024)
+        except (TypeError, ValueError):
+            max_output_bytes = 1024 * 1024
+        return await local_device_command_service.execute_command(
             user_id=user_id,
             device_id=device_id,
             command=command.command,
             path=params.get("path") if isinstance(params.get("path"), str) else None,
             cwd=params.get("cwd") if isinstance(params.get("cwd"), str) else None,
             args=params.get("args") if isinstance(params.get("args"), list) else [],
             env=params.get("env") if isinstance(params.get("env"), dict) else {},
             timeout_seconds=timeout_seconds,
-            max_output_bytes=int(params.get("max_output_bytes") or 1024 * 1024),
+            max_output_bytes=max_output_bytes,
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
max_output_bytes=int(params.get("max_output_bytes") or 1024 * 1024),
try:
max_output_bytes = int(params.get("max_output_bytes") or 1024 * 1024)
except (TypeError, ValueError):
max_output_bytes = 1024 * 1024
return await local_device_command_service.execute_command(
user_id=user_id,
device_id=device_id,
command=command.command,
path=params.get("path") if isinstance(params.get("path"), str) else None,
cwd=params.get("cwd") if isinstance(params.get("cwd"), str) else None,
args=params.get("args") if isinstance(params.get("args"), list) else [],
env=params.get("env") if isinstance(params.get("env"), dict) else {},
timeout_seconds=timeout_seconds,
max_output_bytes=max_output_bytes,
)
🤖 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/api/ws/wework_runtime_namespace.py` at line 178, The
`max_output_bytes` argument parsing in `on_runtime_request` can throw on
malformed client input, bypassing the existing
`RuntimeRpcError`/`DeviceCommandError` handling. Update the
`int(params.get("max_output_bytes") or 1024 * 1024)` logic to parse defensively
inside `wework_runtime_namespace.py`, preferably by introducing a safe helper or
inline validation before constructing the request payload, and fall back to the
default value when the input is missing or invalid. If appropriate, also enforce
an upper bound so `max_output_bytes` cannot be set excessively high.

Comment on lines +95 to +106
(ack: RuntimeIpcAck<T> | undefined) => {
window.clearTimeout(timeout)
if (!ack) {
reject(new Error(`${method} returned an empty acknowledgement`))
return
}
if (ack.ok === false || ack.error) {
reject(new Error(formatRuntimeIpcError(ack)))
return
}
resolve((ack.result ?? null) as T)
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unsafe null cast and permissive ack success check.

Two related issues in the ack handling:

  1. ack.ok === false treats a missing ok field as success. An empty or malformed ack ({}) resolves as a successful response. Use ack.ok !== true to require explicit success confirmation.

  2. resolve((ack.result ?? null) as T) silently casts null to T. If the server returns { ok: true } without a result, callers receive null at runtime while TypeScript believes it's T, causing downstream null-pointer errors.

🔧 Proposed fix for ack handling
       (ack: RuntimeIpcAck<T> | undefined) => {
         window.clearTimeout(timeout)
         if (!ack) {
           reject(new Error(`${method} returned an empty acknowledgement`))
           return
         }
-        if (ack.ok === false || ack.error) {
+        if (ack.ok !== true || ack.error) {
           reject(new Error(formatRuntimeIpcError(ack)))
           return
         }
-        resolve((ack.result ?? null) as T)
+        if (ack.result === undefined || ack.result === null) {
+          reject(new Error(`${method} returned no result`))
+          return
+        }
+        resolve(ack.result as T)
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(ack: RuntimeIpcAck<T> | undefined) => {
window.clearTimeout(timeout)
if (!ack) {
reject(new Error(`${method} returned an empty acknowledgement`))
return
}
if (ack.ok === false || ack.error) {
reject(new Error(formatRuntimeIpcError(ack)))
return
}
resolve((ack.result ?? null) as T)
}
(ack: RuntimeIpcAck<T> | undefined) => {
window.clearTimeout(timeout)
if (!ack) {
reject(new Error(`${method} returned an empty acknowledgement`))
return
}
if (ack.ok !== true || ack.error) {
reject(new Error(formatRuntimeIpcError(ack)))
return
}
if (ack.result === undefined || ack.result === null) {
reject(new Error(`${method} returned no result`))
return
}
resolve(ack.result as T)
}
🤖 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 `@wework/src/api/backend/runtimeIpc.ts` around lines 95 - 106, The ack handling
in the runtime IPC callback is too permissive and can also hide missing data.
Update the success check in runtimeIpc’s acknowledgement handler to require an
explicit true value on ok instead of treating a missing field as success, and
avoid casting a missing result to T when resolving. Use the existing
formatRuntimeIpcError path for failures, and make sure the resolve branch only
returns a real result from the ack payload so malformed or incomplete
acknowledgements do not masquerade as successful responses.

Comment on lines +480 to +497
const cloudDevicesPromise = listCloudDevices()
const [localResult, cloudDevicesResult] = await Promise.allSettled([
localServices.runtimeWorkApi!.searchRuntimeWork(data),
cloudServices.runtimeWorkApi!.searchRuntimeWork(data),
cloudDevicesPromise.then(devices =>
Promise.allSettled(
devices.map(device => cloudRuntimeApi(device.device_id).searchRuntimeWork(data))
)
),
])
const cloudItems =
cloudDevicesResult.status === 'fulfilled'
? cloudDevicesResult.value.flatMap(result =>
result.status === 'fulfilled' ? result.value.items : []
)
: []
return mergeSearchResults(
fulfilledValue(localResult, { items: [] }),
fulfilledValue(cloudResult, { items: [] }),
{ items: cloudItems },

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

searchRuntimeWork's cloud fan-out is missing the same filtering listCloudRuntimeWork applies.

listCloudRuntimeWork (Lines 389-393) filters cloud devices with isUsableDevice and excludes devices sharing a runtime_instance_id with a local device before querying them. searchRuntimeWork queries listCloudDevices() unfiltered (Line 480, 485), so it can issue requests to offline devices and produce duplicate results for a device reachable via two routes (local + cloud-relay to the same runtime instance).

🐛 Suggested fix — reuse the same filter
     async searchRuntimeWork(data: RuntimeWorkSearchRequest) {
-      const cloudDevicesPromise = listCloudDevices()
+      const cloudDevicesPromise = listCloudDevices().then(devices =>
+        devices.filter(
+          device =>
+            isUsableDevice(device) &&
+            !(device.runtime_instance_id && localRuntimeInstanceIds.has(device.runtime_instance_id))
+        )
+      )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cloudDevicesPromise = listCloudDevices()
const [localResult, cloudDevicesResult] = await Promise.allSettled([
localServices.runtimeWorkApi!.searchRuntimeWork(data),
cloudServices.runtimeWorkApi!.searchRuntimeWork(data),
cloudDevicesPromise.then(devices =>
Promise.allSettled(
devices.map(device => cloudRuntimeApi(device.device_id).searchRuntimeWork(data))
)
),
])
const cloudItems =
cloudDevicesResult.status === 'fulfilled'
? cloudDevicesResult.value.flatMap(result =>
result.status === 'fulfilled' ? result.value.items : []
)
: []
return mergeSearchResults(
fulfilledValue(localResult, { items: [] }),
fulfilledValue(cloudResult, { items: [] }),
{ items: cloudItems },
const cloudDevicesPromise = listCloudDevices().then(devices =>
devices.filter(
device =>
isUsableDevice(device) &&
!(device.runtime_instance_id && localRuntimeInstanceIds.has(device.runtime_instance_id))
)
)
const [localResult, cloudDevicesResult] = await Promise.allSettled([
localServices.runtimeWorkApi!.searchRuntimeWork(data),
cloudDevicesPromise.then(devices =>
Promise.allSettled(
devices.map(device => cloudRuntimeApi(device.device_id).searchRuntimeWork(data))
)
),
])
const cloudItems =
cloudDevicesResult.status === 'fulfilled'
? cloudDevicesResult.value.flatMap(result =>
result.status === 'fulfilled' ? result.value.items : []
)
: []
return mergeSearchResults(
fulfilledValue(localResult, { items: [] }),
{ items: cloudItems },
🤖 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 `@wework/src/api/hybrid/hybridServices.ts` around lines 480 - 497, The
`searchRuntimeWork` cloud fan-out in `hybridServices.ts` is bypassing the same
device filtering used by `listCloudRuntimeWork`, so it can query offline devices
and duplicate results for the same `runtime_instance_id`. Reuse the existing
`isUsableDevice` plus local-runtime deduplication logic before building the
`cloudDevicesPromise`, and apply it consistently to the `listCloudDevices()`
result before calling `cloudRuntimeApi(...).searchRuntimeWork(data)`. Keep the
behavior aligned with `listCloudRuntimeWork` so both paths query only reachable,
non-duplicate cloud devices.

@qdaxb
qdaxb merged commit a9caa96 into main Jul 8, 2026
29 checks passed
@qdaxb
qdaxb deleted the agent/unify-wework-runtime-ipc branch July 8, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants