---
module: technical.remote
title: Remote — Remote screen capture, file access & module deployment for ATMs
tab: Overview
order: 10
audience: dev
---

> **DRAFT · internal.** Written 2026-07-01 from hiveops-remote source. Verify against code before relying on specifics.

## Responsibility

`hiveops-remote` is the central server for remote operations on ATM devices: on-demand screenshots, remote directory listing/file download, and distribution of the agent-side "remote" module JAR. Maven multi-module — `hiveops-remote-common` (shared DTOs/enums), `hiveops-remote-module` (agent-side client running on the ATM: screen capture, filesystem, HTTP client), and `hiveops-remote-server` (Spring Boot 3.4.1, Java 21, this service). Communication is HTTP long-poll + multipart chunk upload, **not** WebSocket: agents register, then long-poll `/api/v1/agents/poll` for pending commands and upload binary results in chunks. Owns its agent registry, command queue, and module version catalog exclusively. Cross-link [platform.service-ownership].

## Data

Database `hiveiq_remote` (PostgreSQL). Note: `application.yml` builds the JDBC URL from `DB_HOST`/`DB_PORT`/`DB_NAME`/`DB_USER`/`DB_PASSWORD` env vars (default `DB_NAME` in yml is `hiveops_remote`; production uses `hiveiq_remote`). Tables are created by Hibernate `ddl-auto: update`; `schema.sql` is reference only. Cross-link [platform.data-stores].

| Table | Entity | Holds |
|-------|--------|-------|
| `remote_agents` | `RemoteAgent` | Registered agents: `agent_id` (UUID), name, hostname, platform, agent/module version, status, `last_heartbeat_at`, unique `machine_id` |
| `remote_agent_capabilities` | (element collection) | Per-agent capability strings |
| `remote_commands` | `RemoteCommand` | Command queue: `command_id` (UUID), type, status, `parameters`/`result` (JSONB), `has_binary_result` |
| `binary_chunks` | `BinaryChunk` | Chunked binary results (BYTEA) keyed by `(command_id, chunk_index)`, with checksum |
| `module_versions` | `ModuleVersion` | Remote module JAR catalog: version, filename, checksum, size, `is_latest` |

## Kafka

Produces only — no consumers (`@KafkaListener`) exist in this service.

| Direction | Topic | Class |
|-----------|-------|-------|
| Produced | `hiveops.agent.online` | `AgentConnectionEventProducer.publishOnline` (called from `AgentService` on register), constant `RemoteKafkaConfig.TOPIC_AGENT_ONLINE` |
| Produced | `hiveops.agent.offline` | `AgentConnectionEventProducer.publishOffline` (called from `AgentService` for stale agents), constant `RemoteKafkaConfig.TOPIC_AGENT_OFFLINE` |

Messages are JSON `AgentConnectionEvent`, keyed by `machineId`. Both topics declared as `NewTopic` with 3 partitions / 1 replica. Cross-link [platform.kafka].

## API surface

Three controllers, all under `/api/v1`.

**Agent-facing** — `AgentController` @ `/api/v1/agents` (called by the ATM-side RemoteModule):

| Method Path | Purpose |
|-------------|---------|
| POST `/api/v1/agents/register` | Agent self-registration (permitAll) — returns JWT |
| GET `/api/v1/agents/poll` | Long-poll for pending commands (JWT-authenticated) |
| POST `/api/v1/agents/heartbeat` | Keep-alive, updates `last_heartbeat_at` |
| POST `/api/v1/agents/commands/{commandId}/status` | Report command execution status |
| POST `/api/v1/agents/commands/{commandId}/upload` | Upload binary result chunk (multipart: chunkIndex, totalChunks, checksum, data) |

**User-facing** — `RemoteController` @ `/api/v1`:

| Method Path | Purpose |
|-------------|---------|
| GET `/api/v1/remote/agents` | List registered agents |
| GET `/api/v1/remote/agents/{agentId}` | Get agent details |
| DELETE `/api/v1/remote/agents/{agentId}` | Unregister agent |
| POST `/api/v1/commands/screenshot` | Request a screenshot from an agent |
| POST `/api/v1/commands/file-list` | List a directory on an agent |
| POST `/api/v1/commands/file-download` | Download a file from an agent |
| GET `/api/v1/commands/{commandId}` | Get command status |
| GET `/api/v1/commands/{commandId}/result` | Download binary result (SCREENSHOT→`image/jpeg` `screenshot.jpg`; FILE_DOWNLOAD→`application/octet-stream`) |

**Module distribution** — `ModuleController` @ `/api/v1/modules`:

| Method Path | Purpose |
|-------------|---------|
| GET `/api/v1/modules/latest` | Latest module version info |
| GET `/api/v1/modules/{version}` | Specific version info |
| GET `/api/v1/modules/{version}/download` | Download module JAR for a version |
| GET `/api/v1/modules/latest/download` | Download latest module JAR |

Port **8090**; NGINX path `/api/remote/`.

## Gotchas

- **Not WebSocket.** Despite the "remote command execution" framing, transport is HTTP long-poll (`remote.poll-timeout-ms: 15000`) plus chunked multipart upload (`chunk-size-bytes: 1048576`, `max-file-size-bytes: 104857600`).
- **Three separate security filter chains** (`SecurityConfig`, ordered): `/api/v1/agents/**` uses a custom `AgentAuthenticationFilter` (JWT via `AgentPrincipal`), with only `/register` permitAll; `/api/v1/modules/**` is **fully permitAll** (module JARs are unauthenticated downloads); everything else (user-facing commands, actuator, swagger) falls to the default chain using **HTTP Basic** (`ADMIN_USER`/`ADMIN_PASSWORD`). No `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` here — user-facing endpoints are Basic-auth gated, not JWT-role gated.
- **Fails to start without secrets.** `JWT_SECRET`, `ADMIN_USER`, and `ADMIN_PASSWORD` have no defaults by design.
- **Swagger disabled by default** — enable only in dev with `SWAGGER_ENABLED=true`.
- **Agent liveness is timer-driven.** Offline events (`hiveops.agent.offline`) are emitted when a sweep marks stale agents (`agent-timeout-ms: 60000`, checked every `agent-check-interval-ms: 30000`), not on an explicit disconnect.
- **Module binaries live on a volume** at `MODULE_STORAGE_PATH` (`/app/modules`), not in the DB (only chunked command results use `binary_chunks` BYTEA).
- **`kafka.admin.fail-fast: false`** — the service still boots if Kafka is unavailable.
