Brings azcomputerguru/guru-connect up to the authoritative working copy that had been maintained in the claudetools monorepo: Phase 1 security and infrastructure (middleware, metrics, utils, token blacklist, deployment scripts, security audits) plus the native-remote-control integration spec. Preserves the repo .gitignore, .cargo, and server/static/downloads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Native Remote Control — GC↔RMM Integration Contract & Embedded Viewer — Implementation Plan
Spec created: 2026-05-29 Status: not started Architecture: broker model — RMM orchestrates the separate GC agent, against a versioned integration contract that GC owns. Two independent products, kept in-sync by contract + capability discovery (NOT by shared pipelines). Repos: GC = guru-connect (standalone product, in claudetools repo) · RMM = guru-rmm (submodule).
End-to-end flow (target behavior)
Unattended: tech clicks Remote Control on AgentDetail → RMM checks GC capabilities, (1)
pre-creates a GC session bound to the endpoint's device_id and mints a short-lived viewer token,
(2) commands the endpoint's RMM agent to ensure the GC agent is installed (checksum-verified) and
connected in persistent mode → RMM embeds GC's viewer in the dashboard (scoped iframe) pointed
at that session, native guruconnect:// as fallback.
Attended: same, but RMM mints a support code on GC, the GC agent shows a consent prompt, and the session starts only after the end user accepts.
The contract surface (Tasks 1-3) is GC's; the broker + embed (Tasks 7-10) is RMM's.
Task 0: Commit this spec
git add projects/msp-tools/guru-connect/specs/native-remote-control/
git commit -m "spec: add native-remote-control shape spec"
Do not start Task 1 until this commit exists.
Task 1 (GC): Define & version the integration contract — KEYSTONE
Files touched: CONTRACT.md (new, GC repo root or docs/), server/src/main.rs (routes :254 /health,
:300 /api/version), server/src/api/ (new integration.rs), server/src/middleware/ (integration auth).
- Author a semver'd
CONTRACT.mddocumenting the GC integration surface (auth model, endpoints, payloads, capability flags, viewer embed protocol, error envelope). This is the artifact both teams keep "front of mind" — GC must not break a published version without a major bump. - Create the
/api/integration/v1/route namespace. GET /api/integration/capabilities(model on the existing public/api/versionatreleases.rs:76) →{ contract_version, features: { embedded_viewer, consent_prompt, per_machine_keys, programmatic_sessions } }. RMM reads this to version-gate.- Add server-to-server integration auth: a single integration credential
(
CONNECT_INTEGRATION_KEY, env/SOPS) required on all/api/integration/v1/*routes. Capabilities endpoint may be unauthenticated (like/api/version) so RMM can probe before configuring. - Error envelope per
api/response-format(detail/error_code/status_code).
Task 2 (GC): Per-machine agent keys
Files touched: server/migrations/0XX_agent_keys.sql (new), server/src/db/agent_keys.rs (new),
server/src/relay/mod.rs (validate_agent_api_key :187), server/src/api/integration.rs.
- Idempotent migration:
connect_agent_keys(id,agent_id,key_hash,created_at,revoked_at). Hashed keys only (model on RMMenroll.rsgenerate_api_key/hash_api_key). POST /api/integration/v1/agents/:agent_id/keysmints a per-machine key (plaintext once, store hash).validate_agent_api_key()accepts a valid DB per-machine key; sharedAGENT_API_KEYenv becomes a deprecated fallback. Supportrevoked_at.
Task 3 (GC): Programmatic session pre-create + viewer token
Files touched: server/src/api/integration.rs, server/src/session/mod.rs (register_agent :95),
server/src/db/sessions.rs (create_session :22), server/src/auth/jwt.rs, server/migrations/.
POST /api/integration/v1/sessions— body{ agent_id, mode }. Pre-creates a session row + in-memory slot keyed byagent_id, markedis_managed/source="gururmm"; returns{ session_id }. When the GC agent later registers with thatagent_id,register_agent()binds it to the pre-created session instead of generating a new one.POST /api/integration/v1/sessions/:id/viewer-token— short-lived (~5 min), session-scoped viewer JWT.- Add
is_managed BOOLEAN/source TEXTtoconnect_sessions(idempotent migration). - For attended, the broker reuses the existing
POST /api/codes(main.rs:382); expose/document it under the contract too.
Task 4 (GC): Embedded session viewer
Files touched: server/static/viewer.html, server/src/middleware/security_headers.rs:30,37-39,
server/src/main.rs (per-route header layer for the viewer), CONTRACT.md (embed protocol).
- Add a scoped framing allowlist for the viewer route(s):
frame-ancestors <RMM dashboard origin>(from env, e.g.CONNECT_EMBED_ALLOWED_ORIGINS) and matching/relaxedX-Frame-OptionsONLY on the viewer path. Every other route keepsframe-ancestors 'none'(:30) — do not weaken globally. - Add an embed mode to
viewer.html(e.g.?embed=1): hide standalone chrome, accept the session_id + viewer token from the host, and emitpostMessagelifecycle events (viewer:connected,viewer:disconnected,viewer:error,viewer:resize) for the RMM host to react to. Document this embed protocol inCONTRACT.md.
Task 5 (GC): Consent messages + attended prompt
Files touched: proto/guruconnect.proto (after AdminCommand :286),
agent/src/session/mod.rs, agent/src/consent/mod.rs (new) + agent/src/tray/mod.rs,
server/src/relay/mod.rs, server/src/session/mod.rs.
- Add
ConsentRequest { session_id, technician_name, reason }(server→agent) andConsentResponse { session_id, accepted }(agent→server). - GC agent: on
ConsentRequestin attended mode, show a native consent dialog; Decline → session refused + event logged. Unattended skips consent (gated by sessionmode). - Emit
connect_session_eventsfor consent shown/accepted/declined. Exposeconsent_promptin the capabilities map (Task 1).
Task 6 (GC): Session persistence / restart reconcile (robustness)
Files touched: server/src/session/mod.rs (:81), server/src/db/sessions.rs, server/src/main.rs.
- On startup, load active
connect_sessionsfrom DB intoSessionManagerso a relay restart does not orphan managed sessions; reap stale rows. This satisfies the "robust" requirement.
Task 7 (RMM): GC integration client (capability-aware) + config
Files touched: server/src/connect_client.rs (new), server/src/config (env wiring).
- Client for the GC
/api/integration/v1contract: base URL (CONNECT_SERVER_URL) + integration key (CONNECT_INTEGRATION_KEY), env/SOPS only. - On startup / first use, call
GET /api/integration/capabilities; cache the contract version + feature map and version-gate RMM behavior off it (e.g. only offer attended consent ifconsent_promptis true). Log a[WARNING]if the GC contract version is newer/older than expected. - Methods:
capabilities(),pre_create_session(device_id, mode),mint_viewer_token(session_id),mint_support_code(technician),provision_agent_key(device_id).
Task 8 (RMM): Broker endpoint
Files touched: server/src/api/remote_control.rs (new), server/src/api/mod.rs (:162 register),
server/src/db/ + server/migrations/0XX_remote_control_sessions.sql (new, or extend tech_sessions),
reuse command dispatch server/src/api/commands.rs:87-157.
POST /api/agents/:agent_id/remote-control— body{ mode }. Authz viaauthorize_agent_access. Steps: resolvedevice_id+online → (viaconnect_client) ensure per-machine key, pre-create session, attended→support code → dispatch launch command to the RMM agent (Task 9) → mint viewer token → return{ session_id, viewer_embed_url, viewer_native_url, mode, capabilities }.- Record a
remote_control_sessionsaudit row (agent_id,tech_id,connect_session_id,mode,started_at), mirroring thetunnel_auditpattern.
Task 9 (RMM agent): Ensure-and-launch GC agent
Files touched: agent/src/transport/websocket.rs (run_command :1050, execute_command :971),
agent/src/remote_control/mod.rs (new), agent/src/config.rs, agent/src/service.rs (AppState parity).
- New launch path (Windows,
#[cfg(windows)]): ensure the GC agent binary present; if missing/outdated, download from the GC release channel and verify SHA-256 before executing (supply-chain guard). Launch passing RMMdevice_idas the GCagent_id, the per-machine key, relay URL, and (attended) the support code; unattended = persistent (no code). - Non-Windows: working stub +
// TODO(platform): linux/macos — GC agent not available(pergururmm/platform-parity). Mirror any newAppStatefield intoservice.rs.
Task 10 (RMM dashboard): Remote Control button + embedded viewer
Files touched: dashboard/src/pages/AgentDetail.tsx (:1893-1931), dashboard/src/api/client.ts
(:293-310 pattern), dashboard/src/components/RemoteControlPanel.tsx (new).
remoteControlApi.start(agentId, mode)→POST /api/agents/:agent_id/remote-control.- "Remote Control" button on
AgentDetail(enabled only when online + GC capabilities allow); on success, renderRemoteControlPanelembeddingviewer_embed_urlin a scoped iframe and wiring thepostMessagelifecycle events (Task 4). Nativeviewer_native_urloffered as a fallback link. ASCII markers in toasts/logs; no emojis.
Task 11 (both): Contract tests in each pipeline
Files touched: GC server/tests/integration_contract.rs (new), RMM server/tests/connect_contract.rs (new).
- GC pipeline: a test asserting the
/api/integration/v1surface +capabilitiesshape matches the documentedCONTRACT.mdversion (catches accidental breaking changes before release). - RMM pipeline: a test (against a recorded/mock capabilities response) asserting the client correctly version-gates and parses the contract. This is what keeps the independently-built products in-sync — each pipeline independently fails if it drifts from the contract.
Task 12: Verification
End-to-end (Windows endpoint, both agents installed):
- Capability discovery: RMM logs the GC contract version + feature map on startup; disabling a GC feature flag hides the corresponding RMM affordance.
- Embedded unattended: Remote Control (unattended) on an online managed Windows endpoint → GC
viewer renders inside the RMM dashboard (iframe), screen + mouse/keyboard work, multi-monitor
switch works, no endpoint prompt.
postMessageviewer:connectedfires. - Attended: end user sees the consent dialog (technician name); Accept → session; Decline → refused + logged.
- Embedding security: the GC viewer loads framed only from the RMM origin; any other origin is
refused (
frame-ancestors); all non-viewer GC routes still returnframe-ancestors 'none'. - Supply-chain guard: corrupt the staged GC binary → agent refuses to launch (checksum mismatch in logs).
- Standalone unaffected: GC still builds, runs, and serves a normal (non-embedded) support session with zero RMM present.
- Robustness: restart the GC relay mid-session → managed session reconciled from DB, not orphaned.
- Audit:
remote_control_sessions(RMM) +connect_session_events(GC) show session, technician, mode, consent. - Contract tests: both pipelines' contract tests pass; intentionally bumping the GC contract shape
without updating
CONTRACT.md/RMM fails the relevant pipeline test.