---
module: devices.agent-profile
title: Agent Profile
tab: Architect
order: 30
audience: dev
---

How the **Agent Profile** feature is built end to end. Profiles are lightweight hardware-grouping records; the "which modules" logic is resolved at render time by joining profile records against a CDN-hosted module manifest. See [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Services involved

| Component | Role |
|-----------|------|
| `hiveops-devices` frontend (Svelte, port 5177) | `components/Settings/AgentProfile.svelte` — CRUD table + modal, module-count expansion |
| `hiveops-devices` backend (Spring Boot, port 8096) | `AgentProfileController` / `AgentProfileService` / `AgentProfileRepository` — owns profile records and proxies the CDN manifest |
| CDN (`cdn.bcos.cloud`) | Hosts `downloads/agent/modules/modules.json` — the module→profile mapping |
| Agent (on-ATM) | Reads its own `atm.hardware.profile` property and pulls matching modules |

There is **no cross-service call** for this feature: it is entirely within `hiveops-devices`, which is the source of truth for device/agent-config data. The only external dependency is the CDN fetch.

## Data flow

### Profile CRUD (admin write path)
```
AgentProfile.svelte  ──HTTP──▶  AgentProfileController  ──▶  AgentProfileService  ──▶  agent_profiles (hiveiq_devices)
   getAll/create/update/delete        (@PreAuthorize on mutations)     (@Transactional)
```
- `getAll()` → `repo.findAllByOrderBySortOrderAscProfileKeyAsc()` (sort_order asc, then key).
- `create()` → 409 if PK exists; key is trimmed + lowercased.
- `update()` → mutates label/description/enabled/sortOrder only; **key is immutable** (PK, disabled in the UI).
- `delete()` → guarded by `isProfileInUse(key)`: `SELECT COUNT(a) > 0 FROM Atm a WHERE a.hardwareProfile = :key`; returns 409 when any ATM still references it.
- `@PrePersist`/`@PreUpdate` set `created_at`/`updated_at`.

### Module resolution (read path — the interesting part)
```
AgentProfile.svelte ─GET /modules-manifest─▶ AgentProfileController
                                                   │ 60s in-memory cache (AtomicReference + AtomicLong expiry)
                                                   ▼
                                        HTTPS GET cdn.bcos.cloud/downloads/agent/modules/modules.json
                                                   │ adds X-CDN-Token header (agent.cdn.token) server-side
                                                   ▼
                                        {"modules":[{name,version,filename,profiles?[]}]}
```
The SPA joins the manifest against each profile **client-side**:
- module with empty/absent `profiles[]` → installs on all ATMs (universal).
- module whose `profiles[]` contains the profileKey → **exclusive** to that profile (★ / "+N exclusive" badge).

So a profile record itself stores no module list; membership is derived. This keeps module bundles versioned on the CDN independent of the DB.

### Agent-side effect (out of scope of this screen, for context)
An ATM's `atm.hardware.profile` agent property is matched against `agent_profiles.profile_key` and against `modules.json`. The `enabled` flag pauses provisioning for that profile.

## Kafka

**None for this feature.** Profile CRUD does not produce or consume Kafka events. (The broader `hiveops-devices` service publishes `hiveops.device.events` for device rows via `DeviceEventPublisher`, and consumes `hiveops.fleet.task.events` / `journal.replenishments` — but agent-profile writes touch neither.) See [platform.kafka].

## DB tables

| Table | Access | Notes |
|-------|--------|-------|
| `agent_profiles` (`hiveiq_devices`) | read/write | PK `profile_key` (varchar 40); `label`, `description` (TEXT), `enabled` (bool), `sort_order` (int), `created_at`, `updated_at`. Flyway `V14__agent_profiles_and_hardware_profile.sql`. |
| `atms` (`hiveiq_devices`) | read-only (from this feature) | `hardware_profile` (varchar 40) — the device→profile link, read by the delete-guard count query. |

## Key API endpoints

| Method + Path | Auth | Purpose |
|---------------|------|---------|
| GET `/api/agent-profiles` | authenticated | List profiles (sorted) |
| POST `/api/agent-profiles` | MSP_ADMIN/BCOS_ADMIN | Create |
| PUT `/api/agent-profiles/{key}` | MSP_ADMIN/BCOS_ADMIN | Update (no key change) |
| DELETE `/api/agent-profiles/{key}` | MSP_ADMIN/BCOS_ADMIN | Delete (guarded) |
| GET `/api/agent-profiles/modules-manifest` | authenticated | 60s-cached CDN manifest proxy, injects `X-CDN-Token` |

## Design notes

- **No executor→Kafka→record-store pattern here** — this is a plain CRUD + read-through-cache feature, not a long-running fleet action. (That pattern applies to fleet tasks, not profiles.)
- **CDN token stays server-side.** The token is only ever attached in `AgentProfileController.getModulesManifest`; the SPA never sees it.
- **Manifest cache is process-local** (`AtomicReference`), so each backend replica caches independently and CDN changes take up to 60s per replica to appear.
- See parent architect doc [technical.devices] for the full devices service map.
