Skip to main content

Local Device Architecture

This document describes the technical architecture of local device support, including communication protocols, heartbeat mechanisms, and security design.


πŸ— Architecture Overview​

System Components​

Wework Packaged App Local-First Channel​

The packaged Wework Tauri App defaults to local-first mode. This mode does not start the frontend Node dev server and does not start an extra local HTTP Backend service. The React UI runs inside the Tauri WebView, while the Tauri Rust side is only the app's internal command layer.

Local-first mode needs only two local processes:

Tauri starts the executor sidecar with no arguments and exchanges newline-delimited JSON through the child process stdin/stdout. Stdout carries protocol responses and events only; diagnostics go to stderr and ~/.wegent-executor/logs/executor.log. Sidecars started by the App are owned by the Tauri process: on macOS/Linux they run in an isolated process group, and App close or restart sends SIGTERM before using SIGKILL for remaining child processes. The dev-mode reload supervisor and the executor it launches are included in that cleanup scope. The Wework renderer sends runtime.* and device.execute_command requests through Tauri commands and subscribes to Responses stream events emitted by the sidecar.

The parent-child relationship defines the stdio lifecycle directly: a write failure, stdout EOF, or child exit marks local IPC unavailable. A normal request timeout ends only that request and does not tear down the transport, so system sleep or scheduling delays cannot trigger endpoint reconnection or attach the App to another executor.

Backend connectivity is optional, not a required dependency for the local app. When login, model/capability sync, cloud projects, or web control of the local computer are needed, the executor can register as a local device over the Backend Socket.IO channel. The same executor sidecar reuses one command handler and one runtime work handler while serving Wework App over stdio and Backend over Socket.IO. This design does not introduce a local HTTP gateway and does not require Wework App to start Backend itself.

Executor Startup Environment and Codex Home Initialization​

Before creating its asynchronous runtime or starting Agent child processes, a Unix executor runs the current user's interactive login shell to read the complete environment. It prefers the login shell from the system user database and falls back through $SHELL, zsh, bash, and sh. Environment capture has a fixed timeout. On failure, the executor keeps its parent environment and still appends standard developer locations such as Homebrew and /usr/local. The executor then passes the resulting environment consistently to Codex, Claude Code, plugins, skills, hooks, PTYs, and device commands, so Wework local sidecars, standalone local devices, and Linux cloud or remote devices share the same PATH resolution behavior.

Wework uses an isolated Codex Home for local runtime configuration. During first-run initialization, users can copy configuration, plugins, skills, and plugin marketplace data from the native Codex Home. After initialization, Wework writes apps = true under [features] by default so migrated plugin Apps are immediately available. If a user explicitly disables Apps later in Settings, ordinary subsequent startups preserve that choice.

Wework considers the local runtime usable only after the real Codex app-server completes initialize, not merely when the executor stdio transport is connected. After Tauri starts the executor, it first applies the current local proxy configuration and then starts and initializes the shared Codex app-server through runtime.codex.ensure_started; the renderer proceeds to the interactive workbench only after that call succeeds. The Codex initialization path must not synchronously wait for plugin marketplace refreshes, Git fetches, update checks, or other external network requests. Those background requests must not delay the initialize response even when the network is unavailable or a proxy never responds. Startup E2E coverage must verify this boundary with the real Codex binary and a blocking network proxy, while also confirming that no Agent model request is sent during initialization.

Runtime Task and Goal State​

The runtime task running field represents only whether a model turn is currently executing. After a turn completes, fails, or is cancelled, the executor must settle that field to false. Wework uses it to decide whether to render the stop control and running indicator, and whether a new message can be sent directly.

The active-task set owned by the current executor process is the only authority for running. Codex thread lists, transcripts, persisted task summaries, and Wework reminder state provide content or historical metadata only; they must not independently infer that a task is still executing. The task list, detail pane, system tray, and sleep-inhibition logic must consume the same executor-provided running value. After an executor restart, a thread absent from the new process's active set is idle even if Codex metadata briefly remains active or inProgress.

Task summaries also expose Codex threadStatus (notLoaded, idle, systemError, or active) and turnStatus (inProgress, completed, interrupted, or failed) without conflating their lifecycles. The separate continuable field means that the conversation is not archived and can accept another message; it must not be used to infer that a turn is running. Wework renders running feedback only from the explicit running field and real turn state, and does not convert an active thread or message status into streaming.

