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:
| Type | Lifecycle owner | Connection | Typical entrypoint |
|---|---|---|---|
local | User's local executor | WebSocket | Local installer or manually started executor |
cloud | Wegent cloud device service | WebSocket | Cloud device create, restart, and release flows |
remote | User-managed Docker container or remote host | WebSocket | Remote 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β
| Event | Direction | Description |
|---|---|---|
device:register | Device β Backend | Device registration |
device:heartbeat | Device β Backend | Heartbeat keepalive |
task:execute | Backend β Device | Task dispatch |
task:progress | Device β Backend | Task progress |
task:complete | Device β Backend | Task 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-sessionchat:messagedevice:execute_commanddevice:sync_capabilitiesdevice:start_terminal_session,device:start_code_server_sessionterminal:input,terminal:resize,terminal:closeruntime:rpcdevice:upgradedevice: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β
| Parameter | Value | Description |
|---|---|---|
| Heartbeat Interval | 30 seconds | Device sends heartbeat |
| Online TTL | 90 seconds | Redis key expiration |
| Monitor Interval | 60 seconds | Backend checks expired devices |
| Offline Threshold | 3 missed heartbeats | Device 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 revisioncapabilities.digest: content digest forskills,plugins, andmcpscapabilities.skills: Skills available under~/.claude/skillscapabilities.plugins: Plugins installed in~/.claude/plugins/installed_plugins.jsoncapabilities.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-resolvedInstalledSkill/Skillentries, downloaded by the executor into~/.claude/skillsplugins: backend-resolvedInstalledPluginentries, written by the executor into~/.claude/plugins/installed_plugins.jsonmcps: backend-resolvedInstalledMCPentries, 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:
- The
device_idexplicitly provided by the current request. - The current Project local execution config, such as
config.execution.targetType = localandconfig.execution.deviceId. - The
deviceIdalready 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/cachewhen they are still enabled inenabledPluginsbut 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 withproject_id = 0and task Skills uses task-level.claude/skillsso the global directory is not polluted. - If
WEGENT_FILE_EDIT_HOOK_COMMANDis configured, it writesWrite|Edit|MultiEdit|NotebookEditPreToolUseandPostToolUsehooks into Claudesettings.jsonso 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_COMMANDcan override the full command, andWEWORK_FILE_EDIT_LOG_ENDPOINTcan 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β
| Feature | Description |
|---|---|
| JWT Authentication | WebSocket connections require valid token |
| Token Expiration | 7-day expiry, requires periodic refresh |
| User Isolation | Devices can only execute tasks from their owner |
| Hardware Binding | Device 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:
| Variable | Source | Purpose |
|---|---|---|
WEGENT_AUTH_TOKEN | API key generated by the backend for the cloud device | Allows the executor to connect to the backend and register the device |
WEGENT_USER_JWT_TOKEN | Current user's Bearer JWT from the cloud device creation request | Allows scripts or integrations on the cloud device to access backend capabilities as the current user |
WEGENT_USER_NAME | Current login username | Allows 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:
- Cached ID: Stored in
~/.wegent-executor/device_id(if exists) - Hardware UUID:
- macOS: System hardware UUID
- Linux:
/etc/machine-id - Windows:
MachineGuidfrom registry
- 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β
π Related Documentationβ
- Local Device User Guide - User operation guide
- System Architecture - Overall architecture design
- OpenAPI Responses API - API reference
π¬ Get Helpβ
Need help?
- π Check the FAQ
- π Submit a GitHub Issue
- π¬ Join community discussions