docs(guide): XFS hardware-events coverage (EPIC hiveops-agent#102)

New agent.xfs module (architect/internal/claude tabs) documenting native
CEN/XFS hardware monitoring via the 32-bit helper, framed built-on-develop
/ ships-dark / not-yet-enabled. Adds hiveops.hardware.events to the Kafka
topic catalog, and hardware-events pointers to technical.agent /
.devices / .incident / .agent-proxy overviews. coverage-reconcile green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 11:05:18 -04:00
parent cf525d5f8f
commit ee94f94fa1
8 changed files with 201 additions and 0 deletions
@@ -0,0 +1,75 @@
---
module: agent.xfs
title: XFS Device — native CEN/XFS hardware monitoring (MoniManager replacement)
tab: Architect
order: 30
audience: bcos
---
> **Internal · architecture view.** Confirmed against `hiveops-module-xfs` + `native/hiveops-xfs`
> on develop 2026-07-23. **Built + on dev, ships DARK: `xfs.enabled=false`, and the module is NOT
> in `hiveops-app`'s deps so it is not in the shipped agent JAR yet.** Nothing enabled in prod.
> Groundwork to retire Hyosung **MoniManager**. See [technical.agent] and [platform.kafka].
## Why
MoniManager reads the ATM hardware through the CEN/XFS layer. This module gives HiveIQ the same
native access, so device status and faults come from XFS (a vendor-neutral standard) rather than
from journal/log scraping. It feeds the [hardware-events pipeline](platform.kafka) that turns
faults into incidents.
## Shape — a Java module fronting a native helper
The on-ATM XFS stack is **32-bit** (confirmed on the Nautilus Hyosung lab ATM: `msxfs.dll` is x86,
no 64-bit variant). A 64-bit JVM cannot load it in-process, and XFS service providers fault often,
so the module drives a **separate 32-bit native helper process**.
| Component | Role |
|---|---|
| `XfsModule` (`AgentModule`) | lifecycle, status-change polling, POST orchestration |
| `XfsHelperClient` | spawns the native helper, one-shot commands, line-protocol parse |
| `XfsDeviceStatus` / `HardwareEvent` | status value object / POST payload DTO |
| `HardwareEventClient` | authed POST to agent-proxy (mirrors `IncidentEventClient`) |
| `native/hiveops-xfs/hiveops-xfs.exe` | 32-bit C helper: dynamically loads `msxfs.dll`, `startup`/`open`/`status`/`monitor` |
## Flow (MVP = status-change polling)
`start()` schedules a poller. Each pass, for every configured logical service, the helper runs
`status <logicalName> <category>` (a `WFSGetInfo` read) and returns the device state. On a **state
change** the module builds a `HardwareEvent` and POSTs it to
`agent-proxy /api/atms/hardware-events`. Change-detection is the agent-side debounce.
```
XfsHelperClient.status(name, cat) → WFSGetInfo → deviceState (online/offline/hwerror/…)
if state changed:
HardwareEventClient.post(HardwareEvent{deviceClass, logicalName, deviceState, …})
→ agent-proxy → devices → Kafka hiveops.hardware.events → incident
```
Status polling (not the async event stream) is the MVP because `WFSGetInfo` returns the device
state directly, which is what the incident consumer keys off. The helper also has a long-lived
`monitor` mode (`WFSAsyncRegister` event stream) built + lab-tested — the foundation for a later
real-time layer (decode `dwEventID`), not yet wired into the module.
## Native bridge — the four load-bearing CEN/XFS facts
All standard CEN/XFS 3.x, all confirmed on real Hyosung hardware:
1. Async completions are numeric **`WM_USER + N`** window messages (`WFS_OPEN_COMPLETE = WM_USER+1`),
NOT `RegisterWindowMessage` strings.
2. `WFSRESULT` must be **`#pragma pack(1)`** or the field offsets are wrong.
3. Request the service version **exactly 3.00** — these Hyosung `BSSpi` SPs reject a range whose
high exceeds their max.
4. `WFSOpen` takes **`LOGICAL_SERVICES`** names (`CardReader`, `Sensors`, `Encryptor`,
`VendorDependentMode`) from `HKEY_USERS\.DEFAULT\XFS`, not the `SERVICE_PROVIDERS` keys.
**Coexistence:** HiveIQ opens devices and reads status **concurrently with the live terminal
application** (MoniPlus2) — the SPs allow multiple apps, `WFSOpen` is non-exclusive. So status
monitoring needs no VDM.
## Not yet built (follow-ups, epic hiveops-agent#102)
Real-time async event stream wired into the module · **VDM in/out-of-service control**
(`WFS_CMD_VDM_ENTER/EXIT_MODE`, the original MoniManager control function) · device resets ·
per-class status structs (vendored CEN/XFS SDK headers) · descending version negotiation ·
native-helper CI/signing.
@@ -0,0 +1,49 @@
---
module: agent.xfs
title: XFS Device — native CEN/XFS hardware monitoring (MoniManager replacement)
tab: Claude
order: 10
audience: bcos
---
> **Read this to act.** Built on develop, ships DARK (`xfs.enabled=false`, not in the agent JAR).
> Full design: `hiveops-agent/docs/xfs-integration.md` + `docs/hardware-events-pipeline.md`. Epic
> hiveops-agent#102.
## What it does
On-ATM module that reads Nautilus Hyosung ATM hardware natively via CEN/XFS (through a 32-bit helper
process), and POSTs device faults into the [hardware-events pipeline](platform.kafka) so they become
incidents. Replaces MoniManager's hardware monitoring.
## Key files
- `hiveops-agent/hiveops-module-xfs/` — `XfsModule` (poller), `XfsHelperClient`, `HardwareEvent`,
`HardwareEventClient`.
- `hiveops-agent/native/hiveops-xfs/` — `xfs_helper.c`, `xfs_min.h`, build scripts, README.
## To test the helper on an ATM (read-only, safe)
Lab ATM `AT000999` (`NHAdministrator@192.168.200.43`) has the helper at
`C:\hiveops-agent\native\hiveops-xfs.exe`. **That box points at PROD** — `startup`/`open`/`status`
are read-only and emit nothing to the backend, but do NOT run the agent module against it.
```
hiveops-xfs.exe startup # negotiate CEN/XFS version
hiveops-xfs.exe open CardReader # open a service provider
hiveops-xfs.exe status CardReader 201 # read device state (online/offline/hwerror)
```
## To simulate the backend pipeline (no ATM needed)
Produce a `HardwareEvent` JSON to `hiveops.hardware.events`, or POST to devices
`/api/internal/atms/hardware-events` (header `X-Internal-Secret`). A fault deviceState
(offline/hwerror/nodevice/fraud) raises an incident; `online` resolves it. See [technical.incident].
## Gotchas
- **Ships dark** — enabling requires un-darking the module (add to `hiveops-app`) + `xfs.enabled=true`.
- The `EVENT` lines from the helper's `monitor` mode carry `dwEventID`, not device state — decoding
them needs the CEN/XFS SDK; the module uses `status` polling for the MVP.
- 32-bit helper only (the fielded `msxfs.dll` is x86). Cross-build with MinGW-i686.
- Never issue a PIN/EPP command (PCI). See the Architect tab for the four native-bridge facts.
@@ -0,0 +1,48 @@
---
module: agent.xfs
title: XFS Device — native CEN/XFS hardware monitoring (MoniManager replacement)
tab: Internal
order: 20
audience: bcos
---
> **Internal · config & ops.** Built on develop, ships DARK. Not enabled anywhere yet.
## Configuration (`hiveops.properties`)
| Property | Default | Meaning |
|---|---|---|
| `xfs.enabled` | `false` | master switch; module is fully inert until `true` |
| `xfs.helper.path` | `native/hiveops-xfs.exe` | path to the 32-bit helper (relative to the agent install dir) |
| `xfs.status.devices` | `CardReader:201,Sensors:801,Encryptor:401` | `logicalName:WFS_INF_category` pairs to poll |
| `xfs.poll.seconds` | `60` | status-change poll interval |
The module **self-disables** (stays inert, logs why) unless ALL of: `xfs.enabled=true`, Windows,
an `agent.endpoint` is set, and the helper binary exists. Safe to ship to the whole fleet dark.
## What it emits
On a device state change it POSTs a `HardwareEvent` (bearer-authed, via `HttpClientHelper`) to
`agent-proxy /api/atms/hardware-events`. `deviceClass` is derived from the logical name
(CardReader→IDC, Sensors→SIU, Encryptor→PIN, CashDispenser→CDM, ReceiptPrinter→PTR,
VendorDependentMode→VDM). `severity` is advisory only — the incident consumer derives its own.
## Enabling / go-live (not done yet)
1. Add `hiveops-module-xfs` to `hiveops-app` deps (un-dark → in the JAR).
2. Cross-build the 32-bit helper: `native/hiveops-xfs/build-win32.sh` (needs `gcc-mingw-w64-i686`)
or `build-win32.cmd` on Windows. `build-dist.sh` bundles `hiveops-xfs.exe` into the Windows dist
when present.
3. Ship the agent build, set `xfs.enabled=true` per-ATM.
## Category constants (confirmed on the lab ATM)
`WFS_INF_IDC_STATUS = 201` (CardReader), `WFS_INF_SIU_STATUS = 801` (Sensors),
`WFS_INF_PIN_STATUS = 401` (Encryptor). Service version negotiated is **3.00**. The encryptor is
read for status only — **never commanded** (keeps the EPP/PIN path out of active PCI scope).
## Debug
Set env `HIVEOPS_XFS_DEBUG=1` when running the helper directly for message-pump tracing. Manual
helper commands on an ATM: `hiveops-xfs.exe startup` · `open <logicalName>` ·
`status <logicalName> <category>` · `monitor <seconds> <logicalName...>`.
@@ -26,6 +26,7 @@ relevant `technical.<service>` guide.
| Topic | Producer | Consumers | Payload | | Topic | Producer | Consumers | Payload |
|-------|----------|-----------|---------| |-------|----------|-----------|---------|
| `hiveops.device.events` | devices | incident, fleet, reports, recon, vault (`DeviceEventConsumer`) | device create/update/delete → `device_summary` mirrors | | `hiveops.device.events` | devices | incident, fleet, reports, recon, vault (`DeviceEventConsumer`) | device create/update/delete → `device_summary` mirrors |
| `hiveops.hardware.events` | devices | incident (`HardwareEventConsumer`) | native-XFS device faults/status from the agent → fault→incident. **Stream topic (retention, not compacted).** Built on develop, ships dark — see [agent.xfs], epic hiveops-agent#102. |
| `hiveops.fleet.task.events` | fleet | devices | fleet task lifecycle → device-scoped `DeviceFleetTask` history | | `hiveops.fleet.task.events` | fleet | devices | fleet task lifecycle → device-scoped `DeviceFleetTask` history |
| `hiveops.incidents.created` | incident | (see service guide) | incident opened | | `hiveops.incidents.created` | incident | (see service guide) | incident opened |
| `hiveops.incidents.updated` | incident | (see service guide) | incident changed | | `hiveops.incidents.updated` | incident | (see service guide) | incident changed |
@@ -56,3 +56,9 @@ Not exhaustive — see the controller package for the full set.
- **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`. - **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. - **`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. - **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.
## Hardware events (XFS) — built on develop, ships DARK
New agent-authed `POST /api/atms/hardware-events` (AgentAuthFilter) injects `institutionKey` and
forwards to devices `/api/internal/atms/hardware-events`. Thin gateway, no Kafka here — mirrors the
existing `hardware/sync` path. Not yet enabled. See [agent.xfs], [technical.devices].
@@ -99,3 +99,10 @@ From `hiveops-app/src/main/resources/hiveops.properties`:
- **ATM id resolution order matters:** property → `HIVEOPS_ATM_ID` env → Windows registry → hostname fallback; a mis-imaged box can silently take the wrong identity/institution. - **ATM id resolution order matters:** property → `HIVEOPS_ATM_ID` env → Windows registry → hostname fallback; a mis-imaged box can silently take the wrong identity/institution.
- **`agent.update.enabled=false` customers** (e.g. ATEC Java 8 branch) get patches by hand via share.bcos.cloud, not CDN auto-update. - **`agent.update.enabled=false` customers** (e.g. ATEC Java 8 branch) get patches by hand via share.bcos.cloud, not CDN auto-update.
- **`hiveops-module-tcr-events` directory exists but is not a listed `<module>`** in the root `pom.xml` (the built TCR modules are `atec-tcr-events` / `nh-tcr-events`); treat the bare `tcr-events` dir as legacy. (unverified intent) - **`hiveops-module-tcr-events` directory exists but is not a listed `<module>`** in the root `pom.xml` (the built TCR modules are `atec-tcr-events` / `nh-tcr-events`); treat the bare `tcr-events` dir as legacy. (unverified intent)
## XFS device monitoring — built on develop, ships DARK
New module `hiveops-module-xfs` ([agent.xfs]) reads Nautilus Hyosung ATM hardware natively
via CEN/XFS (through a 32-bit native helper), and POSTs device faults into the hardware-events
pipeline. Inert by default (`xfs.enabled=false`, not yet in the shipped JAR). Groundwork to retire
Hyosung MoniManager. See [platform.kafka] (`hiveops.hardware.events`), epic hiveops-agent#102.
@@ -129,3 +129,10 @@ Base paths: `/api/atms` + `/api/devices`, `/api/atm-groups`, `/api/atm-makes`, `
else `IN_SUPERVISOR` if `inSupervisor`, else `serviceState || status`. An offline agent therefore **masks** else `IN_SUPERVISOR` if `inSupervisor`, else `serviceState || status`. An offline agent therefore **masks**
supervisor mode in the UI. supervisor mode in the UI.
- **Error message hiding.** `server.error.include-message=never` and `management.endpoint.health.show-details=never` — expect terse errors in prod. - **Error message hiding.** `server.error.include-message=never` and `management.endpoint.health.show-details=never` — expect terse errors in prod.
## Hardware events (XFS) — built on develop, ships DARK
Devices is the **producer** of `hiveops.hardware.events`. Internal endpoint
`POST /api/internal/atms/hardware-events` (agent-proxy forwards agent events to it) →
`HardwareEventPublisher` → Kafka topic (stream, not compacted). Consumed by incident. Not yet
enabled. See [agent.xfs], [platform.kafka], epic hiveops-agent#102.
@@ -81,3 +81,11 @@ Not exhaustive — the service has ~26 controllers (ATM groups, cassette config,
- **`/atm/...` vs `/device/...` dual mappings** exist on incident and journal-event controllers (Phase 2 device-rename aliases) — both resolve to the same handler. - **`/atm/...` vs `/device/...` dual mappings** exist on incident and journal-event controllers (Phase 2 device-rename aliases) — both resolve to the same handler.
- `JWT_SECRET` is shared with hiveops-auth and hiveops-mgmt; `mgmt.service.url` is used for agent token validation. - `JWT_SECRET` is shared with hiveops-auth and hiveops-mgmt; `mgmt.service.url` is used for agent token validation.
- Server port is **8080** (NGINX path `/api/incident/`); multipart uploads allow up to 500MB (agent log files). - Server port is **8080** (NGINX path `/api/incident/`); multipart uploads allow up to 500MB (agent log files).
## Hardware events (XFS) — built on develop, ships DARK
`HardwareEventConsumer` consumes `hiveops.hardware.events` and maps native-XFS device faults to
incidents via existing trigger types (IDC→CARD_READER_FAIL, CDM→DISPENSER_JAM(CRIT),
fraud/tamper→SECURITY_INVESTIGATION(CRIT), else HARDWARE_ERROR), reusing `IncidentAutoCreationService`
(`createDeviceFaultIncident`) for dedup + severity. `online` recovery auto-resolves the matching
fault. Benign events are history-only. Not yet enabled. See [agent.xfs], [platform.kafka].