Refreshing thread metadata must not overwrite a persisted terminal task state. When a Codex thread becomes idle, the executor preserves local done, cancelled, or failed; only a genuinely active turn can move the task back to running. A normally completed conversation that remains available for follow-up therefore has status=done, running=false, continuable=true, threadStatus=idle, and turnStatus=completed.

Goals have an independent lifecycle. An active goal means that its objective can continue in later turns; it does not mean that a model turn is currently executing. Keeping an active goal while a task is idle must not mark the task as running again. A user's next message creates a new turn directly instead of being sent as guidance to an in-progress turn.

The Wework frontend manages every task lifecycle through one user-scoped RuntimeTaskLifecycleStore. The Store owns one state machine per task and routes events to it. The state machine is the aggregate root for execution, turn, Goal, and unread state; its reducer is only an internal transition implementation. The React Provider adapts that same Store for subscription and neither stores nor infers execution state. The task list, composer, message thinking feedback, system tray, close guard, and completion reminders all read the same Store snapshot.

Frontend execution state is memory-only and is never written to a local file or browser storage. The optimistic starting state created when a user sends a message is owned by the same state machine and converges when the executor explicitly reports running=true or running=false. During automatic continuation of an active Goal, the task remains visibly running between turns while the executor keeps it in the current process's active set, but the turn state is idle, so Wework shows no thinking indicator and creates no unread marker. After Wework or the executor restarts, running=false from the new process makes the task idle even if the Goal remains active, and execution does not resume automatically. Only unread completion reminders are persisted, and they can never be used to infer execution state.

Codex guidance is sent to the active turn through the shared app-server. If that turn finishes or changes while guidance is being sent, the executor reports the race as no_active_turn; Wework then sends the same content as a normal follow-up message so user input is preserved without a misleading send failure.

A conversation can switch models and providers between turns. Wework sends the selected model and provider configuration with every continuation, and Codex app-server applies the new modelProvider during thread/resume. For tasks routed through the Wework router, the executor assigns one stable local model-proxy URL per task and atomically updates its upstream configuration at the start of every turn. After the root thread is created, the proxy binds its thread ID and accepts requests only from that thread or its child threads. The upstream and model supplied by the executor for the current turn are the sole routing authority.

OpenAI reasoning and remote-compaction items can contain encrypted_content that is valid only for the provider that produced it. Before continuing, runtime work compares the previous modelSelection in the task summary with the requested selection and passes a switch marker to the executor when the model or model type changed. On the first target request through the Wework router, the proxy removes provider-bound encrypted reasoning or compaction items and previous_response_id, then consumes the marker only after a successful request. This also covers a compaction request that occurs immediately after switching and temporarily omits the <model_switch> marker, preventing stale ciphertext from reaching OpenAI Responses, Chat Completions, or Anthropic Messages upstreams.

After an explicit failure, β€œswitch model and retry” starts one new turn. It preserves the original task and portable thread context and sends exactly one request to the newly selected upstream. The executor also writes the turn's modelSelection back to the task summary so the model shown after a refresh matches the model that handled the request. Guidance sent while a turn is running remains part of that turn and does not switch models; the new model applies only to a normal new turn or one created by interrupt-and-send.

The local model proxy uses the Codex Responses protocol as its internal canonical representation and converts bidirectionally among OpenAI Responses, OpenAI Chat Completions, and Anthropic Messages upstream protocols. When the protocol changes, tool-call IDs and tool-result references in history must be normalized at the request boundary to stable IDs containing only letters, numbers, underscores, or dashes, while preserving a one-to-one mapping within that history. Raw provider IDs must not be forwarded directly into another protocol. Tool-call IDs returned by streaming responses follow the same normalization rule so later tool results still reference the original call and item/started and item/completed settle the same Wework tool block.

Before sending a user message, Wework generates a stable clientUserMessageId and renders an optimistic local message. The ID travels unchanged through the runtime create/send request to Codex app-server's turn/start.clientUserMessageId. When the Codex transcript returns the user message, the executor preserves the same clientUserMessageId, which Wework uses to reconcile it with the optimistic message. The Codex provider item ID remains the provider-event identity, but it cannot replace the client user message ID; otherwise transcript pagination or refresh can interpret one send as two messages.

Tool state follows app-server lifecycle events: item/started creates a running tool block, while item/completed must settle the matching block to done unless the item explicitly failed. Some standalone tool items, including image view, sleep, and web search, do not carry a status field. The executor normalizes these terminal items to done in both live event mapping and transcript restoration so Wework does not keep showing a running state or advancing timer after the tool completes.

