Merge dev-2026-03-21 into main #3
@@ -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.
|
||||
|
||||
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);
|
||||
health(): Promise<MonitorHealth | 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;
|
||||
}
|
||||
/**
|
||||
* OpenClaw metadata payload sent to the Monitor bridge.
|
||||
*/
|
||||
export interface OpenClawMeta {
|
||||
version: string;
|
||||
plugin_version: string;
|
||||
agents?: any[];
|
||||
}
|
||||
//# 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() {
|
||||
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();
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
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[];
|
||||
}
|
||||
|
||||
@@ -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<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', () => {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user