Skip to main content

Async Data-Collection Commands (RMM)

Status

Implemented — backend, agent, and operator UI. Branch feature/rmm-async-commands.

  • Taskboard OP-066i (Remote command UI) and its command-execution backend dependency.
  • RMM pillar: basic command lifecycle (AGENTS.md product boundary).

Problem Statement

Operators need to run on-demand actions against enrolled endpoints without inbound access. Phase 1 is bounded to data-collection commands — the agent is deliberately not a remote shell, patch manager, or software deployer.

Architectural Intent

Reuse the frozen command.* event contracts and the existing command_jobs / command_results projection. Delivery and result reporting ride the existing agent heartbeat round-trip, so no new long-lived channel or agent dependency is introduced.

What Was Implemented

  • POST /api/v1/commands queues a command (command.requestedcommand_jobs), idempotent on idempotency_key, role-gated (operator/tenant_admin/system_admin), audited, and rejected for assets with no enrolled agent.
  • Heartbeat response returns this asset's pending jobs (pending_commands) and marks them accepted; heartbeat request accepts command_results, projecting command.running/completed/failed.
  • Agent executes bounded collect_inventory / collect_telemetry against the current snapshot and reports results on the next heartbeat; unsupported types are reported failed, never shell-executed.
  • Operator UI (CommandPanel) on the asset detail: pick a template, run, and see recent status + result summary.

Components Involved

  • poc/ingest_api/command_service.py, agent_telemetry_service.py, http_routes.py, api_models.py
  • Agent: internal/telemetry/{commands,collector,reporter}.go, internal/heartbeat/heartbeat.go
  • Frontend: components/command-panel.tsx, console-api/oneprotect/commands/route.ts

APIs / Events / Schemas

  • New: POST /api/v1/commands (CommandRequest). Heartbeat request gains command_results; response item gains pending_commands (OpenAPI updated).
  • Events (existing, frozen): command.requested/accepted/running/completed/failed.
  • No new tables/migrations — command_jobs/command_results exist (PG migration 006 + RLS 003).

Deployment Notes

Deploy the platform image (no migration). Run the rebuilt agent (it consumes the new heartbeat contract). Command scope is collect_inventory/collect_telemetry.

Security / Tenant Isolation

Role-gated request; all queries tenant_id-scoped; a different tenant cannot target an asset (no agent in its scope → 403). Every request is audited (command.requested).

Validation Steps

UI Validation

Assets → select an enrolled host → Remote commands → run Collect inventory → status moves requested → accepted → succeeded with a result summary after the agent's next heartbeats.

API Validation

POST /api/v1/commands {asset_id, command_type: collect_inventory, idempotency_key}
GET /api/v1/commands?asset_id=... # status + result_summary

Smoke / Tests

  • tests/test_agent_telemetry_runtime.py: round-trip (request→deliver→report), scope (out-of-Phase-1 type → 400), tenant isolation + auditor denial (403).
  • tests/test_frontend_response_contracts.py: command request/list shape-pin.
  • Agent: internal/telemetry/commands_test.go.

Known Limitations

  • run_osquery_pack is out of Phase-1 scope (deferred).
  • Result payloads are summaries (result_summary); object-stored result artifacts are later work.

Follow-Up Work

  • Object-storage result artifacts + richer result rendering.
  • osquery pack execution.

Acceptance Criteria Mapping

OP-066i — operator selects asset, runs an approved command template, and sees pending/running/completed status and output. Satisfied via CommandPanel + the command lifecycle above.