A Codex turn may interleave reasoning, assistant text, and tool calls. The executor must track streaming offsets and completed snapshots for each assistant text segment by provider item ID. A delta and completed event for the same item represent an incremental stream and its snapshot and must be deduplicated. Completed text from a different item must still be forwarded as subsequent text even when it occurs in the same turn; it cannot be discarded merely because an earlier item emitted deltas. Before Wework moves current assistant text into a tool or processing block, it clears that text stream's offset state so the next assistant segment after the tool starts at offset 0 and preserves transcript event order.

Assistant text always enters Wework as process text while it streams. A phase carried by item/started is provisional: Codex can start an item as final_answer and complete that same item as commentary after more tools run. The executor therefore waits for completed items and the successful turn boundary before committing final content, so the UI never has to demote visible final content back into a process block. A completed explicit final or final_answer item wins; if the turn has no explicit final item, the latest completed assistant text becomes the fallback final result.

Reasoning summaries supplied by Codex enter Wework as thinking processing blocks. A streaming summary appears as a single β€œThinking Β· summary” row and reports only the currently active reasoning progress. After the turn completes, fails, or is cancelled, Wework removes the thinking block instead of retaining a summary placeholder or detail in message history. The executor must map both reasoning deltas and item/completed notifications that carry only the complete summary; otherwise a long reasoning phase degrades to a generic waiting state with no visible progress. Internal reasoning that the provider does not include in its summary is not displayed.

Backend Device Chat Task REST Entrypoint​

The web device chat page still sends messages through WebSocket. For external systems or curl-based callers that need to create the same kind of task, Backend exposes a REST entrypoint:

POST /api/device-chat/tasks

This entrypoint writes central TaskResource and Subtask rows, and reuses the same create_chat_task, device resolution, and trigger_ai_response_unified path as the device chat page. The request does not include workspacePath or localTaskId: regular device chat tasks do not have a project workspace concept. If projectId is provided, Backend resolves the target device from the Project config. If projectId is omitted, Backend resolves the target from explicit deviceId, the existing task device, Wework defaults, or the user's default local device.

Creating a task only requires teamId and message; callers may also send deviceId, projectId, model options, and context fields. Continuing a task sends taskId; Backend verifies that the current user can access the task and then reuses the existing task's client_origin. The response returns the central task and message identifiers:

{
"taskId": 2267,
"userSubtaskId": 3332,
"assistantSubtaskId": 3333,
"messageId": 5,
"aiTriggered": true,
"deviceId": "device-de8f474294621dd5acfd1287",
"chatUrl": "/devices/chat?taskId=2267"
}

The OpenAPI schema is generated automatically by FastAPI from DeviceChatTaskRequest and DeviceChatTaskResponse; no static docs/api file needs to be maintained.

Communication Architecture​

The following diagram shows how local devices communicate with the Wegent system:

Device Types​

Device CRDs use spec.deviceType to separate lifecycle ownership and frontend capabilities:

TypeLifecycle ownerConnectionTypical entrypoint
localUser's local executorWebSocketLocal installer or manually started executor
cloudWegent cloud device serviceWebSocketCloud device create, restart, and release flows
remoteUser-managed Docker container or remote hostWebSocketRemote Docker command generated from Wework connection settings

remote devices reuse the local executor WebSocket registration, heartbeat, task execution, and command RPC channels, but RemoteDeviceProvider lists them separately and returns remoteConfig. Backend does not persist the WEGENT_AUTH_TOKEN contained in the generated command; the Device CRD stores only non-sensitive metadata such as provider, image, deviceId, deviceName, backendUrl, publicBaseUrl, and createdAt.

After a remote Docker device starts, it sends device:register with device_type=remote, which updates the matching Device CRD. Online state still uses the Redis device-online key, so task routing, slot accounting, and terminal/code-server session RPC use the same protocol as local devices. The frontend does not expose cloud lifecycle actions for remote devices; users stop, restart, or remove the container on the Docker host.


πŸ“‘ WebSocket Protocol​

Event Types​

EventDirectionDescription
device:registerDevice β†’ BackendDevice registration
device:heartbeatDevice β†’ BackendHeartbeat keepalive
task:executeBackend β†’ DeviceTask dispatch
task:progressDevice β†’ BackendTask progress
task:completeDevice β†’ BackendTask completion

Rust Executor Local Event Coverage​

