feat: push OpenClaw metadata to Monitor bridge periodically
- MonitorBridgeClient gains pushOpenClawMeta() method for POST /openclaw - OpenClawMeta interface defines version/plugin_version/agents payload - Plugin pushes metadata on gateway_start (delayed 2s) and periodically - Interval aligns with reportIntervalSec (default 30s) - Pushes are non-fatal — plugin continues if Monitor is unreachable - Interval cleanup on gateway_stop - Updated monitor-server-connector-plan.md with new architecture
This commit is contained in:
@@ -2,46 +2,53 @@
|
|||||||
|
|
||||||
## Current design
|
## 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`
|
### Data flow
|
||||||
- **API Key authentication** via `X-API-Key`
|
|
||||||
- **Gateway lifecycle hooks**: `gateway_start` / `gateway_stop`
|
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
|
## No longer used
|
||||||
|
|
||||||
The following design has been retired:
|
The following design has been retired:
|
||||||
|
|
||||||
- challenge UUID
|
- challenge UUID / RSA handshake / WebSocket telemetry
|
||||||
- RSA public key fetch
|
- Plugin-side `server/` sidecar process
|
||||||
- encrypted handshake payload
|
|
||||||
- WebSocket telemetry
|
|
||||||
|
|
||||||
## Runtime flow
|
## Heartbeat payload
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"identifier": "vps.t1",
|
"identifier": "vps.t1",
|
||||||
"openclaw_version": "OpenClaw 2026.3.13 (61d171a)",
|
"openclaw_version": "OpenClaw 2026.3.13 (61d171a)",
|
||||||
"plugin_version": "0.1.0",
|
"plugin_version": "0.2.0",
|
||||||
"agents": [],
|
"agents": [],
|
||||||
"cpu_pct": 10.5,
|
"cpu_pct": 10.5,
|
||||||
"mem_pct": 52.1,
|
"mem_pct": 52.1,
|
||||||
"disk_pct": 81.0,
|
"disk_pct": 81.0,
|
||||||
"swap_pct": 0.0,
|
"swap_pct": 0.0,
|
||||||
"load_avg": [0.12, 0.09, 0.03],
|
"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.
|
||||||
|
|||||||
14
plugin/core/monitor-bridge.d.ts
vendored
14
plugin/core/monitor-bridge.d.ts
vendored
@@ -36,6 +36,20 @@ export declare class MonitorBridgeClient {
|
|||||||
constructor(port: number, timeoutMs?: number);
|
constructor(port: number, timeoutMs?: number);
|
||||||
health(): Promise<MonitorHealth | null>;
|
health(): Promise<MonitorHealth | null>;
|
||||||
telemetry(): Promise<MonitorTelemetryResponse | null>;
|
telemetry(): Promise<MonitorTelemetryResponse | null>;
|
||||||
|
/**
|
||||||
|
* 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<boolean>;
|
||||||
private fetchJson;
|
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
|
//# sourceMappingURL=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"}
|
{"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"}
|
||||||
@@ -23,6 +23,28 @@ class MonitorBridgeClient {
|
|||||||
async telemetry() {
|
async telemetry() {
|
||||||
return this.fetchJson('/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) {
|
async fetchJson(path) {
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|||||||
@@ -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"}
|
{"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"}
|
||||||
@@ -50,6 +50,30 @@ export class MonitorBridgeClient {
|
|||||||
return this.fetchJson<MonitorTelemetryResponse>('/telemetry');
|
return this.fetchJson<MonitorTelemetryResponse>('/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<boolean> {
|
||||||
|
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<T>(path: string): Promise<T | null> {
|
private async fetchJson<T>(path: string): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
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[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os';
|
import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os';
|
||||||
import { getLivePluginConfig, type HarborForgeMonitorConfig } from './core/live-config';
|
import { getLivePluginConfig, type HarborForgeMonitorConfig } from './core/live-config';
|
||||||
import { MonitorBridgeClient } from './core/monitor-bridge';
|
import { MonitorBridgeClient, type OpenClawMeta } from './core/monitor-bridge';
|
||||||
|
|
||||||
interface PluginAPI {
|
interface PluginAPI {
|
||||||
logger: {
|
logger: {
|
||||||
@@ -93,12 +93,56 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Periodic metadata push interval handle
|
||||||
|
let metaPushInterval: ReturnType<typeof setInterval> | 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', () => {
|
api.on('gateway_start', () => {
|
||||||
logger.info('HarborForge plugin active');
|
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', () => {
|
api.on('gateway_stop', () => {
|
||||||
logger.info('HarborForge plugin stopping');
|
logger.info('HarborForge plugin stopping');
|
||||||
|
if (metaPushInterval) {
|
||||||
|
clearInterval(metaPushInterval);
|
||||||
|
metaPushInterval = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tool: plugin status
|
// Tool: plugin status
|
||||||
|
|||||||
Reference in New Issue
Block a user