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β
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 |
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 |
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.
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.
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.
π 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 |
Local Executor Connection Configurationβ
On startup, the local executor resolves configuration in this order: environment variables, ~/.wegent-executor/device-config.json, then defaults. The mode field selects the startup mode, while connection.backend_url and connection.auth_token are used to connect to the Backend and authenticate the device.
EXECUTOR_MODE overrides mode, WEGENT_BACKEND_URL overrides connection.backend_url, and WEGENT_AUTH_TOKEN overrides connection.auth_token. This means normal startup scripts do not need to require those environment variables; if the device config already contains valid mode and connection settings, the executor can start directly.
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