The Rust executor Backend channel must remain event-compatible with the legacy Python local device runner. In addition to task execution and heartbeat events, the local device currently registers and handles:

  • task:cancel, task:close-session
  • chat:message
  • device:execute_command
  • device:sync_capabilities
  • device:start_terminal_session, device:start_code_server_session
  • terminal:input, terminal:resize, terminal:close
  • runtime:rpc
  • device:upgrade
  • device:run_extension

The extension_scope field of device:run_extension accepts task or global and defaults to task when omitted. Task-scoped extensions run from the current task's .claude/skills/<extension> directory; global extensions run from ~/.claude/skills/<extension> for the user running the executor. The script path must remain inside the selected extension directory, and other scope values are rejected.

The migration coverage matrix is tracked in executor/docs/LOCAL_DEVICE_PYTHON_MIGRATION_TESTS.md. When adding a local device event, add coverage to executor/tests/local_backend_device_migration_contract.rs first, then update that migration matrix.

Message Format​

// device:register
{
"event": "device:register",
"data": {
"device_id": "uuid-xxx",
"name": "Darwin - MacBook-Pro.local",
"max_slots": 5
}
}

// device:heartbeat
{
"event": "device:heartbeat",
"data": {
"device_id": "uuid-xxx",
"running_task_ids": ["task-1", "task-2"]
}
}

// task:execute
{
"event": "task:execute",
"data": {
"subtask_id": "subtask-xxx",
"prompt": "User message",
"context": {}
}
}

πŸ’“ Heartbeat Mechanism​

Sequence Diagram​

Timing Parameters​

ParameterValueDescription
Heartbeat Interval30 secondsDevice sends heartbeat
Online TTL90 secondsRedis key expiration
Monitor Interval60 secondsBackend checks expired devices
Offline Threshold3 missed heartbeatsDevice marked as offline

If one device:heartbeat ACK times out, is rejected by Backend, or hits a Socket.IO transport error, the executor quickly retries the next heartbeat after 10 seconds instead of waiting for the full 30-second heartbeat interval. After two consecutive heartbeat failures, the executor proactively disconnects the current socket and enters the reconnect-and-register flow. This tolerates one transient hiccup while still normally letting the device re-register and refresh online state before the 90-second online TTL expires after a short network interruption recovers.

Running Task Tracking​

Each heartbeat contains currently running task IDs, used for:

  • Real-time slot usage tracking
  • Orphaned task detection
  • Automatic cleanup on disconnection

Global Capability Reporting​

Local devices also report Claude Code global capability state through heartbeats. A full report includes:

  • capabilities.revision: local Wegent-managed manifest revision
  • capabilities.digest: content digest for skills, plugins, and mcps
  • capabilities.skills: Skills available under ~/.claude/skills
  • capabilities.plugins: Plugins installed in ~/.claude/plugins/installed_plugins.json
  • capabilities.mcps: Wegent-managed global MCP configuration

Plugin reports must include the Skills contained inside each plugin. The executor scans SKILL.md files under each plugin install directory and returns them in plugins[].skills[]:

{
"name": "context7",
"marketplace": "claude-plugins-official",
"version": "1057d02c5307",
"source": "wegent",
"installed_plugin_id": 301,
"skills": [
{
"name": "context7",
"description": "Look up version-specific documentation.",
"path": "skills/context7"
}
]
}

Backend persists the complete capability state only when capabilities.full = true. Later heartbeats with the same digest refresh device liveness without rewriting the full capability lists.

Global Capability Sync​

Backend can send desired global capability state to an online local device through device:sync_capabilities. The sync payload currently includes:

  • skills: backend-resolved InstalledSkill / Skill entries, downloaded by the executor into ~/.claude/skills
  • plugins: backend-resolved InstalledPlugin entries, written by the executor into ~/.claude/plugins/installed_plugins.json
  • mcps: backend-resolved InstalledMCP entries, written into the Wegent-managed manifest

In replace mode, the executor only removes capabilities marked as managed in the Wegent manifest and missing from the desired state. Plugins installed directly by the user on the local machine are not removed by a Wegent sync.

Capability package downloads are constrained to the configured Backend origin. The executor resolves relative package paths against connection.backend_url, rejects package URLs from other origins, and only attaches the device bearer token to same-origin Backend requests. Skill download URLs are built with encoded query parameters, and package extraction uses a per-sync staging directory before replacing the managed skill directory.

When a project task runs through the local executor, its task-level CLAUDE_CONFIG_DIR exposes both global skills and plugins directories and inherits non-sensitive plugin settings such as enabledPlugins and extraKnownMarketplaces from the local ~/.claude/settings.json. This lets Claude Code load global Skills and Skills provided by Plugins. Sensitive model and token configuration is still injected through runtime environment variables and is not copied from global settings into the task directory.

