Async Data-Collection Commands (RMM)
Status
Implemented — backend, agent, and operator UI. Branch feature/rmm-async-commands.
Related Requirements
- Taskboard OP-066i (Remote command UI) and its command-execution backend dependency.
- RMM pillar: basic command lifecycle (
AGENTS.mdproduct 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/commandsqueues a command (command.requested→command_jobs), idempotent onidempotency_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 themaccepted; heartbeat request acceptscommand_results, projectingcommand.running/completed/failed. - Agent executes bounded
collect_inventory/collect_telemetryagainst the current snapshot and reports results on the next heartbeat; unsupported types are reportedfailed, 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 gainscommand_results; response item gainspending_commands(OpenAPI updated). - Events (existing, frozen):
command.requested/accepted/running/completed/failed. - No new tables/migrations —
command_jobs/command_resultsexist (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_packis 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.