diff --git a/docs/monitor-server-connector-plan.md b/docs/monitor-server-connector-plan.md index 291a9ee..2a2ae0b 100644 --- a/docs/monitor-server-connector-plan.md +++ b/docs/monitor-server-connector-plan.md @@ -2,46 +2,53 @@ ## Current design -The plugin uses: +The plugin and Monitor communicate over a local bridge port (`monitor_port` / `MONITOR_PORT`). -- **HTTP heartbeat** to `/monitor/server/heartbeat-v2` -- **API Key authentication** via `X-API-Key` -- **Gateway lifecycle hooks**: `gateway_start` / `gateway_stop` +### Data flow + +1. **Monitor → Plugin** (GET): Plugin queries `GET /telemetry` on the bridge for host hardware data. +2. **Plugin → Monitor** (POST): Plugin pushes OpenClaw metadata via `POST /openclaw` to the bridge. +3. **Monitor → Backend**: Monitor heartbeats to `POST /monitor/server/heartbeat-v2` with `X-API-Key`, enriched with any available OpenClaw metadata. + +### Bridge endpoints (on Monitor, 127.0.0.1:MONITOR_PORT) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check, returns monitor version and identifier | +| `/telemetry` | GET | Latest hardware telemetry snapshot | +| `/openclaw` | POST | Receive OpenClaw metadata from plugin | + +### Plugin behavior + +- On `gateway_start`, plugin begins periodic metadata push (aligned with `reportIntervalSec`). +- Initial push is delayed 2s to allow Monitor bridge startup. +- If bridge is unreachable, pushes fail silently. Plugin remains fully functional. +- On `gateway_stop`, periodic push is stopped. ## No longer used The following design has been retired: -- challenge UUID -- RSA public key fetch -- encrypted handshake payload -- WebSocket telemetry +- challenge UUID / RSA handshake / WebSocket telemetry +- Plugin-side `server/` sidecar process -## Runtime flow - -1. Gateway loads `harborforge-monitor` -2. Plugin reads config from OpenClaw plugin config -3. On `gateway_start`, plugin launches `server/telemetry.mjs` -4. Sidecar collects: - - system metrics - - OpenClaw version - - plugin version - - configured agents -5. Sidecar posts telemetry to backend with `X-API-Key` - -## Payload +## Heartbeat payload ```json { "identifier": "vps.t1", "openclaw_version": "OpenClaw 2026.3.13 (61d171a)", - "plugin_version": "0.1.0", + "plugin_version": "0.2.0", "agents": [], "cpu_pct": 10.5, "mem_pct": 52.1, "disk_pct": 81.0, "swap_pct": 0.0, "load_avg": [0.12, 0.09, 0.03], - "uptime_seconds": 12345 + "uptime_seconds": 12345, + "nginx_installed": true, + "nginx_sites": ["default"] } ``` + +`openclaw_version`, `plugin_version`, and `agents` are optional enrichment from the plugin. If plugin never pushes metadata, these fields are omitted and the heartbeat contains only hardware telemetry. diff --git a/plugin/core/monitor-bridge.d.ts b/plugin/core/monitor-bridge.d.ts index 84f626f..afa27f9 100644 --- a/plugin/core/monitor-bridge.d.ts +++ b/plugin/core/monitor-bridge.d.ts @@ -36,6 +36,20 @@ export declare class MonitorBridgeClient { constructor(port: number, timeoutMs?: number); health(): Promise; telemetry(): Promise; + /** + * POST OpenClaw metadata to the Monitor bridge so it can enrich + * its heartbeat uploads with OpenClaw version, plugin version, + * and agent information. + */ + pushOpenClawMeta(meta: OpenClawMeta): Promise; private fetchJson; } +/** + * OpenClaw metadata payload sent to the Monitor bridge. + */ +export interface OpenClawMeta { + version: string; + plugin_version: string; + agents?: any[]; +} //# sourceMappingURL=monitor-bridge.d.ts.map \ No newline at end of file diff --git a/plugin/core/monitor-bridge.d.ts.map b/plugin/core/monitor-bridge.d.ts.map index bffa006..d170573 100644 --- a/plugin/core/monitor-bridge.d.ts.map +++ b/plugin/core/monitor-bridge.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"monitor-bridge.d.ts","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC;QACzB,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO;IAKpC,MAAM,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAIvC,SAAS,IAAI,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;YAI7C,SAAS;CAgBxB"} \ No newline at end of file +{"version":3,"file":"monitor-bridge.d.ts","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC;QACzB,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO;IAKpC,MAAM,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAIvC,SAAS,IAAI,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;IAI3D;;;;OAIG;IACG,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;YAmB9C,SAAS;CAgBxB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;CAChB"} \ No newline at end of file diff --git a/plugin/core/monitor-bridge.js b/plugin/core/monitor-bridge.js index 5657649..144b4e4 100644 --- a/plugin/core/monitor-bridge.js +++ b/plugin/core/monitor-bridge.js @@ -23,6 +23,28 @@ class MonitorBridgeClient { async telemetry() { return this.fetchJson('/telemetry'); } + /** + * POST OpenClaw metadata to the Monitor bridge so it can enrich + * its heartbeat uploads with OpenClaw version, plugin version, + * and agent information. + */ + async pushOpenClawMeta(meta) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + const response = await fetch(`${this.baseUrl}/openclaw`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(meta), + signal: controller.signal, + }); + clearTimeout(timeout); + return response.ok; + } + catch { + return false; + } + } async fetchJson(path) { try { const controller = new AbortController(); diff --git a/plugin/core/monitor-bridge.js.map b/plugin/core/monitor-bridge.js.map index cdeccf0..174276a 100644 --- a/plugin/core/monitor-bridge.js.map +++ b/plugin/core/monitor-bridge.js.map @@ -1 +1 @@ -{"version":3,"file":"monitor-bridge.js","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA2BH,MAAa,mBAAmB;IACtB,OAAO,CAAS;IAChB,SAAS,CAAS;IAE1B,YAAY,IAAY,EAAE,SAAS,GAAG,IAAI;QACxC,IAAI,CAAC,OAAO,GAAG,oBAAoB,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,IAAI,CAAC,SAAS,CAAgB,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,SAAS,CAA2B,YAAY,CAAC,CAAC;IAChE,CAAC;IAEO,KAAK,CAAC,SAAS,CAAI,IAAY;QACrC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBACrD,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YAEtB,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC;YAC9B,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAjCD,kDAiCC"} \ No newline at end of file +{"version":3,"file":"monitor-bridge.js","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA2BH,MAAa,mBAAmB;IACtB,OAAO,CAAS;IAChB,SAAS,CAAS;IAE1B,YAAY,IAAY,EAAE,SAAS,GAAG,IAAI;QACxC,IAAI,CAAC,OAAO,GAAG,oBAAoB,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,IAAI,CAAC,SAAS,CAAgB,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,SAAS,CAA2B,YAAY,CAAC,CAAC;IAChE,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,gBAAgB,CAAC,IAAkB;QACvC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,WAAW,EAAE;gBACvD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC1B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YAEtB,OAAO,QAAQ,CAAC,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,SAAS,CAAI,IAAY;QACrC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBACrD,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YAEtB,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC;YAC9B,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAzDD,kDAyDC"} \ No newline at end of file diff --git a/plugin/core/monitor-bridge.ts b/plugin/core/monitor-bridge.ts index 1da54e0..e755c35 100644 --- a/plugin/core/monitor-bridge.ts +++ b/plugin/core/monitor-bridge.ts @@ -50,6 +50,30 @@ export class MonitorBridgeClient { return this.fetchJson('/telemetry'); } + /** + * POST OpenClaw metadata to the Monitor bridge so it can enrich + * its heartbeat uploads with OpenClaw version, plugin version, + * and agent information. + */ + async pushOpenClawMeta(meta: OpenClawMeta): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + const response = await fetch(`${this.baseUrl}/openclaw`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(meta), + signal: controller.signal, + }); + clearTimeout(timeout); + + return response.ok; + } catch { + return false; + } + } + private async fetchJson(path: string): Promise { try { const controller = new AbortController(); @@ -67,3 +91,12 @@ export class MonitorBridgeClient { } } } + +/** + * OpenClaw metadata payload sent to the Monitor bridge. + */ +export interface OpenClawMeta { + version: string; + plugin_version: string; + agents?: any[]; +} diff --git a/plugin/index.ts b/plugin/index.ts index b71fbdd..abef652 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -10,7 +10,7 @@ */ import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os'; import { getLivePluginConfig, type HarborForgeMonitorConfig } from './core/live-config'; -import { MonitorBridgeClient } from './core/monitor-bridge'; +import { MonitorBridgeClient, type OpenClawMeta } from './core/monitor-bridge'; interface PluginAPI { logger: { @@ -93,12 +93,56 @@ export default { }; } + // Periodic metadata push interval handle + let metaPushInterval: ReturnType | null = null; + + /** + * Push OpenClaw metadata to the Monitor bridge. + * This enriches Monitor heartbeats with OpenClaw version/plugin/agent info. + * Failures are non-fatal — Monitor continues to work without this data. + */ + async function pushMetaToMonitor() { + const bridgeClient = getBridgeClient(); + if (!bridgeClient) return; + + const meta: OpenClawMeta = { + version: api.version || 'unknown', + plugin_version: '0.2.0', + agents: [], // TODO: populate from api agent list when available + }; + + const ok = await bridgeClient.pushOpenClawMeta(meta); + if (ok) { + logger.debug('pushed OpenClaw metadata to Monitor bridge'); + } else { + logger.debug('Monitor bridge unreachable for metadata push (non-fatal)'); + } + } + api.on('gateway_start', () => { logger.info('HarborForge plugin active'); + + // Push metadata to Monitor bridge on startup and periodically. + // Interval aligns with typical Monitor heartbeat cycle (30s). + // If Monitor bridge is unreachable, pushes silently fail. + const live = resolveConfig(); + const intervalSec = live.reportIntervalSec || 30; + + // Initial push (delayed 2s to let Monitor bridge start) + setTimeout(() => pushMetaToMonitor(), 2000); + + metaPushInterval = setInterval( + () => pushMetaToMonitor(), + intervalSec * 1000, + ); }); api.on('gateway_stop', () => { logger.info('HarborForge plugin stopping'); + if (metaPushInterval) { + clearInterval(metaPushInterval); + metaPushInterval = null; + } }); // Tool: plugin status