Claude Code, Agno, and Codex task shells receive a task identity environment set. WEGENT_TASK_ID identifies the current Task, AUTH_TOKEN provides the per-turn bearer token for Backend API access, WEGENT_RUNTIME_AUTH_TOKEN provides the bearer token that local Skills use to access Wegent runtime APIs, and WEGENT_SKILL_IDENTITY_TOKEN plus WEGENT_SKILL_USER_NAME identify task-scoped Skill operations. Claude Code and Agno receive the values through their child-process environments. Codex receives them through thread-scoped shell_environment_policy.set.* settings, so task identity never enters the shared app-server process environment and cannot leak across tasks. After Wework connects to cloud, it issues the runtime token through POST /api/users/me/wegent-runtime-token and refreshes it before the returned expires_in; disconnecting from cloud removes WEGENT_RUNTIME_AUTH_TOKEN from the local Codex config. The executor does not inject WEGENT_SUBTASK_ID into these child runtimes.

When project mode calls Claude or Codex model APIs, the executor adds a wecode-project: <project_id> request header in the directly launched runtime context and fills source identity headers: wecode-action: wegent, wecode-source: wegent-local, and wecode-executor: <runtime>, where Claude Code uses claudecode and Codex uses codex. Claude Code local mode first merges existing ANTHROPIC_CUSTOM_HEADERS from the executor startup process environment and the runtime environment, then appends the project identity and writes the resulting header set to both ANTHROPIC_CUSTOM_HEADERS and DEFAULT_HEADERS/default_headers. This keeps the Claude Code child process and downstream model gateways on the same header set. Codex writes the header into provider http_headers for Wegent-managed provider configs, and also injects it for personal Codex config runs when the execution model explicitly names the provider.

Chat Task Device Resolution And Claude Code Launch Context​

When a regular chat Task runs through the local executor, Backend resolves the actual dispatch device before creating or continuing the task. Resolution order is:

  1. The device_id explicitly provided by the current request.
  2. The current Project local execution config, such as config.execution.targetType = local and config.execution.deviceId.
  3. The deviceId already stored in the existing Task spec.

The appDeviceId used by frontend App IPC is only the local process identity. Backend maps it to the executor Socket.IO name stored on the Device CRD before dispatching. If the resolved local device is stale or offline and the current user has exactly one online local executor, Backend switches the task to that online device so a stale id does not block local execution. Unknown device ids are not silently rewritten.

Before launching a Claude Code child process, the executor prepares the task context:

  • It downloads turn attachments into the task directory. Project workspaces use .wegent/attachments/<taskId>/<subtaskId>/; non-Project tasks use an attachment subdirectory under the executor task directory.
  • It restores plugin packages from ~/.claude/plugins/cache when they are still enabled in enabledPlugins but their install directory is missing, and it repairs plugin hook permissions.
  • It deploys task-selected Skills into SKILLS_DIR. Regular Project tasks use global ~/.claude/skills; standalone local work with project_id = 0 and task Skills uses task-level .claude/skills so the global directory is not polluted.
  • If WEGENT_FILE_EDIT_HOOK_COMMAND is configured, it writes Write|Edit|MultiEdit|NotebookEdit PreToolUse and PostToolUse hooks into Claude settings.json so file-change records can be captured as turn artifacts. When Wework macapp starts the local sidecar, it generates this command by default; WEGENT_FILE_EDIT_HOOK_COMMAND can override the full command, and WEWORK_FILE_EDIT_LOG_ENDPOINT can change the default reporting endpoint.

The local executor converts Claude stdout NDJSON into Responses API events as soon as output arrives: visible text becomes response.output_text.delta, reasoning summaries become response.reasoning_summary_text.delta, and the process still sends a final response.completed or error event after exit. Backend and frontend code must not assume that response.created is followed immediately by a terminal event.


πŸ”„ Task Execution Flow​

Task State Transitions​


πŸ” Security Mechanisms​

Authentication Flow​

Security Features​

FeatureDescription
JWT AuthenticationWebSocket connections require valid token
Token Expiration7-day expiry, requires periodic refresh
User IsolationDevices can only execute tasks from their owner
Hardware BindingDevice ID generated from hardware identifiers

Backend-triggered terminal and code-server sessions resolve relative paths under the configured local workspace root. Backend-triggered upgrades must stop running local tasks before restarting the executor: if force_stop_tasks is not set, the upgrade is rejected as busy; if forced cancellation fails for any task, the upgrade is aborted and an error status is emitted instead of proceeding to restart.

