---
module: agent.remote
title: Remote Control Module — how it's built
tab: Architect
order: 30
audience: dev
---

> **Architect · dev view.** Grounded in `hiveops-module-remote` source (parent v4.4.3, module `pom` version 1.0.0) on 2026-07-01. See [technical.agent] for the shared module system this plugs into.

## Shape

A standard agent module (SPI `com.hiveops.core.module.AgentModule`, registered in `META-INF/services/com.hiveops.core.module.AgentModule` → `com.hiveops.remote.module.RemoteModule`). Lifecycle is the platform default: `getDefaultConfiguration()` → `isEnabled()` → `initialize()` → `start()` → `stop()`. Unlike most modules it is a **command executor**, not a monitor: there is no log tail, no journal parse, and no incident/journal event emission. It drives a request/response loop against a separate server.

## Classes and flow

```
RemoteModule                (SPI entry: config defaults, enable gate, wiring, lifecycle)
  └─ RemoteCommandProcessor (register → poll loop → dispatch → status/upload)
       ├─ RemoteHttpClient  (HttpURLConnection + Jackson; Bearer auth)
       ├─ ScreenCaptureService  (AWT Robot → JPEG)
       └─ FileSystemService     (dir list / file open, guarded by PathSecurityUtils)
  common.dto.*   (PollResponse, CommandType, CommandStatus, CommandStatusUpdate,
                  AgentRegistrationRequest/Response, FileEntry, ...)
  common.security.PathSecurityUtils
```

**`RemoteModule`** — `initialize()` reads `remote.server.url` (required; throws `ModuleInitializationException` if blank), derives `machineId` from `remote.machine.id` else the platform hostname (`PlatformService.getSystemInfoProvider().getHostname()`) else a random UUID, and constructs the HTTP client + services. Advertised `capabilities` are hardcoded to `["SCREENSHOT","FILE_LIST","FILE_DOWNLOAD"]`. `isEnabled()` gates on `remote.enabled`.

**`RemoteCommandProcessor`** — owns a 2-thread `ScheduledExecutorService`. On `start()`:
1. `registerWithServer()` — builds `AgentRegistrationRequest` (name = `context.getAtmName()`, hostname, `os.name`, agent version from `hiveops.version`, module version `1.0.0-SNAPSHOT`, machineId, capabilities), `POST`s it, and stores the returned Bearer token. **Registration errors are caught and logged, not retried** — a failed register leaves the token null and every subsequent poll 401s.
2. `scheduleWithFixedDelay(pollAndProcess, 0, remote.poll.interval.ms)`.

Each `pollAndProcess()` calls `GET /api/v1/agents/poll`; if `PollResponse.hasCommand`, it dispatches on `CommandType`:
- `SCREENSHOT` → `ScreenCaptureService.capture(quality, displayIndex)` → `uploadBinaryResult`.
- `FILE_LIST` → `FileSystemService.listDirectory(path)` (defaults to `user.home` if blank) → status `COMPLETED` with a JSON `{path, entries}` result (no binary).
- `FILE_DOWNLOAD` → `FileSystemService.downloadFile(path)` → read all bytes → `uploadBinaryResult`.
- default (incl. `SYSTEM_INFO`) → `FAILED: Unknown command type`.

Status transitions use `CommandStatusUpdate` factories (`inProgress()`, `completed(result)`, `failed(msg)`, `completedWithBinary(totalChunks)`) mapping to `CommandStatus` `IN_PROGRESS` / `COMPLETED` / `FAILED`. The processor sets `IN_PROGRESS` before running and reports `FAILED` on any exception (with a nested try so a failed status-report doesn't crash the loop).

**`uploadBinaryResult`** — splits `byte[]` into `remote.chunk.size.bytes` (1 MiB) chunks, computes a **SHA-256** hex checksum per chunk, and posts each as multipart `POST /api/v1/agents/commands/{id}/upload`. Empty results still send 1 chunk. No retry on chunk failure.

**`ScreenCaptureService`** — cross-platform via `java.awt.Robot`. Detects headless at construction; `capture()` throws `UnsupportedOperationException` in headless mode. JPEG quality is `quality/100f` or default `0.85`. `displayIndex` selects a `GraphicsDevice`; multi-monitor defaults to device 0. Alpha images are flattened to `TYPE_INT_RGB` before JPEG encode.

**`FileSystemService`** — enforces, in order: `PathSecurityUtils.validatePath` (null/empty, null-byte, `..` traversal), `validateWithinAllowedPaths` (only when `remote.allowed.paths` is set), and `isPathBlocked` against `remote.blocked.paths` (OS-specific defaults if unset, `*` wildcards). Then existence/type/readable checks; `downloadFile` also enforces `remote.max.file.size.bytes`. `FileEntry` carries name/size/mtime/type (`DIRECTORY`/`FILE`/`SYMLINK`/`OTHER`)/readable/writable and POSIX perm string (null on Windows). Symlinks are read `NOFOLLOW_LINKS`.

## How results reach the platform

This module does **not** use the standard agent → agent-proxy/incident journal-event path. It speaks a dedicated REST protocol to the **hiveops-remote** server (`RemoteHttpClient`, base `remote.server.url`, prod path `/api/remote/`, internal `:8090`):

| Call | Method + path | Auth |
|---|---|---|
| Register | `POST /api/v1/agents/register` | none (returns Bearer token) |
| Poll for command | `GET /api/v1/agents/poll` | Bearer |
| Report status | `POST /api/v1/agents/commands/{id}/status` | Bearer |
| Upload binary chunk | `POST /api/v1/agents/commands/{id}/upload` (multipart: `chunkIndex`, `totalChunks`, `checksum`, `data`) | Bearer |
| Heartbeat | `POST /api/v1/agents/heartbeat` | Bearer |

`RemoteHttpClient` is plain `HttpURLConnection` + Jackson (`FAIL_ON_UNKNOWN_PROPERTIES=false`, `JavaTimeModule`); any `>=400` throws `IOException` with the error body. Note: `heartbeat()` is implemented but **never invoked** by the processor — liveness is inferred from polling.

## Platform abstraction

Minimal: only `PlatformService.getSystemInfoProvider().getHostname()` for the machine-id fallback. Screen capture and file access use portable JVM APIs (AWT Robot, `java.nio.file`) rather than the agent's `SystemCommands`/`PathResolver`, so behavior differs by environment (headless, POSIX vs Windows perms) rather than by an explicit platform impl. Delivery/restart on `INSTALL_MODULE`, by contrast, does go through the platform layer (`ProcessInstallModule` → `SystemCommands.restartService`).
