Persist a complete per-machine device inventory on connect_machines (OS+locale+install, CPU/RAM, mfr/model/serial, external WAN IP captured server-side via trusted-proxy client_ip + private LAN IP + MAC, logged-on user, idle, time zone, uptime, local-admin-present), refreshed each AgentStatus and surfaced in the dashboard machine detail — ScreenConnect "Guest Info" parity. Data layer for SPEC-002 Phase 2; closes the GC side of the agent-IP gap (coord todo 7459428e). Requested by Mike 2026-05-30. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
SPEC-003: Full Machine Inventory in the Connection Database
Status: Proposed Priority: P2 Requested By: Mike (2026-05-30) Estimated Effort: Large
Overview
Persist a complete per-machine device inventory on connect_machines — refreshed
from the agent on each status cycle and stamped server-side with the real external
IP — and surface it in the dashboard machine-detail view, bringing GuruConnect to
parity with ScreenConnect's "Guest Info" panel. Today a machine record holds only
hostname, os_version (literally std::env::consts::OS → "windows"),
is_elevated, organization, site, tags, and timestamps; a technician cannot
answer "what is this box, who's on it, what's its WAN/LAN IP, what's its MAC" from
GuruConnect at all. Success = every connected agent shows hostname, company, site,
logged-on user, idle time, OS (full string + locale + install date), CPU, memory,
manufacturer/model, serial, external WAN IP, private LAN IP, MAC, client
version, time zone, uptime, and local-admin-present in the machine detail, kept
current as the agent reconnects.
Reference example (ScreenConnect Guest Info — DESKTOP-VRBQ6LM, Pavon / Curves):
Network Address 174.78.94.186, Private Network Address 192.168.1.128, MAC
04:42:1A:0C:8C:A6, Client Version 26.1.24.9579, TZ (UTC-07:00) Arizona, Uptime
33d 4h, Local Admin Present Yes, Logged On User DESKTOP-VRBQ6LM\GeoVision, OS
Microsoft Windows 11 Pro (10.0.26200) en-US, OS install 3/15/2025, CPU i7-11700 (16 virtual) X64, Mem 5260 / 16140 MB, Mfr/Model ASUS System Product Name.
This is the data-layer companion to SPEC-002 Phase 2, which builds the "machines inventory/history/delete" dashboard surface (SPEC-002 §"Phase 2"). SPEC-003 defines what data exists and how it is collected; SPEC-002 renders it. It also closes the GuruConnect side of the GuruRMM agent-IP gap (coord todo
7459428e): GuruRMM has no IP fields at all, whereas GC already computes the client IP at connect time (utils::ip_extract::client_ip) and merely fails to store it.
Scope
Included in v1
- New nullable inventory columns on
connect_machines(migration 008). - A
DeviceInventoryprotobuf message reported by the agent (Windows collection via WMI/Win32 APIs), carried onAgentStatusand written through to the machine row. - Server-side capture of the external IP from the already-extracted
trusted-proxy
client_ip(never trust an agent-reported WAN IP). - Extend
MachineInfoAPI DTO +GET /api/machines/GET /api/machines/:agent_idto return the inventory. - Dashboard machine-detail rendering of the inventory (the Guest-Info-style panel).
- Derived/display-only fields composed from existing tables: Hosts/Guests Connected
and "Guest Last Connected" from
connect_sessionshistory.
Explicitly out of scope
- macOS / Linux inventory collection — the agent is Windows-only today (cross-platform
agents are roadmap "Future Considerations" P3). The collector is structured so
non-Windows builds report
None/empty without breaking the wire or schema. - Editable/operator-authored fields (Department, Device Type, custom Attributes as
free-text the tech sets). v1 stores
department/device_typeas agent-reported or null; operator-editable metadata is a follow-up (needs a dashboard write path- audit).
- Live polling/push of inventory between status cycles, scheduled re-inventory jobs, and historical inventory diffing — v1 is "latest snapshot, refreshed on agent status".
- Pending Activity / screenshot thumbnail (separate concerns; screenshot is SPEC-002 viewer work).
Architecture
Agent (agent/src/)
- New module
agent/src/inventory/mod.rs—collect() -> DeviceInventory. Windows implementation gathers:- OS:
Win32_OperatingSystem(Caption, Version, OSLanguage→locale, InstallDate, FreePhysicalMemory, TotalVisibleMemorySize). - System:
Win32_ComputerSystem(Manufacturer, Model, Domain/Workgroup, UserName→logged-on user, TotalPhysicalMemory). - CPU:
Win32_Processor(Name, NumberOfLogicalProcessors, Architecture). - Serial:
Win32_BIOS.SerialNumber(fallbackWin32_SystemEnclosure). - Network:
GetAdaptersAddresses(iphlpapi) for primary LAN IPv4 + MAC of the active adapter (skip loopback/virtual). - Idle time:
GetLastInputInfo(must run in the interactive session — see Open Questions; SYSTEM service sees its own idle, so query via the same session path the agent already uses for capture, or reportNonewhen unavailable). - Local admin present: enumerate the local
Administratorsgroup for any enabled non-builtin member (NetLocalGroupGetMembers). - Time zone:
GetDynamicTimeZoneInformation. - Uptime already available (
AgentStatus.uptime_secs).
- OS:
- Win7 SP1+ safe: all of the above are available on Win7/Server 2008 R2. Prefer the
wmicrate (pure-Rust over COM, no runtime redistributable — satisfies the "statically linked, no .NET/VC++" constraint) or directwindows-crate calls. - Collection cadence: inventory is near-static, so collect once at session start and
re-collect every ~15 min (or on a
display_count/IP change), not every heartbeat. Carry it onAgentStatus.inventoryonly when present; the lightweight status tick stays cheap. - Replace the bogus
os_version: std::env::consts::OSwith the real OS caption (keepos_versionfor back-compat; populate the richeros_namefrom inventory).
Protobuf (proto/guruconnect.proto)
- Add a
DeviceInventorymessage and attach it toAgentStatusat the next free field number 12 (fields 1–11 are taken;supports_h264 = 11):
message DeviceInventory {
string os_name = 1; // "Microsoft Windows 11 Pro (10.0.26200)"
string os_locale = 2; // "en-US"
int64 os_install_unix = 3; // OS install time, unix secs
string machine_domain = 4; // "WORKGROUP" or AD domain
string logged_on_user = 5; // "DESKTOP-VRBQ6LM\\GeoVision"
int64 idle_secs = 6; // -1 = unknown
string cpu_model = 7;
int32 cpu_logical = 8;
string cpu_arch = 9; // "X64"
int64 mem_total_mb = 10;
int64 mem_available_mb = 11;
string manufacturer = 12;
string model = 13;
string serial_number = 14;
string machine_description = 15;
string private_ip = 16; // LAN IPv4 of the active adapter
string mac_address = 17;
string time_zone = 18; // "(UTC-07:00) Arizona"
bool local_admin_present = 19;
string department = 20; // agent-config-reported (optional)
string device_type = 21; // agent-reported (optional)
}
message AgentStatus {
// ... existing fields 1-11 ...
DeviceInventory inventory = 12; // present periodically, not every tick
}
- External IP is not an agent-reported field (deliberately) — see Security.
DB schema (server/migrations/008_machine_inventory.sql)
ALTER TABLE connect_machines ADD COLUMN IF NOT EXISTS ...— all nullable, noNOT NULL(consistent with the table's existing NULL-tolerant reality and the manualFromRow):os_name TEXT, os_locale TEXT, os_install_at TIMESTAMPTZ, machine_domain TEXT, logged_on_user TEXT, idle_secs BIGINT, cpu_model TEXT, cpu_logical INT, cpu_arch TEXT, mem_total_mb BIGINT, mem_available_mb BIGINT, manufacturer TEXT, model TEXT, serial_number TEXT, machine_description TEXT, external_ip INET, private_ip INET, mac_address TEXT, time_zone TEXT, uptime_secs BIGINT, local_admin_present BOOLEAN, department TEXT, device_type TEXT, inventory_updated_at TIMESTAMPTZ.- Idempotent
ADD COLUMN IF NOT EXISTS, applied bysqlx::migrate!()on startup — never pre-applied via psql (see.claude/standards/gururmm/sqlx-migrations.md, and the 005→007 lesson: do not rely onIF NOT EXISTSto add constraints later). - Client Version reuses the existing version column written by
db::releases::update_machine_version— do not duplicate.
Server (server/src/)
db/machines.rs:- Extend the
Machinestruct + manualFromRow(machines.rs:30–107) with the new columns, eachOption<T>read NULL-tolerantly (same pattern asorganization). - New
update_machine_inventory(pool, agent_id, &DeviceInventory)(sibling ofupdate_machine_metadata, machines.rs:213) —COALESCEeach field so a partial snapshot never nulls a previously-known value; setinventory_updated_at = NOW(). - New
update_machine_external_ip(pool, agent_id, IpAddr).
- Extend the
relay/mod.rs:- At connect/upsert (mod.rs:591, where
client_ipis in scope) callupdate_machine_external_ip(agent_id, client_ip)so the WAN IP is stamped from the trusted-proxy-derived address. - In the
AgentStatushandler (mod.rs:779–835), whenstatus.inventoryis present, callupdate_machine_inventory(...)alongside the existingupdate_machine_metadata(...)write.
- At connect/upsert (mod.rs:591, where
api/mod.rs: extendMachineInfo(mod.rs:117) +From<Machine>(mod.rs:130) with the inventory fields (ISO-8601 for timestamps, IPs as strings).GET /api/machines,GET /api/machines/:agent_id(main.rs:385–386) return them unchanged in shape beyond the added fields. Compose Hosts/Guests-connected + "Guest Last Connected" fromdb::sessionsfor the detail endpoint.
Dashboard (dashboard/src/)
- Extend the
Machinetype (dashboard/src/api/types.ts) and machine-detail view with a Guest-Info-style inventory panel (Session / Device / Network groupings). Coordinate with SPEC-002 Phase 2 so this is one panel, not two.
Implementation details
- Files to touch:
proto/guruconnect.proto(~line 303,AgentStatus);agent/src/inventory/mod.rs(new) + wire intoagent/src/session/mod.rs:236(send_status);server/migrations/008_machine_inventory.sql(new);server/src/db/machines.rs:30,101,213;server/src/relay/mod.rs:591,824;server/src/api/mod.rs:117,130;dashboard/src/api/types.ts,dashboard/src/api/machines.ts, machine-detail component. - Key structs/messages:
DeviceInventory(proto),Machine(db),MachineInfo(api). idle_secs = -1, empty strings, and absent adapters all map toNULL/unknown in the DB — display as "—", never as0/falsemasquerading as real data.
Security considerations
- External IP is server-authoritative. Capture it only from
utils::ip_extract::client_ip(trusted-proxy-aware; ignores client-suppliedX-Forwarded-For/X-Real-IPfrom untrusted peers — see the module doc). An agent-reported WAN IP would be spoofable and is intentionally not modeled. - Auth unchanged: inventory flows over the already-authenticated agent WS (support code or per-machine key) and is served only on JWT-authenticated machine endpoints.
serial_number,logged_on_user,machine_domain, and local-admin presence are mildly sensitive; they are admin-dashboard-only (no new unauthenticated surface).- Input validation: treat all agent-reported strings as untrusted — length-cap each
field server-side before persisting (defense against a compromised/rogue agent
bloating the row);
bytea/oversized values rejected. - Audit: no new audit events required, but an inventory write is
debug!-logged with agent_id (no PII beyond what the admin already sees).
Testing strategy
- Unit:
inventory::collect()on Windows returns populated fields on a real box; non-Windows returns all-None.update_machine_inventoryCOALESCE semantics (partial snapshot preserves prior values).MachineInfoserialization round-trip. - Integration: agent → relay → DB: connect an agent, assert
connect_machinesgainsexternal_ip(matching the test'sclient_ip) at connect and the full inventory after the firstAgentStatus.inventory;GET /api/machines/:idreturns it. Verify a status tick without inventory does not null existing values. - Manual: enroll the Pavon/Raiders or a lab box, confirm the dashboard detail
matches the real machine (cross-check WAN IP against the box's actual egress IP,
LAN IP/MAC against
ipconfig /all).
Effort estimate & dependencies
- Size: Large. The bulk is the Windows inventory collector (WMI/Win32, Win7-safe)
and the dashboard panel; the proto/DB/relay/API wiring is mechanical and follows
the existing
organization/site/tagsprecedent end-to-end. - Depends on: nothing blocking. Reuses existing
client_ipextraction and theAgentStatuspipeline. - Unblocks / aligns with: SPEC-002 Phase 2 dashboard "machines inventory" surface;
resolves the GC side of coord todo
7459428e(agent IP tracking).
Open questions
- Idle time as a SYSTEM service.
GetLastInputInforeturns input idle for the calling session; a SYSTEM-context agent may not see the interactive user's idle. Does the agent already have an interactive-session path (it injects input/captures the active console) we can reuse, or do we reportidle_secs = -1when running headless? Resolve during planning. - Multiple NICs. Store only the primary active adapter's IP/MAC (v1), or all
adapters (
TEXT[])? ScreenConnect shows one; v1 proposes one. Revisit if multi-homed boxes matter. - Operator-editable Department / Device Type / Attributes. v1 treats these as agent-reported/null. Confirm whether the dashboard should also let a tech set them (adds a write endpoint + audit) now or as a follow-up spec.
- Re-inventory cadence. 15 min fixed vs. change-triggered vs. configurable — pick a default; 15 min proposed.