chore: sync repository to current working state
Some checks failed
Build and Test / Build Server (Linux) (push) Has been cancelled
Build and Test / Build Agent (Windows) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
Some checks failed
Build and Test / Build Server (Linux) (push) Has been cancelled
Build and Test / Build Agent (Windows) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
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>
This commit is contained in:
186
specs/native-remote-control/plan.md
Normal file
186
specs/native-remote-control/plan.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# 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.md` documenting 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/version` at
|
||||
`releases.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 RMM `enroll.rs` `generate_api_key`/`hash_api_key`).
|
||||
- `POST /api/integration/v1/agents/:agent_id/keys` mints a per-machine key (plaintext once, store hash).
|
||||
- `validate_agent_api_key()` accepts a valid DB per-machine key; shared `AGENT_API_KEY` env becomes a
|
||||
deprecated fallback. Support `revoked_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 by `agent_id`, marked `is_managed`/`source="gururmm"`; returns `{ session_id }`.
|
||||
When the GC agent later registers with that `agent_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 TEXT` to `connect_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/relaxed `X-Frame-Options` ONLY on the
|
||||
viewer path. Every other route keeps `frame-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 emit `postMessage` lifecycle events
|
||||
(`viewer:connected`, `viewer:disconnected`, `viewer:error`, `viewer:resize`) for the RMM host to
|
||||
react to. Document this embed protocol in `CONTRACT.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) and
|
||||
`ConsentResponse { session_id, accepted }` (agent→server).
|
||||
- GC agent: on `ConsentRequest` in attended mode, show a native consent dialog; Decline → session
|
||||
refused + event logged. Unattended skips consent (gated by session `mode`).
|
||||
- Emit `connect_session_events` for consent shown/accepted/declined. Expose `consent_prompt` in 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_sessions` from DB into `SessionManager` so 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/v1` contract: 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 if
|
||||
`consent_prompt` is 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 via `authorize_agent_access`.
|
||||
Steps: resolve `device_id`+online → (via `connect_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_sessions` audit row (`agent_id`, `tech_id`, `connect_session_id`, `mode`,
|
||||
`started_at`), mirroring the `tunnel_audit` pattern.
|
||||
|
||||
## 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 RMM `device_id` as the GC `agent_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`
|
||||
(per `gururmm/platform-parity`). Mirror any new `AppState` field into `service.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, render `RemoteControlPanel` embedding `viewer_embed_url` in a scoped iframe and wiring the
|
||||
`postMessage` lifecycle events (Task 4). Native `viewer_native_url` offered 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/v1` surface + `capabilities` shape matches the
|
||||
documented `CONTRACT.md` version (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. `postMessage` `viewer:connected` fires.
|
||||
- **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 return `frame-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.
|
||||
115
specs/native-remote-control/references.md
Normal file
115
specs/native-remote-control/references.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Native Remote Control — Code References
|
||||
|
||||
> Two repos. **GC** = guru-connect (`D:\claudetools\projects\msp-tools\guru-connect`, lives
|
||||
> in the claudetools repo). **RMM** = GuruRMM (`projects/msp-tools/guru-rmm`, a git submodule
|
||||
> tracking `azcomputerguru/gururmm`). Paths below are relative to each repo root.
|
||||
|
||||
## Files that will be touched
|
||||
|
||||
### guru-connect (GC)
|
||||
|
||||
- `server/src/main.rs` — route table; `create_code` `:382`, `list_sessions` `:425`, `get_session`
|
||||
`:433`, `list_machines` `:467`, `/health` `:254`, public `/api/version` `:300`. **Add** the
|
||||
`/api/integration/v1/` namespace: `GET .../capabilities`, `POST .../sessions`,
|
||||
`POST .../sessions/:id/viewer-token`, `POST .../agents/:agent_id/keys`; register the
|
||||
server-to-server integration auth layer. Model the (unauthenticated) capabilities endpoint on the
|
||||
existing `/api/version` route.
|
||||
- `CONTRACT.md` (new — GC repo root or `docs/`) — the semver'd integration contract doc both teams
|
||||
keep front of mind. Source of truth for the surface; tested in CI (Task 11).
|
||||
- `server/src/api/releases.rs:76` — `GET /api/version` handler (no auth, for agent polling). Pattern
|
||||
to model `GET /api/integration/v1/capabilities` on.
|
||||
- `server/static/viewer.html` — the existing **web viewer**; gets an `?embed=1` mode (hide standalone
|
||||
chrome, accept host-provided session/token, emit `postMessage` lifecycle events for the RMM host).
|
||||
- `server/src/middleware/security_headers.rs:30` (`frame-ancestors 'none'`) and `:37-39`
|
||||
(`X-Frame-Options`) — **the embedding blocker.** Add a per-route scoped allowlist for the viewer
|
||||
path only (RMM origin from env); leave every other route at `'none'`.
|
||||
- `server/src/session/mod.rs` — in-memory `SessionManager`; `register_agent()` `:95`,
|
||||
`join_session()` `:254`. **Change** to allow a session to be pre-created/keyed by `agent_id`
|
||||
before the agent connects, then bound when the agent registers.
|
||||
- `server/src/db/sessions.rs` — `create_session()` `:22`. **Change/add** to persist pre-created
|
||||
sessions and a `is_managed`/`source` marker; reconcile in-memory state on startup.
|
||||
- `server/src/db/support_codes.rs` — `create_support_code()` `:24`, `get_support_code()` `:43`.
|
||||
Reused as-is for the attended path (broker calls `POST /api/codes`).
|
||||
- `server/src/relay/mod.rs` — agent WS handler `:55`/`:236`; `validate_agent_api_key()` `:187`
|
||||
(currently JWT-or-shared-`AGENT_API_KEY`, comment at `:200` flags DB keys as future).
|
||||
**Change** to validate against the new per-machine key table.
|
||||
- `server/src/auth/jwt.rs` — JWT signing/validation. **Add** a short-lived, session-scoped
|
||||
viewer token mint.
|
||||
- `server/migrations/` — **add** `connect_agent_keys` (per-machine keys) and session columns;
|
||||
follow the existing `001_initial_schema.sql` / `003_auto_update.sql` style. Idempotent
|
||||
(`IF NOT EXISTS`).
|
||||
- `proto/guruconnect.proto` — `SessionRequest` `:8`, `StartStream` `:261`, `AgentStatus` `:271`,
|
||||
`AdminCommand` `:286`. **Add** `ConsentRequest` / `ConsentResponse` messages.
|
||||
- `agent/src/session/mod.rs` — `SessionState` `:71`, persistent-vs-support logic. **Change** to
|
||||
register against a broker-assigned `agent_id` (= GuruRMM `device_id`).
|
||||
- `agent/src/transport/websocket.rs` — `connect()` `:32` (builds `?agent_id=&api_key=&support_code=`).
|
||||
Pass the per-machine key.
|
||||
- `agent/src/tray/mod.rs` + a new consent dialog — **add** the attended-mode consent prompt
|
||||
(handle `ConsentRequest`).
|
||||
- `agent/src/install.rs` — `register_protocol_handler()` `:131` (`guruconnect://<session>?token=&server=`).
|
||||
Reused for native-viewer launch URLs the broker returns.
|
||||
|
||||
### GuruRMM (RMM)
|
||||
|
||||
- `server/src/api/commands.rs:87-157` — `POST /api/agents/{agent_id}/command` dispatch
|
||||
(online → WS `ServerMessage::Command`; offline → queued). **Reuse** to push the
|
||||
"ensure + launch guru-connect" instruction to the endpoint agent.
|
||||
- `server/src/api/mod.rs:162` — route registration site. **Add** the new broker route.
|
||||
- `server/src/api/` — **add** `remote_control.rs`: `POST /api/agents/:agent_id/remote-control`
|
||||
(body selects `unattended|attended`); talks to the GC server API, returns a viewer launch URL.
|
||||
- `server/src/db/` + `server/migrations/` — **add** a `remote_control_sessions` record (or reuse
|
||||
`tech_sessions` from `010_tunnel_sessions.sql`) for audit (`agent_id`, `tech_id`, `connect_session_id`,
|
||||
`mode`, timestamps).
|
||||
- `agent/src/transport/websocket.rs` — `run_command()` `:1050`, `execute_command()` `:971`.
|
||||
**Add** a `RemoteControl`/launch path (or a dedicated command_type) that, on Windows, ensures
|
||||
the guru-connect agent binary is present (download + SHA-256 verify) and launches it in the
|
||||
requested mode passing `device_id` as the GC `agent_id`.
|
||||
- `agent/src/device_id.rs:1-99` — source of the stable cross-product identity. Read-only.
|
||||
- `dashboard/src/pages/AgentDetail.tsx:1893-1931` — tab/header + action-button area.
|
||||
**Add** the "Remote Control" button (open viewer URL on success).
|
||||
- `dashboard/src/components/CommandTerminal.tsx:60-106` — the canonical
|
||||
button→`api.post()`→`useQuery` action pattern to copy.
|
||||
- `dashboard/src/api/client.ts:293-310` — `commandsApi` pattern. **Add** `remoteControlApi.start(agentId, mode)`.
|
||||
|
||||
## Similar existing implementations (patterns to follow)
|
||||
|
||||
- **Per-agent action dispatch (RMM):** `server/src/api/commands.rs:87-157` + agent reception
|
||||
`agent/src/transport/websocket.rs:570-573` → `execute_command()` `:971` → `run_command()` `:1050`.
|
||||
The broker's "launch guru-connect" instruction follows this exact send-command path.
|
||||
- **Dashboard action button → poll (RMM):** `dashboard/src/components/CommandTerminal.tsx:82-105`
|
||||
(`useMutation` → `commandsApi.send` → `useQuery` poll). The Remote Control button mirrors this.
|
||||
- **Per-agent credential issuance (RMM):** `server/src/api/enroll.rs:38-139` — `generate_api_key("agk_")`
|
||||
`:103`, `hash_api_key()` `:104`, plaintext returned once `:138`. Model `connect_agent_keys`
|
||||
provisioning on this.
|
||||
- **Support-code minting (GC):** `server/src/main.rs:382` `create_code` + `server/src/db/support_codes.rs:24`.
|
||||
The attended path reuses this directly.
|
||||
- **Agent WS auth handshake (RMM):** `agent/src/transport/websocket.rs:100-197` — how api_key/device_id
|
||||
are presented; the per-machine GC key provisioning should align with this lifecycle.
|
||||
- **Half-built generic tunnel (RMM), for reference only:** server `server/src/api/tunnel.rs:1-232`
|
||||
(routes NOT registered), `server/src/db/tunnel.rs:1-152`, `server/migrations/010_tunnel_sessions.sql`,
|
||||
agent `agent/src/tunnel/mod.rs:62-197`, WS msgs `server/src/ws/mod.rs:287-300`. The
|
||||
`tech_sessions`/`tunnel_audit` schema is a usable model for the remote-control audit record.
|
||||
|
||||
## Database schema
|
||||
|
||||
### guru-connect (existing — `server/migrations/`)
|
||||
- `connect_machines` (`001_initial_schema.sql:8`) — `agent_id` UNIQUE, `hostname`, `is_persistent`,
|
||||
`status`, plus `agent_version`/`organization`/`site`/`tags` from `003_auto_update.sql`.
|
||||
- `connect_sessions` (`001_initial_schema.sql:27`) — `id`, `machine_id`, `is_support_session`,
|
||||
`support_code`, `status`. **Add** `is_managed` / `source` marker for broker-initiated sessions.
|
||||
- `connect_support_codes` (`001_initial_schema.sql:59`) — reused unchanged for attended.
|
||||
- `connect_session_events` (`001_initial_schema.sql:43`) — audit; emit broker/consent events here.
|
||||
- `releases` (`003_auto_update.sql:9`) — has `checksum_sha256`; reuse for the verify-before-launch
|
||||
supply-chain guard.
|
||||
- **New:** `connect_agent_keys` — `id`, `agent_id` FK, `key_hash`, `created_at`, `revoked_at`.
|
||||
Idempotent migration, hashed keys only (mirror RMM enroll pattern).
|
||||
|
||||
### GuruRMM (existing — `server/migrations/`)
|
||||
- Agent identity: `agent_id` (UUID, assigned at WS auth), `device_id` (`agent/src/device_id.rs`),
|
||||
`site_id`, per-agent `agk_` key (hashed) from `server/src/api/enroll.rs`.
|
||||
- `tech_sessions` / `tunnel_audit` (`010_tunnel_sessions.sql`) — model for the new
|
||||
`remote_control_sessions` audit table (or extend `tech_sessions` with a `mode`).
|
||||
|
||||
> Migration discipline for both Rust servers: idempotent `IF NOT EXISTS`, let the server binary
|
||||
> apply migrations on startup, `cargo sqlx prepare` if any `query!()` macro changes. See
|
||||
> `gururmm/sqlx-migrations` standard.
|
||||
88
specs/native-remote-control/shape.md
Normal file
88
specs/native-remote-control/shape.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Native Remote Control — GC↔RMM Integration Contract & Embedded Viewer — Shape & Constraints
|
||||
|
||||
## What this is
|
||||
|
||||
guru-connect (GC) is a **standalone product** — a ScreenConnect/Splashtop-style remote-support
|
||||
tool that must work fully on its own, with its **own release pipeline, cadence, and development
|
||||
cycle**, independent of GuruRMM (RMM).
|
||||
|
||||
This feature establishes and maintains the **integration contract** that lets RMM embed GC as an
|
||||
**integrated session viewer** — a technician launches a live remote-control session on a managed
|
||||
endpoint from inside the RMM dashboard, and the GC session viewer renders **inside RMM's UI** —
|
||||
while GC and RMM remain separately developed products. The deliverable is therefore not a one-off
|
||||
broker wiring; it is a **durable, versioned boundary** (owned by GC) plus the broker that consumes
|
||||
it. "Keep integration front of mind" = GC treats this contract as a first-class, supported surface
|
||||
that it does not break as it evolves on its own cadence.
|
||||
|
||||
## What this is NOT (out of scope)
|
||||
|
||||
- **File transfer** — no drag/drop or browse-and-copy during a session (deferred).
|
||||
- **Session recording** — no session-to-video capture for audit/compliance (deferred).
|
||||
- **Non-Windows agents** — macOS/Linux remote-control endpoints are out of scope; the GC agent is
|
||||
Windows-only today. Windows-first. (Multi-monitor IS in scope.)
|
||||
- **Not coupling the two products.** This must NOT merge GC into the RMM agent, share build
|
||||
pipelines, or make either product unbuildable/unreleasable without the other. GC must still ship
|
||||
and run standalone with zero RMM dependency.
|
||||
- Not a replacement for RMM's generic admin `tunnel` scaffold (terminal/file/registry channels) —
|
||||
that is a separate text-channel feature; this is video remote control.
|
||||
|
||||
## In scope
|
||||
|
||||
- **A versioned GC integration contract** (`/api/integration/v1/...`) owned and documented by GC,
|
||||
with a capability/version discovery endpoint so RMM can detect what a given GC build supports and
|
||||
degrade gracefully. This is the keystone of the feature.
|
||||
- **Embedded session viewer** — RMM hosts GC's web viewer inside its dashboard (scoped iframe /
|
||||
panel), not only the native `guruconnect://` launch.
|
||||
- Unattended remote control of managed endpoints (primary RMM use case).
|
||||
- Attended remote control with an end-user consent prompt.
|
||||
- Multi-monitor (display switching) — GC already reports `display_count`.
|
||||
- Short-lived, per-session viewer credentials (no long-lived viewer tokens).
|
||||
|
||||
## Hard constraints
|
||||
|
||||
- **GC stays standalone.** Independent pipeline/cadence preserved. The integration contract is
|
||||
additive to GC and must not introduce any RMM build/runtime dependency into GC.
|
||||
- **Stability via versioning, not lockstep.** Because the two products release on different cadences,
|
||||
the contract is **semver'd** and exposes `GET /api/integration/capabilities`. RMM version-gates
|
||||
features off that response; GC never breaks a published contract version without a major bump.
|
||||
- **No external apps / no supply-chain exposure.** Remote control runs entirely on our Rust stack.
|
||||
The RMM agent obtains the GC agent binary only from GC's own release channel and **verifies a
|
||||
SHA-256 checksum before launch** (reuse GC's `releases.checksum_sha256`). No third-party downloads.
|
||||
- **Embedding must not weaken security.** The viewer is framable only by an explicit RMM-origin
|
||||
allowlist via scoped `frame-ancestors` / `X-Frame-Options` on the viewer route(s); the global
|
||||
`frame-ancestors 'none'` (`security_headers.rs:30`) stays for every other route.
|
||||
- **No hardcoded secrets.** Integration key, per-machine agent keys, viewer tokens come from
|
||||
env/SOPS, never source. No endpoint URLs in TOML/config files — env vars only.
|
||||
- **Single static binary, no runtime deps**; Windows 7 SP1+ target preserved for the GC agent.
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **GC owns the integration contract.** It lives in the GC repo (this spec + a versioned
|
||||
`CONTRACT.md` / OpenAPI doc), is exposed under `/api/integration/v1/`, and is GC's responsibility
|
||||
to keep stable. RMM is purely a consumer.
|
||||
- **Decouple cadences with capability discovery.** `GET /api/integration/capabilities` returns the
|
||||
contract version + a feature map (e.g. `embedded_viewer`, `consent_prompt`, `per_machine_keys`).
|
||||
RMM reads it at integration time and only offers what the connected GC build supports. This is how
|
||||
"in-sync" is achieved without lockstep releases.
|
||||
- **Broker model (RMM orchestrates the separate GC agent).** Reuses GC's existing engine as-is;
|
||||
aligns naturally with two independent products. Endpoints both agents stay separate binaries.
|
||||
- **Stable cross-product identity = RMM `device_id`.** The RMM agent launches the GC agent passing
|
||||
RMM's `device_id` as the GC `agent_id`, so the broker's pre-created session deterministically
|
||||
matches the endpoint (`agent/src/device_id.rs` survives reinstalls).
|
||||
- **Embedded viewer over native-only.** GC exposes an embed-mode `viewer.html` (scoped framing +
|
||||
`postMessage` lifecycle events for the RMM host); the native `guruconnect://` handler remains a
|
||||
fallback. This is what makes GC a true "integrated session viewer."
|
||||
- **Per-machine agent keys replace the shared `AGENT_API_KEY`** (`relay/mod.rs:187` flags this as
|
||||
future work); programmatic **session pre-create + short-lived viewer token** are added because GC
|
||||
has neither today; **consent** for attended mode is new (`ConsentRequest`/`ConsentResponse`).
|
||||
|
||||
## Priority
|
||||
|
||||
P2 — important, near-term. The contract/capability layer (Tasks 1) is the part to get right first,
|
||||
because it is the long-lived surface both products depend on.
|
||||
|
||||
## Roadmap reference
|
||||
|
||||
`projects/msp-tools/guru-rmm/docs/FEATURE_ROADMAP.md:635-675` — "Remote Access" (supersedes the
|
||||
"Remote desktop (RDP/VNC proxy) - P3" line with our own stack). `docs/UI_GAPS.md:155-186`.
|
||||
GC side: this spec + the new `CONTRACT.md` become GC's integration-surface roadmap entry.
|
||||
88
specs/native-remote-control/standards.md
Normal file
88
specs/native-remote-control/standards.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Native Remote Control — Applicable Standards
|
||||
|
||||
The following standards from `.claude/standards/` apply to this feature.
|
||||
|
||||
## security/credential-handling
|
||||
|
||||
No hardcoded credentials. The GuruRMM→guru-connect integration key (`CONNECT_INTEGRATION_KEY`),
|
||||
per-machine agent keys, and viewer tokens come from env/SOPS — never source. Per-machine agent
|
||||
keys and viewer tokens are **hashed/short-lived**; JWT for auth, Argon2id for any password
|
||||
storage. Log all auth attempts and session brokering (timestamp, identity, agent_id).
|
||||
|
||||
Source: `.claude/standards/security/credential-handling.md`
|
||||
|
||||
## api/response-format
|
||||
|
||||
New endpoints (`POST /api/agents/:agent_id/remote-control`, GC `POST /api/sessions`,
|
||||
`POST /api/sessions/:id/viewer-token`, `POST /api/agents/:agent_id/keys`) use RESTful plural
|
||||
nouns, kebab-case multi-word segments (`/remote-control`), and the standard error envelope
|
||||
`{ detail, error_code, status_code }`. Prefer `sqlx::query()` (runtime) over the `query!()`
|
||||
macro for new queries.
|
||||
|
||||
Source: `.claude/standards/api/response-format.md`
|
||||
|
||||
## gururmm/sqlx-migrations
|
||||
|
||||
New migrations (`connect_agent_keys`, session `is_managed`/`source` columns,
|
||||
`remote_control_sessions`) must be idempotent (`CREATE TABLE IF NOT EXISTS`,
|
||||
`ADD COLUMN IF NOT EXISTS`). Let the server binary apply migrations on startup; never pre-apply
|
||||
via psql without the `_sqlx_migrations` row. Run `cargo sqlx prepare` and commit `.sqlx/` if any
|
||||
`query!()` macro changes.
|
||||
|
||||
Source: `.claude/standards/gururmm/sqlx-migrations.md`
|
||||
|
||||
## gururmm/platform-parity
|
||||
|
||||
The endpoint launch logic (Task 7) is Windows-only because the guru-connect agent is Windows-only.
|
||||
This is allowed, but the non-Windows path must be a working stub with
|
||||
`// TODO(platform): linux/macos — guru-connect agent not available`, not a silent no-op. Any new
|
||||
`AppState` field added in `main.rs` must also be mirrored in `service.rs` (Windows-service entry).
|
||||
|
||||
Source: `.claude/standards/gururmm/platform-parity.md`
|
||||
|
||||
## gururmm/build-pipeline
|
||||
|
||||
Never run `build-agents.sh` / build scripts manually over SSH. All agent and server builds go
|
||||
through the Gitea webhook pipeline (push to `main`). Deploy = stop → copy binary → start.
|
||||
|
||||
Source: `.claude/standards/gururmm/build-pipeline.md`
|
||||
|
||||
## conventions/no-emojis & conventions/output-markers
|
||||
|
||||
No emojis anywhere in code, logs, dashboard strings, or commit messages. Use ASCII status markers
|
||||
`[OK] [ERROR] [WARNING] [SUCCESS] [INFO] [CRITICAL]` in any script or operator-facing output
|
||||
(installer scripts, agent launch logs, dashboard toasts).
|
||||
|
||||
Source: `.claude/standards/conventions/no-emojis.md`, `.claude/standards/conventions/output-markers.md`
|
||||
|
||||
## git/commit-style
|
||||
|
||||
Conventional commit types (`feat:`, `fix:`, `spec:`, `build:`), and `Co-Authored-By` for
|
||||
Claude-assisted commits. Never commit `.env`, keys, or unencrypted secrets.
|
||||
|
||||
Source: `.claude/standards/git/commit-style.md`
|
||||
|
||||
## Integration contract versioning (feature-specific rule)
|
||||
|
||||
Because GC and RMM ship on independent pipelines/cadences, the integration surface is **semver'd**
|
||||
and namespaced (`/api/integration/v1/`). GC must not change a published contract version in a
|
||||
breaking way without a major bump, and must keep `CONTRACT.md` in lockstep with the code (enforced
|
||||
by the Task 11 contract test in each pipeline). RMM discovers support via
|
||||
`GET /api/integration/capabilities` and version-gates — never assumes a feature exists. This is the
|
||||
mechanism that keeps the two products "in-sync" without coupling their releases.
|
||||
|
||||
## Embedding / clickjacking (security, feature-specific)
|
||||
|
||||
The embedded viewer relaxes `frame-ancestors`/`X-Frame-Options` **only on the viewer route**, to an
|
||||
explicit RMM-origin allowlist sourced from env. The global `frame-ancestors 'none'`
|
||||
(`server/src/middleware/security_headers.rs:30`) and `X-Frame-Options` (`:37-39`) stay in force for
|
||||
every other route. Never disable framing protection globally to enable the embed.
|
||||
|
||||
## guru-connect project conventions (`projects/msp-tools/guru-connect/CLAUDE.md`)
|
||||
|
||||
Not in `.claude/standards/` but binding for the GC repo: Rust uses `tracing` (not `println!`),
|
||||
`anyhow` in binaries, `thiserror` for library errors, `async`/`await`, `cargo clippy` before
|
||||
commits; protobuf is the source of truth (`proto/guruconnect.proto`); transport is protobuf over
|
||||
`wss://`; Argon2id for passwords; agent stays a single static binary with no runtime deps.
|
||||
|
||||
Source: `projects/msp-tools/guru-connect/CLAUDE.md`
|
||||
Reference in New Issue
Block a user