Local Executor Connection Configuration​

On startup, the local executor resolves configuration in this order: environment variables, ~/.wegent-executor/device-config.json, then defaults. If WEGENT_EXECUTOR_HOME is not set, the executor uses ~/.wegent-executor. The executor always starts the HTTP server. Wework sets WEGENT_APP_IPC_DEVICE_ID on its child process, explicitly enabling local App JSONL IPC on the current process stdin/stdout. If connection.backend_url or WEGENT_BACKEND_URL is also set, that same process connects to Backend, using connection.auth_token or WEGENT_AUTH_TOKEN for device authentication. A standalone Local Executor launched only with Backend connection settings does not enable the stdio control plane and continues to use the existing Socket.IO remote-device path. Wework App manages and communicates only with the executor child process it starts directly; it does not discover or attach to an executor started manually outside the App. A full App exit also terminates only the child it owns.

Development mode uses wegent-executor-dev to watch the source tree and restart the real executor. The supervisor must also monitor the Wework process that launched it: on Unix, a parent PID change stops the current executor and exits so the operating system cannot adopt the supervisor and let it keep restarting executors after Wework has exited.

EXECUTOR_MODE overrides mode. docker starts only the HTTP server. Other values start the loopback HTTP server and select either the Wework stdio control plane or the standalone Local Executor remote Backend control plane from the explicit identity described above; no local IPC socket is created. WEGENT_BACKEND_URL overrides connection.backend_url, WEGENT_SOCKET_URL overrides connection.socket_url, and WEGENT_AUTH_TOKEN overrides connection.auth_token. An empty Socket URL defaults to the Backend URL. When the origins are split, HTTP APIs use the Backend URL while the executor Socket.IO transport uses the independent Socket URL. Normal standalone startup scripts therefore do not set WEGENT_APP_IPC_DEVICE_ID, preserving the existing remote behavior and transport.

Cloud Device Bootstrap Identity Variables​

Cloud devices use a user data startup script to install and run the executor automatically. The startup script injects these identity-related environment variables:

VariableSourcePurpose
WEGENT_AUTH_TOKENAPI key generated by the backend for the cloud deviceAllows the executor to connect to the backend and register the device
WEGENT_USER_JWT_TOKENCurrent user's Bearer JWT from the cloud device creation requestAllows scripts or integrations on the cloud device to access backend capabilities as the current user
WEGENT_USER_NAMECurrent login usernameAllows scripts or integrations on the cloud device to identify the current user

WEGENT_AUTH_TOKEN and WEGENT_USER_JWT_TOKEN must not be used interchangeably: the former represents the device authentication identity, while the latter represents the user identity at cloud device creation time.

Cloud Device Bootstrap System Configuration​

When creating a cloud device, the backend generates the initial login password for the ubuntu user and stores it in the Device CRD spec.cloudConfig.ubuntuInitialPassword field. The user data startup script uses that password with chpasswd to initialize the ubuntu user's password.

The same user data startup script also creates /etc/systemd/system/fstrim.timer.d/override.conf, configures fstrim.timer to run daily, then reloads, restarts, and enables the timer.

User Isolation​

Each device session is bound to a user:

  • Devices can only receive tasks from their registered owner
  • Prevents cross-user task execution
  • Subtasks validated against user namespace

Data Privacy​

When using local devices:

  • Code stays local: Source code is never uploaded to cloud
  • Local execution: All processing happens on user's machine
  • Result streaming: Only output text is transmitted
  • No persistent storage: Cloud doesn't store local files

πŸ”§ Device ID Generation​

The Executor automatically generates a stable device ID based on the following priority:

  1. Cached ID: Stored in ~/.wegent-executor/device_id (if exists)
  2. Hardware UUID:
    • macOS: System hardware UUID
    • Linux: /etc/machine-id
    • Windows: MachineGuid from registry
  3. Fallback: MAC address or random UUID

This ensures devices maintain consistent identity across restarts.


πŸ“Š Concurrency Control​

Slot Management​

Each device supports up to 5 concurrent tasks:

  • Slot usage tracked in real-time via heartbeats
  • Device shows "busy" when all slots are occupied
  • Tasks queue if busy device is selected

Load Balancing​



πŸ’¬ Get Help​

Need help?

  • πŸ“– Check the FAQ
  • πŸ› Submit a GitHub Issue
  • πŸ’¬ Join community discussions