---
module: technical.agent-proxy
title: Agent-Proxy — agent-facing API gateway
tab: Overview
order: 10
audience: dev
---

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

## Responsibility

`hiveops-agent-proxy` is the **sole entry point for all ATM agent traffic** — no agent request reaches `hiveops-incident` directly. It (1) authenticates agents (bearer `agtkn_*` tokens validated via mgmt, or legacy `X-Institution-Key` header), (2) tracks per-machine fingerprints in its own DB, and (3) forwards each agent call natively to incident's internal API (`/api/internal/**`) using the shared `X-Internal-Secret`. Runs on container port **8093** (prod host port 8015). See [platform.service-ownership].

## Data

Own PostgreSQL DB **`hiveiq_agent`** (prod profile uses `DB_HOST`/`DB_PORT`/`DB_NAME`/`DB_USERNAME`/`DB_PASSWORD`; `DB_NAME` defaults to `hiveiq_agent`). Dev profile runs H2 in-memory. See [platform.data-stores].

| Table / Entity | Purpose |
|---|---|
| `agent_connections` (`AgentConnection`) | Machine fingerprint registry — `machine_id` (unique), `atm_id`, `institution_key`, `agent_version`, `hostname`, `first_seen`, `last_seen`. V2 adds `device_id` (dual-write with `atm_id` via DB trigger). |

## Kafka

No Kafka. This service is a synchronous HTTP proxy — no producers or `@KafkaListener` consumers exist in the source.

## API surface

Base path `/api/agent` (plus a few root-level paths). All paths require `ROLE_AGENT` (set by `AgentAuthFilter`). Each controller forwards to the matching incident `/api/internal/**` endpoint via `IncidentInternalService`.

| Method + Path | Controller | Purpose |
|---|---|---|
| PUT `/api/agent/{country}/{atm}/heartbeat` | HeartbeatController | Agent heartbeat; upserts machine fingerprint + updates device connection state |
| GET `/api/agent/{country}/{atm}/task` | FleetTaskController | Poll for pending fleet task |
| GET `/api/agent/tasks/{tid}/download` | FleetTaskController | Download task artifact |
| POST `/api/agent/tasks/{tid}/status` | FleetTaskController | Report task status |
| POST `/api/agent/tasks/{tid}/verify` | FleetTaskController | Verify task completion |
| POST `/api/agent/tasks/{tid}/upload` | FleetTaskController | Upload task result/log |
| POST `/api/agent/{country}/{atm}/auto-update` | FleetTaskController | Agent auto-update check |
| POST `/api/journal-events` | JournalEventController | Submit journal events |
| POST `/api/agent/register` | AgentRegistrationController | Agent/ATM registration |
| POST `/api/agent/{country}/{atm}/log/errors` | AgentLogController | Push error log entries |
| POST `/api/agent/{country}/{atm}/log/upload` | AgentLogController | Upload log file (multipart, ≤100MB) |
| POST `/api/atms/hardware/sync` · POST `/api/atms/config/sync` | AtmSyncController | Hardware / config sync |
| POST `/api/agent/{country}/{atm}/config` | AtmSyncController | Config sync (injects atmName+country) |
| POST `/api/agent/{country}/{atm}/modules` · PUT `/api/agent/{country}/{atm}/transit` | AgentModuleTransitController | Module inventory / transit state |
| `/api/agent/**` (wildcard) | AgentProxyController | Fallback for un-migrated legacy agent paths |

Not exhaustive — see the controller package for the full set.

## Gotchas

- **Auth role is `ROLE_AGENT`, not the admin pattern.** `SecurityConfig` requires `hasRole("AGENT")` on every request except `/actuator/health`, `/actuator/info`, `/error`. Do NOT expect `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` here — that is for admin-facing services.
- **Two auth mechanisms, checked in order** (`AgentAuthFilter`): (1) `Authorization: Bearer agtkn_*` → validated against mgmt `/api/internal/agent-tokens/validate`, cached 5 min (`AGENT_TOKEN_CACHE_SECONDS`, default 300); (2) legacy `X-Institution-Key` header → provisional `ROLE_AGENT`, uppercased, incident re-validates. Principal is `agent:{institutionKey}` in both cases.
- **`INTERNAL_SERVICE_SECRET` is required.** All outbound calls to incident carry it as `X-Internal-Secret`; incident's `InternalSecretFilter` grants `ROLE_INTERNAL`. Empty by default in config.
- **Uses `DB_HOST`/`DB_PORT`/`DB_NAME`, not `DB_URL`.** (Contrast with hiveops-journal, which expects a full `DB_URL`.) Dev profile has Flyway disabled and uses H2 `create-drop`; prod uses Flyway + `ddl-auto: validate`.
- **`atm_id` / `device_id` dual-write.** V2 migration keeps both columns in sync via a BEFORE INSERT/UPDATE trigger (device-rename Phase 1). `MachineFingerprintService` should accept both `atmId` and `deviceId` JSON field names from heartbeats.
- **Exact `@PutMapping`/`@PostMapping` paths win over the `/api/agent/**` wildcard** in `AgentProxyController` — the wildcard is only a fallback for paths not yet given a native controller.
