-
Notifications
You must be signed in to change notification settings - Fork 12
discovery detailed analysis
This document proposes a unified solution for three related features in the Device Management Toolkit (DMT). The common goal is to give operators greater visibility into their AMT-capable device fleet — covering what devices are present on the network, what their firmware and platform state is, and whether key services such as Intel LMS are running. The proposal defines the required data model changes, API impact across Console, MPS, RPS, and rpc-go, and a design for the discovery agent that collects and reports device state.
| Issue | Summary |
|---|---|
| sample-web-ui#1265 — SKU Type & Config Mode in Devices UI | Display SKU type (vPro / ISM / Non-vPro) and Configuration mode (ACM / CCM / Pre-Prov) in the Devices list of the OpenAMT UI. Use the existing deviceInfo JSON column in MPS; add the same column to Console if not already present. |
| console#858 — Device Discovery | Add a discovery capability so customers gain visibility into AMT-capable devices across their network. A lightweight agent (likely extending rpc-go) collects AMT/ME details (FW version, SKU, control mode, TLS mode, network config, UPID, cert hashes) and platform details (OS, LMS, CPU, hostname, IP, ethernet adapters, monitor presence) and reports them back to Console. Console ingests, stores, and visualises the data via a dashboard with filtering, sorting, capability breakdowns, and export support. |
| rpc-go#1246 — Detect LMS During Orchestration | Detect whether Intel LMS (Local Manageability Service) is installed on a device during the v3 orchestration/registration process. rpc-go reports LMS presence in the registration payload; Console captures and persists it in a new is_lms_available field in the device information table. UI enhancements based on LMS presence are out of scope for this story. |
The table below maps each data field required by the three issues against what is currently stored.
| Required Field | Required By | Currently in MPS/Console? | Notes |
|---|---|---|---|
| CSME FW Version | Discovery, SKU/Mode UI | ✅ deviceInfo.fwVersion
|
Already stored in deviceInfo JSON |
| SKU type label (vPro / ISM / Non-vPro) | Discovery, SKU/Mode UI | deviceInfo.fwSku string |
SKU is stored as a raw number; decoded label (features field) exists but is not a structured enum |
| Control Mode (ACM / CCM / Pre-Prov) | Discovery, SKU/Mode UI | ✅ deviceInfo.currentMode
|
Stored as "0"/"1"/"2"; UI label decoding needed |
| Provisioned State | Discovery | ✅ Derivable from currentMode
|
0 = pre-provisioned, 1/2 = provisioned |
| TLS Mode | Discovery | ❌ Not stored | Not in deviceInfo or any column |
| DHCP vs Static | Discovery | deviceInfo.ipAddress only |
No DHCP flag, gateway, or subnet stored |
| 802.1x / wireless config | Discovery | ❌ Not stored | Available via Console AMT API (/amt/networkSettings) but not persisted |
| AMT certificate hashes | Discovery | ❌ Not stored | Available during activation payload but not persisted in deviceInfo
|
| UPID | Discovery | ❌ Not stored | Not collected anywhere today |
| AMT enabled in BIOS | Discovery | ❌ Not stored | Collectable via rpc-go but not stored |
| ME Interface Version | Discovery | ❌ Not stored | Not in deviceInfo
|
| LMS version / installed | Discovery, LMS Orchestration | ❌ Not stored | Not collected today |
| OS details (distro, version) | Discovery | ❌ Not stored | Not collected today |
| CPU details | Discovery | ❌ Not stored | Available via Console CIM API (/amt/hardwareInfo) but not persisted |
| OS IP address | Discovery | deviceInfo.ipAddress is AMT IP |
OS IP may differ from AMT IP |
| Number of ethernet adapters | Discovery | ❌ Not stored | Not collected today |
| Monitor connected | Discovery | ❌ Not stored | Not collected today |
| OS hostname | Discovery | ✅ hostname column |
Already a top-level column |
| DNS suffix (AMT) | Discovery | ✅ dnsSuffix column |
Already a top-level column |
| Last reported/discovered time | Discovery (NFR) | ✅ deviceInfo.lastUpdated
|
Exists in deviceInfo JSON |
| Export support | Discovery (NFR) | Console has an export usecase but not wired for discovery data |
-
MPS is the system of record for CIRA connection state and the
deviceInfoJSON blob. It does not store credentials. -
Console mirrors
deviceInfo, adds credentials/TLS fields, and is the entry point for all AMT management actions. It has live APIs to query most of the missing fields (network, hardware, features) but does not persist them back to the database. - The
deviceInfoJSON column is the natural extension point for adding new discovery fields without a disruptive schema change. All new fields — both AMT/ME and OS/platform — are added as flat top-level keys, consistent with the existing flat AMT keys. Fields that are queried live today (network settings, hardware info, features) should be persisted into this blob by the discovery agent on each report cycle. - Fields not yet collected anywhere (UPID, LMS version, OS distro, monitor detection, 802.1x state) require new collection logic.
rpc amtinfoalready collects AMT data and automatically falls back to OS-only data when AMT is unavailable — extending its sync payload to include OS/platform fields is incremental work within rpc-go.
The three features require extending the existing devices table with additional persisted data. All new fields are stored in the single deviceInfo JSON column as flat top-level keys — consistent with the existing flat AMT keys and fully backward-compatible. A separate discoveryInfo column is not introduced — deviceInfo is already schemaless, so splitting into a second column would add DTOs, API plumbing, and duplicate SQL + Mongo backend implementations with no querying or migration benefit.
| Concern | Storage Location | Rationale |
|---|---|---|
| AMT/ME firmware state (SKU, mode, TLS, UPID) |
deviceInfo JSON — flat keys, both MPS and Console |
Consistent with existing flat AMT keys; fully backward-compatible; RPS and rpc amtinfo --sync already populate this blob |
| OS and platform state (LMS, OS details, network adapters) |
deviceInfo JSON — flat keys, Console only |
Same schemaless column; flat keys are consistent and require no struct nesting in the Go DTOs |
LMS presence at registration (lmsInstalled) |
deviceInfo.lmsInstalled — Console only, partial JSON merge at registration |
No new column needed; deviceInfo already accepts partial JSON merges |
| Credentials and TLS connection config | Existing Console-only columns — no change | Already correctly separated |
Live connection state (connectionStatus, lastConnected) |
Existing MPS columns — no change | Updated at CIRA runtime; not a discovery concern |
The diagram below shows the current entity structure and the proposed additions. Each field is annotated with its presence — M = MPS, C = Console, M+C = both — and new fields are marked with <<new>>.
erDiagram
DEVICES {
uuid guid PK "M+C"
string hostname "M+C"
string dnsSuffix "M+C"
boolean connectionStatus "M+C"
string mpsusername "M+C"
string mpsInstance "M+C"
string tenantId "M+C"
string friendlyName "M+C"
string tags "M+C"
timestamp lastConnected "M+C"
timestamp lastSeen "M+C"
timestamp lastDisconnected "M+C"
json deviceInfo "M+C"
string username "C"
string password "C"
string mpspassword "C"
string mebxpassword "C"
boolean useTLS "C"
boolean allowSelfSigned "C"
string certHash "C"
}
DEVICE_INFO_AMT {
string fwVersion "M+C"
string fwBuild "M+C"
string fwSku "M+C"
string currentMode "M+C"
string features "M+C"
string ipAddress "M+C"
timestamp lastUpdated "M+C"
string tlsMode "M+C <<new>>"
json upid "M+C <<new>>"
boolean amtEnabledInBIOS "M+C <<new>>"
string meInterfaceVersion "M+C <<new>>"
boolean dhcpEnabled "M+C <<new>>"
string[] certHashes "M+C <<new>>"
string lmsVersion "C <<new>>"
boolean lmsInstalled "C <<new>>"
string osName "C <<new>>"
string osVersion "C <<new>>"
string osDistro "C <<new>>"
string cpuModel "C <<new>>"
string osIpAddress "C <<new>>"
int ethernetAdapterCount "C <<new>>"
boolean monitorConnected "C <<new>>"
boolean ieee8021xEnabled "C <<new>>"
timestamp lastDiscovered "C <<new>>"
}
DEVICES ||--|| DEVICE_INFO_AMT : "deviceInfo (JSON)"
These fields are added as flat top-level keys within the existing deviceInfo JSON blob, consistent with all existing keys (fwVersion, fwBuild, etc.) and fully backward-compatible. The write path is unchanged (RPS activation + rpc amtinfo --sync). rpc amtinfo already collects AMT data and falls back to OS-only data when AMT is unavailable, so OS/platform fields can be populated even without AMT connectivity.
| Field | Type | Example | Feature | Collection Method | Presence |
|---|---|---|---|---|---|
tlsMode |
string | "TLS 1.2" |
Discovery |
AMT_TLSProtocolEndpointCollection via WSMAN (/amt/tls/:guid) |
M + C |
upid |
json | {"oemPlatformIdType":"Not Set (0)","oemId":"","csmeId":"4A45A39C5ED94620…82510000"} |
Discovery |
UPID.MarshalJSON() in rpc-go pkg/upid — already in InfoResult; keys: oemPlatformIdType, oemId, csmeId
|
M + C |
amtEnabledInBIOS |
boolean | true |
Discovery |
AMT_BootSettingData via WSMAN or rpc-go amtinfo |
M + C |
meInterfaceVersion |
string | "16.1.25.2124" |
Discovery |
CIM_SoftwareIdentity (ME version) — via GET /amt/version/:guid in Console |
M + C |
dhcpEnabled |
boolean | true |
Discovery |
AMT_EthernetPortSettings.DHCPEnabled — already in GET /amt/networkSettings/:guid
|
M + C |
certHashes |
string[] | ["a1b2c3…"] |
Discovery | Sent in rpc-go activation payload (MessagePayload.CertificateHashes) — already collected, not persisted |
M + C |
Key: M = MPS · C = Console · M + C = both
These fields describe the OS environment and platform state rather than AMT configuration. They are stored as flat top-level keys within the existing deviceInfo JSON column (Console only) — no new column or sub-object is introduced.
Key: for Presence M = MPS · C = Console · M + C = both
| Field | Type | Example | Feature | Collection Method | Presence |
|---|---|---|---|---|---|
lmsVersion |
string | "2410.5.0.0" |
Discovery, LMS Orchestration |
rpc amtinfo --sync (OS collection) — OS package query or LMS API |
C |
lmsInstalled |
boolean | true |
Discovery, LMS Orchestration |
rpc amtinfo --sync (OS collection) — check process/service presence; partial write at registration (#1246), enriched at discovery scan |
C |
osName |
string |
"linux", "windows"
|
Discovery |
rpc amtinfo --sync (OS collection) — runtime.GOOS
|
C |
osVersion |
string |
"6.8.0-51-generic", "10.0.26100"
|
Discovery |
rpc amtinfo --sync (OS collection) — OS version API |
C |
osDistro |
string |
"Ubuntu 24.04 LTS", "" (Windows) |
Discovery |
rpc amtinfo --sync (OS collection) — Linux: /etc/os-release; Windows: WMI |
C |
cpuModel |
string | "Intel(R) Core(TM) Ultra 7 165H" |
Discovery |
rpc amtinfo --sync (OS collection) — Linux: /proc/cpuinfo; Windows: WMI Win32_Processor
|
C |
osIpAddress |
string | "10.49.76.163" |
Discovery |
rpc amtinfo --sync (OS collection) — net.InterfaceAddrs()
|
C |
ethernetAdapterCount |
int | 2 |
Discovery |
rpc amtinfo --sync (OS collection) — net.Interfaces()
|
C |
monitorConnected |
boolean | true |
Discovery |
rpc amtinfo --sync (OS collection) — Windows: EnumDisplayMonitors; Linux: EDID/DRM check |
C |
ieee8021xEnabled |
boolean | false |
Discovery |
rpc amtinfo --sync (OS collection) — Linux: nmcli/wpa_supplicant config; Windows: WMI Win32_NetworkAdapterConfiguration
|
C |
lastDiscovered |
timestamp | "2026-05-14T10:23:00Z" |
Discovery (NFR) | Set by rpc amtinfo --sync on each report cycle |
C |
rpc-go#1246 explicitly calls for storing LMS presence in the "device information table". All fields in deviceInfo are flat, so lmsInstalled is simply added as a flat key alongside the existing AMT fields.
Decision: deviceInfo.lmsInstalled (flat)
All LMS state is stored as flat keys in deviceInfo. Concretely:
- The boolean from rpc-go#1246 maps to
deviceInfo.lmsInstalled— no new column or sub-object is needed. - The Console registration handler performs a partial JSON merge into
deviceInfo, writing only{ "lmsInstalled": <value> }when the device registers, leaving all other new fields absent until a discovery scan runs. - A subsequent discovery scan enriches the same blob with
lmsVersion,osName, and the remaining platform fields.
This stores all LMS state in one place, requires no new column (and therefore no new DTO, no new Repository method, and no duplicate SQL + Mongo implementations), and ensures LMS presence is captured immediately at registration even for devices that never undergo a full discovery scan.
This section maps the existing API surface against the three features, identifying which endpoints need modification and which new endpoints must be introduced.
rpc-go already supports two mechanisms for managing a device's presence in Console. The three new features extend both paths.
Path A — Local activation (direct to Console as part of next branch):
rpc activate --local --profile <profile.yaml> \
--auth-endpoint <console>/api/v1/authorize \
--auth-username <user> --auth-password <pass>- Authenticate with Console:
POST /api/v1/authorize→ JWT token - Register device in Console before AMT orchestration:
POST /api/v1/deviceswith aDevicePayloadstruct carrying GUID, hostname, AMT/MEBx/MPS credentials, and TLS configuration flags - Run orchestrator (CCM/ACM activation, optional CIRA configuration)
- If CIRA configuration fails: clear MPS password via
PATCH /api/v1/devices { "guid": "...", "mpspassword": "" }
Path A — Local deactivation (direct to Console as part of next branch):
rpc deactivate --local --auth-endpoint <console>/api/v1/authorize \
--auth-username <user> --auth-password <pass>- Resolve device GUID from AMT (before unprovisioning — AMT is unavailable afterwards)
- Locally unprovision AMT
- Delete device from Console:
DELETE /api/v1/devices/{guid}
Path B — Remote activation via RPS (unchanged):
rpc activate -u wss://rps.example.com --profile <profile>rpc-go sends a MessagePayload to RPS over WebSocket. This path does not call POST /api/v1/devices directly from rpc-go.
The new LMS detection (rpc-go#1246) must be propagated through both paths, and Console's registration handler must persist the flag into deviceInfo.lmsInstalled.
| # | Endpoint | Service | Type | Feature |
|---|---|---|---|---|
| 1 | POST /api/v1/devices |
Console only | Modify — accept isLMSAvailable; write to deviceInfo.lmsInstalled
|
LMS Orchestration |
| 2 | GET /api/v1/devices[/:guid] |
Console only | Modify — return all deviceInfo fields including new flat keys |
Discovery, SKU/Mode UI |
| 3 | GET /api/v1/devices |
Console only | Modify — add filter/sort query params | Discovery |
| 4 | GET /api/v1/devices/export |
Console only | New | Discovery |
| 5 | PATCH /api/v1/devices |
Console + MPS | Modify — extend deviceInfo with new flat fields; fix Console wiring — payload currently received but dropped; returns 404 for unknown GUIDs (no server-side upsert — agent handles 404 with a POST fallback) |
Discovery, SKU/Mode UI, LMS Orchestration |
| 6 |
rpc amtinfo --sync payload |
rpc-go | Modify — extend syncDeviceInfo with new flat fields; rpc amtinfo already falls back to OS-only when AMT is unavailable; add 404→POST fallback in SyncDeviceInfo: on 404 from PATCH, call POST /api/v1/devices to create a minimal connectionStatus=discovered record, then re-issue the PATCH
|
Discovery |
| 7 | rpc-go registration payloads | rpc-go | Modify — add isLMSAvailable (Path A DevicePayload) + lmsInstalled (Path B MessagePayload) |
LMS Orchestration |
Feature: LMS Orchestration (rpc-go#1246)
Change: The existing device registration endpoint must accept a new isLMSAvailable boolean field in the request body and persist it as the flat key deviceInfo.lmsInstalled (a partial JSON merge into deviceInfo; all other new fields remain absent until a discovery scan runs).
This is a Console-side change that receives the value sent by rpc-go Path A (local activation). The DevicePayload struct in rpc-go internal/device/api.go must be extended with the field:
IsLMSAvailable bool `json:"isLMSAvailable"`| JSON field | Maps to | Type | Write event |
|---|---|---|---|
isLMSAvailable |
deviceInfo.lmsInstalled |
boolean | Device registration via Path A |
Console handler must read isLMSAvailable from the POST body and perform a partial JSON merge into the deviceInfo column: { "lmsInstalled": <value> }. No new column is created; the existing deviceInfo JSON blob is updated in place. No equivalent change is needed in MPS.
2. GET /api/v1/devices and GET /api/v1/devices/:guid — Return all deviceInfo fields (Console — Modify)
Feature: Device Discovery, SKU/Mode UI
Change: Ensure all deviceInfo fields are returned in the response DTO, including all new flat keys. New fields absent from a given device (not yet collected) should be omitted (omitempty).
MPS's equivalent GET /api/v1/devices endpoints return only the AMT fields they know about — OS/platform fields are Console-only.
Feature: Device Discovery
Change: The discovery dashboard requires server-side filtering and sorting on the new fields. Extend the existing GET /api/v1/devices to support:
| Parameter | Example | Purpose |
|---|---|---|
filter[skuType] |
vPro |
Filter by decoded SKU label |
filter[currentMode] |
ACM |
Filter by control mode |
filter[lmsInstalled] |
true |
Filter by LMS presence |
filter[osName] |
Windows |
Filter by OS |
sort |
hostname, fwVersion
|
Sort column |
order |
asc / desc
|
Sort direction |
These map to new SQL query clauses against the deviceInfo JSON column (using JSON path operators for the new flat keys).
Feature: Device Discovery (NFR) Change: Add a new endpoint that returns all discovery data as a downloadable file (CSV or JSON).
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/devices/export |
Returns all devices with all deviceInfo fields as CSV/JSON |
Query parameters: format=csv|json, same filter parameters as the list endpoint.
5. PATCH /api/v1/devices — Extend deviceInfo with new flat fields + fix Console wiring (Console — Modify)
Feature: Discovery, SKU/Mode UI, LMS Orchestration
Change: The request body's deviceInfo object must accept all new flat keys. The existing PATCH handler returns 404 for unknown GUIDs — this is correct REST behaviour and is not changed. Unmanaged devices are handled client-side in rpc amtinfo --sync (see §6 below).
| Scenario | Handler behaviour |
|---|---|
| GUID matches an existing device record | Merge the deviceInfo JSON fields into the existing row (existing behaviour, just extended to persist the payload) |
| GUID is unknown | Return 404 — agent handles this by calling POST /api/v1/devices first, then re-issuing the PATCH
|
Required wiring fix: Console currently receives the
deviceInfopayload fromrpc amtinfo --syncbut drops it without persisting —DeviceInfois commented out indtoToEntity/entityToDTOand"deviceinfo"is absent fromdeviceFieldSetters. The PATCH handler must be updated to deserialise the payload and merge it into thedeviceInfocolumn. Until this is fixed, no new fields sent byrpc amtinfo --syncwill be stored.
rpc amtinfo already falls back to OS-only data collection when AMT is unavailable on a device, so OS/platform fields can be populated even for non-activated devices.
AMT/ME fields to add to the accepted deviceInfo payload:
| Field | Type | Populated By |
|---|---|---|
tlsMode |
string |
rpc amtinfo --sync (reads AMT_TLSProtocolEndpointCollection) |
upid |
json |
rpc amtinfo --sync — UPID.MarshalJSON() emits {oemPlatformIdType, oemId, csmeId}; stored as a nested JSON object within deviceInfo
|
amtEnabledInBIOS |
boolean |
rpc amtinfo --sync (reads AMT_BootSettingData) |
meInterfaceVersion |
string |
rpc amtinfo --sync (reads CIM_SoftwareIdentity) |
dhcpEnabled |
boolean |
rpc amtinfo --sync (reads AMT_EthernetPortSettings.DHCPEnabled) |
certHashes |
string[] | RPS activation payload (MessagePayload.CertificateHashes) |
Console must deserialise these into the DeviceInfo struct and merge them into the deviceInfo JSON column. No schema migration is required — deviceInfo is already a TEXT/JSON column.
OS/platform fields to add to the accepted deviceInfo payload:
| Field | Type | Example | Populated By |
|---|---|---|---|
lmsInstalled |
boolean | true |
rpc amtinfo --sync + registration (rpc-go#1246) |
lmsVersion |
string | "2410.5.0.0" |
Discovery scan |
osName |
string |
"linux", "windows"
|
rpc amtinfo --sync (OS fallback) |
osVersion |
string |
"6.8.0-51-generic", "10.0.26100"
|
rpc amtinfo --sync (OS fallback) |
osDistro |
string |
"Ubuntu 24.04 LTS", "" (Windows) |
rpc amtinfo --sync (OS fallback) |
cpuModel |
string | "Intel(R) Core(TM) Ultra 7 165H" |
Discovery scan |
osIpAddress |
string | "10.49.76.163" |
rpc amtinfo --sync (OS fallback) |
ethernetAdapterCount |
int | 2 |
Discovery scan |
monitorConnected |
boolean | true |
Discovery scan |
ieee8021xEnabled |
boolean | false |
Discovery scan |
lastDiscovered |
timestamp | "2026-05-14T10:23:00Z" |
Discovery scan |
Feature: Discovery, LMS Orchestration
Change: Extend the syncDeviceInfo struct to include all new flat fields: the AMT/ME fields (tlsMode, upid, amtEnabledInBIOS, meInterfaceVersion, dhcpEnabled, certHashes) and the OS/platform fields. rpc amtinfo already falls back to OS-only data collection when AMT is unavailable on a device — the extended sync payload can therefore carry OS/platform fields even on unactivated devices. The SyncDeviceInfo function collects and includes all fields in the PATCH body as flat keys within the deviceInfo object.
Note: Console currently drops the
deviceInfopayload fromrpc amtinfo --syncwithout persisting it (see item #5). The Console PATCH wiring fix is a prerequisite for any extendedsyncDeviceInfofields to be stored.
Feature: LMS Orchestration (rpc-go#1246)
Change: rpc-go detects LMS presence via TCP probe to localhost:16992 (or :16993 for AMT 19+ with local TLS) before sending any registration payload. The LMS flag must be added to both registration paths:
Path A — Local activation (DevicePayload → Console): Add IsLMSAvailable bool json:"isLMSAvailable" to the DevicePayload struct in internal/device/api.go. Console receives it via POST /api/v1/devices and stores it as deviceInfo.lmsInstalled (see item #1).
Path B — Remote activation (MessagePayload → RPS): Extend the MessagePayload struct in internal/rps/message.go to include LmsInstalled bool json:"lmsInstalled". RPS receives this over WebSocket during activation, then POSTs the device record to ${mps_server}/api/v1/devices — in legacy deployments this is MPS; in Console v3 deployments mps_server is reconfigured to point at Console, so the write goes directly to Console. RPS does not need code changes for the routing; only the payload struct needs extending.
Discovery data collection is a one-shot operation — rpc amtinfo --sync runs, collects, reports, and exits. Scheduling, periodic cadence, and reboot persistence are delegated to the OS platform scheduler. No long-running daemon or service harness is required.
| Platform | Recommended Mechanism | Notes |
|---|---|---|
| Windows |
Task Scheduler (XML task via schtasks) |
Runs as SYSTEM; triggers at system startup + repeating interval; survives reboots; no SCM service registration required |
| Linux |
systemd timer (.timer + Type=oneshot .service unit pair) |
Standard on Ubuntu 18.04+; OnBootSec + OnUnitActiveSec for boot + interval; integrates with journald for log capture |
| Linux (fallback) | cron | Adequate for basic periodic execution; add @reboot entry for boot-time start |
A Windows SCM service or a continuous Type=notify systemd unit would both require an in-process sleep loop and a service harness library (kardianos/service) solely to replicate what the OS scheduler already provides natively. Task Scheduler and systemd timers also produce cleaner failure logs (each run is a distinct journal / Event Log entry) and allow the binary to be updated without a service stop/start cycle.
Three approaches are feasible. All produce or reuse a Go binary consistent with rpc-go.
A new dedicated binary (rpc-discovery) published from a new repository, running as a long-lived service and shelling out to the rpc binary for AMT data collection.
rpc-discovery (new binary, new repo)
├── service harness (kardianos/service)
├── in-process scheduler (sleep loop)
├── AMT collector → subprocess: rpc amtinfo --all --json
└── Console reporter → PATCH <console>/api/v1/devices
| Pros | Cons |
|---|---|
| Independent versioning from rpc-go | New repository duplicates rpc-go's existing CI, cross-compile matrix, GitHub Releases pipeline, SECURITY.md, CONTRIBUTING.md, and governance setup |
| Clean separation of concerns | Two binaries must be co-deployed and kept in version-sync on every device |
JSON output of rpc amtinfo --json becomes a soft API between two binaries — silent breakage risk on rpc-go output changes |
|
| Subprocess invocation adds process-spawn overhead per scan cycle | |
| The OS scheduler already handles scheduling — the service harness adds complexity for no gain |
Not recommended. rpc-go already cross-compiles for all target platforms, has GitHub Actions CI, and ships via GitHub Releases. A new repository duplicates all of that plus governance overhead. Extending rpc amtinfo --sync directly (Approach C) eliminates the subprocess model and the second artifact entirely.
Add an agent subcommand to the existing rpc binary. When invoked as rpc agent start, the process registers as a Windows SCM service or systemd unit and runs continuously, waking on a configured interval.
rpc agent start → installs and starts the persistent service
rpc agent stop → stops the service
rpc agent status → reports current state
| Pros | Cons |
|---|---|
| Single binary to distribute — no new artifact | A persistent process requires an in-process sleep loop and service signal handling (SCM SERVICE_CONTROL_STOP, etc.) for the sole purpose of scheduling a periodic action that the OS scheduler already provides natively |
| Self-contained — no OS scheduler configuration required | Updating the binary requires stopping and restarting the service |
| Harder to inspect and control via standard IT tooling compared to a Task Scheduler task or systemd timer |
Note: the CLI-vs-DLL concern raised in earlier drafts does not apply. The
rpcCLI binary and thec-sharedlibrary are already separate build artifacts from separate source entry points; anagentsubcommand in the CLI binary has no impact on the DLL surface.
Not selected for v1. The OS scheduler provides all required capabilities (boot-time start, periodic interval, SYSTEM account, restart on failure, log capture) with zero in-process logic. A persistent daemon can be reconsidered if requirements emerge that the OS scheduler cannot satisfy (e.g. real-time streaming, sub-minute intervals, or complex retry queuing).
Extend the existing rpc amtinfo --sync command in rpc-go to also collect OS/platform fields (LMS probe, OS details, CPU, adapters, monitor detection). The binary runs once and exits; the OS scheduler provides the periodic cadence.
rpc amtinfo --sync --url <console>/api/v1/devices
├── AMT collection (existing: WSMAN via MEI/LME or network)
├── OS collection (new: LMS probe, OS name/version, CPU, adapters, monitor)
├── PATCH /api/v1/devices (update existing device — returns 404 if unknown)
└── POST /api/v1/devices (on 404 of PATH: create minimal "discovered" record, then re-PATCH)
| Pros | Cons |
|---|---|
| No new binary, no new repository, no new CI pipeline |
rpc amtinfo --sync gains OS-collection responsibilities beyond its current AMT-only scope |
| Leverages existing rpc-go cross-compile matrix (Windows / Linux / macOS, amd64 / arm64) and release pipeline | OS-level collection (LMS probe, monitor detection) requires platform-specific Go code in rpc-go |
| Single binary already deployed on managed devices today | Non-vPro devices have no GUID — requires a fallback identification strategy (see Open Design Questions) |
| OS scheduler handles retry, reboot persistence, and interval with zero in-process logic | |
No service harness dependency — kardianos/service not required |
|
| Binary can be updated by replacing the file; no service stop/start cycle needed | |
CLI and c-shared builds are already separate artifacts — no build conflict |
rpc amtinfo already falls back to OS-only data collection when AMT is unavailable, so the same binary works on non-vPro and pre-activation devices.
Approach C — extend rpc amtinfo --sync + OS scheduler. No new binary. No service harness. rpc-go already provides CI, cross-compile, governance, and a release pipeline. The OS scheduler provides native scheduling, reboot persistence, and failure logging. See Open Design Questions for unresolved questions that must be answered before implementation.
No new Console endpoints are needed for agent distribution. Operators obtain the rpc binary from the existing rpc-go GitHub Releases channel and register the OS scheduler task directly, embedding the Console URL and a scoped JWT as command arguments or environment variables.
Bootstrap flow:
sequenceDiagram
actor Operator
participant GHRelease as GitHub Releases (rpc-go)
participant Device as Target Device
participant Console
participant Scheduler as OS Scheduler
Operator->>GHRelease: Download rpc binary for platform
GHRelease-->>Operator: rpc binary (application/octet-stream)
Note over Operator: Generate scoped JWT via POST /api/v1/authorize
Operator->>Console: POST /api/v1/authorize
Console-->>Operator: Bearer JWT
Operator->>Device: Deploy rpc binary
Note over Device: Register OS scheduler task with Console URL + JWT embedded
loop Every scheduled interval (default: 1 hour)
Scheduler->>Device: rpc amtinfo --sync --url <console>/api/v1/devices --token <JWT>
Device-->>Console: PATCH /api/v1/devices (Bearer JWT)
alt Device exists
Console-->>Device: 200 OK
else Device unknown (404)
Device->>Console: POST /api/v1/devices (minimal discovered record)
Console-->>Device: 201 Created
Device->>Console: PATCH /api/v1/devices (deviceInfo payload)
Console-->>Device: 200 OK
end
Device-->>Scheduler: exit 0
end
Windows — Task Scheduler (runs as SYSTEM with highest privileges):
Save the following as rpc-amtinfo-sync.xml, then import it with:
schtasks /create /xml rpc-amtinfo-sync.xml /tn "DMT\rpc amtinfo sync"<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers>
<BootTrigger>
<Delay>PT5M</Delay>
<Enabled>true</Enabled>
</BootTrigger>
<TimeTrigger>
<Repetition>
<Interval>PT1H</Interval>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>2026-01-01T00:00:00</StartBoundary>
<Enabled>true</Enabled>
</TimeTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>S-1-5-18</UserId> <!-- SYSTEM account -->
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<ExecutionTimeLimit>PT10M</ExecutionTimeLimit>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
</Settings>
<Actions>
<Exec>
<Command>C:\Program Files\rpc\rpc.exe</Command>
<Arguments>amtinfo --sync --url https://console.example.com/api/v1/devices --token <scoped-JWT></Arguments>
</Exec>
</Actions>
</Task>Why SYSTEM? The MEI/HECI driver on Windows restricts device access to elevated processes. Running as SYSTEM (or a member of the local Administrators group) is required to open the Intel MEI device handle that
rpcuses to query the AMT UUID and ME firmware version.
Linux — systemd timer (runs as root):
# /etc/systemd/system/rpc-amtinfo-sync.service
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/rpc amtinfo --sync --url https://console.example.com/api/v1/devices --token <scoped-JWT>
# /etc/systemd/system/rpc-amtinfo-sync.timer
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
[Install]
WantedBy=timers.targetWhy root?
/dev/mei0is owned byroot:meiwith mode0660. Therpcbinary must either run as root or the service user must be a member of themeigroup. Running as root is the most portable choice across distributions; operators who prefer least-privilege can setUser=rpc-agentand add that user to themeigroup instead.
The reporting interval is set in the scheduler task itself, keeping it adjustable via standard IT tooling (GPO, Ansible, MDM) with no Console API involvement.
Security considerations:
- The JWT should be scoped to the
discovery:writepermission only — it cannot manage devices or read credentials. - Console should support token revocation per device or per batch to allow decommissioning agents without rotating the global credential.
- For air-gapped or no-internet environments, the binary and a pre-populated scheduler task can be distributed via standard software deployment tooling (SCCM, Ansible, GPO, MDM).
All questions resolved. Decisions are recorded in the ADR § 3.
Resolved: use the SMBIOS System UUID as fallback when HECI is unavailable.
How rpc-go currently reads the UUID: rpc amtinfo calls pthi.Command.GetUUID(), which sends a GET_UUID_REQUEST over the HECI interface (/dev/mei0 on Linux, Intel MEI kernel driver on Windows) directly to the ME firmware. The ME firmware returns the 16-byte SMBIOS System UUID. This path is gated on heciAvailable — if the MEI driver is not loaded or the device has no AMT/ME, the UUID field is left empty in InfoResult and SyncDeviceInfo sends an empty GUID.
For non-vPro or pre-AMT devices (HECI unavailable), a SMBIOS fallback is needed. The SMBIOS System UUID is the same value the ME returns over HECI — both ultimately read from the BIOS DMI table — so the keys will match if the device is later activated.
Go package for the SMBIOS fallback: github.com/digitalocean/go-smbios reads the SMBIOS tables in pure Go on both platforms with no subprocess or admin rights:
| Platform | Underlying mechanism |
|---|---|
| Linux | Reads /sys/firmware/dmi/tables/ (kernel-exported, no root required on Linux 4.0+) |
| Windows | Calls GetSystemFirmwareTable("RSMB", ...) Win32 API (available to unprivileged processes) |
Note:
github.com/digitalocean/go-smbiosis not currently in rpc-go'sgo.mod. Adding it is a new dependency for the non-vPro fallback path.
Proposed fallback logic in GetAMTInfo:
// Existing path — HECI/PTHI:
if s.heciAvailable {
uuid, err := s.amtCommand.GetUUID()
// ...
result.UUID = uuid
}
// New fallback — SMBIOS (non-vPro / HECI unavailable):
if result.UUID == "" {
uuid, err := readSMBIOSSystemUUID() // new helper, uses go-smbios
if err != nil {
log.Warn("SMBIOS UUID not available, falling back to hostname: ", err)
result.UUID = "" // hostname fallback handled in SyncDeviceInfo
} else {
result.UUID = uuid
}
}The SMBIOS Type 1 structure at byte offset 0x08 holds the 16-byte UUID, formatted as a standard RFC 4122 string.
Why this is the right key:
- Same value as AMT GUID. Both the PTHI path and the SMBIOS path return the same OEM-assigned UUID from the BIOS DMI table. If HECI is later enabled or the device is activated, the record matches automatically.
- Globally unique. Assigned by the OEM; stable across reboots and OS reinstalls.
- No composite-key complexity. Console's upsert key remains a single UUID field regardless of how it was read.
If the SMBIOS table is not accessible (e.g. some hypervisors with no DMI passthrough), SyncDeviceInfo falls back to hostname as a last resort and logs a warning — uniqueness is not guaranteed in that case.
No Console API change is required. The existing GUID field in the PATCH /api/v1/devices payload is populated with the SMBIOS UUID for non-AMT devices; Console does not need to distinguish the source.
Resolved: rpc-go ships sample OS scheduler config files at a 1-hour default interval.
rpc-go releases include a sample .task.xml (Windows Task Scheduler) and .timer + .service unit pair (Linux) defaulting to a 1-hour repeat. Operators override the interval in their own tooling (GPO, Ansible, MDM) without touching the agent config file.
Resolved: exit non-zero and let the OS scheduler retry on the next cycle.
A one-cycle data gap is acceptable for v1. No local spool file — avoids local state management and stale-data edge cases. rpc amtinfo --sync logs the failure and exits with a non-zero code; the OS scheduler provides native retry semantics.
Resolved: not supported in v1. consoleURL is required.
The binary exits with a non-zero code if consoleURL is missing or unreachable. A future --output flag can add local JSON output without Console when needed — that is a v2 concern.