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​

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

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

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.

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​

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

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:

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