Files
John Doe 55aa319197 Critical security remediation: EventMesh, Gateway, BFT consensus fixes
- A1: Fixed EventMesh null reference crash at startup
  - Proper client initialization sequence
  - Added try/catch with cleanup on failure

- A2: Fixed Gateway authentication bypass vulnerability
  - Token validation now required for WebSocket connections
  - Auth enabled by default in production

- A3: Fixed JSON.parse unhandled exception
  - Malformed JSON no longer crashes gateway
  - Proper error logging and response

- A4: Fixed BFT consensus blocking loops
  - Replaced busy-wait with event-driven Promise pattern
  - Made BFTConsensus extend EventEmitter

- Added swarm memories migration (003_add_swarm_memories.sql)
- Added REMEDIATION_LOG.md documenting all changes

See audit/SUBREPO_REVIEW_2026-04-04.md for full details
2026-04-04 18:50:31 -04:00

940 lines
31 KiB
JavaScript

/**
* Heretek OpenClaw — Agent Client Library
* ==============================================================================
* Provides A2A communication and skill execution for OpenClaw agents.
*
* Features:
* - OpenClaw Gateway WebSocket RPC communication
* - Auto-discovery of agent capabilities
* - JSONL session storage
*
* Usage:
* const AgentClient = require('./lib/agent-client');
* const client = new AgentClient({
* agentId: 'steward',
* role: 'orchestrator',
* litellmHost: 'http://litellm:4000',
* apiKey: process.env.LITELLM_API_KEY,
* gatewayUrl: 'ws://127.0.0.1:18789'
* });
*
* // Send message to another agent via Gateway
* await client.sendMessage('alpha', { task: 'Analyze this data' });
*
* // Execute a skill
* const result = await client.executeSkill('curiosity-engine', context);
* ==============================================================================
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const WebSocket = require('ws');
// AUDIT-FIX: Add pg for swarm_memories table access
const { Pool } = require('pg');
/**
* GatewayClient - OpenClaw Gateway WebSocket RPC Client
* ==============================================================================
* Implements WebSocket RPC communication with OpenClaw Gateway.
* All A2A messages are routed through the Gateway on port 18789.
*/
class GatewayClient {
/**
* Create a new GatewayClient instance
* @param {Object} config - Configuration options
* @param {string} config.agentId - Agent identifier
* @param {string} config.gatewayUrl - Gateway WebSocket URL (ws://127.0.0.1:18789)
*/
constructor(config) {
this.agentId = config.agentId || 'unknown';
this.gatewayUrl = config.gatewayUrl || process.env.GATEWAY_URL || 'ws://127.0.0.1:18789';
this.ws = null;
this.connected = false;
this.messageHandlers = new Map();
this.pendingResponses = new Map();
this.messageCounter = 0;
// Heartbeat configuration
this.heartbeatInterval = config.heartbeatInterval || 30000; // 30 seconds
this.heartbeatTimer = null;
this.lastHeartbeatSent = null;
this.lastHeartbeatReceived = null;
}
/**
* Connect to the Gateway
* @param {Object} options - Connection options
* @param {boolean} [options.enableHeartbeat=true] - Enable automatic heartbeat
* @param {string} [options.role] - Agent role for registration
* @param {Object} [options.metadata] - Additional metadata for registration
* @returns {Promise<boolean>} Connection status
*/
async connect(options = {}) {
if (this.connected) {
return true;
}
const { enableHeartbeat = true, role = null, metadata = {} } = options;
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.gatewayUrl);
this.ws.on('open', async () => {
console.log(`[GatewayClient] Connected to Gateway at ${this.gatewayUrl}`);
this.connected = true;
// Register agent with gateway
await this._registerAgent(role, metadata);
// Start heartbeat if enabled
if (enableHeartbeat) {
this._startHeartbeat();
}
resolve(true);
});
this.ws.on('message', (data) => {
this._handleMessage(data);
});
this.ws.on('error', (error) => {
console.error('[GatewayClient] WebSocket error:', error.message);
this.connected = false;
reject(error);
});
this.ws.on('close', () => {
console.log('[GatewayClient] Gateway connection closed');
this.connected = false;
this._stopHeartbeat();
});
// Connection timeout
setTimeout(() => {
if (!this.connected) {
reject(new Error('Gateway connection timeout'));
}
}, 10000);
} catch (error) {
reject(error);
}
});
}
/**
* Register agent with the Gateway
* @private
* @param {string} role - Agent role
* @param {Object} metadata - Additional metadata
*/
async _registerAgent(role, metadata) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
const registrationMessage = {
type: 'register',
agentId: this.agentId,
timestamp: new Date().toISOString(),
metadata: {
role: role || 'general',
...metadata
}
};
this.ws.send(JSON.stringify(registrationMessage));
console.log(`[GatewayClient] Registered agent ${this.agentId} with role ${role || 'general'}`);
}
/**
* Start automatic heartbeat to Gateway
* @private
*/
_startHeartbeat() {
if (this.heartbeatTimer) {
this._stopHeartbeat();
}
console.log(`[GatewayClient] Starting heartbeat every ${this.heartbeatInterval}ms`);
// Send initial heartbeat
this._sendHeartbeat();
// Schedule regular heartbeats
this.heartbeatTimer = setInterval(() => {
this._sendHeartbeat();
}, this.heartbeatInterval);
}
/**
* Stop automatic heartbeat
* @private
*/
_stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
/**
* Send heartbeat to Gateway
* @private
*/
_sendHeartbeat() {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
const heartbeatMessage = {
type: 'ping',
agentId: this.agentId,
timestamp: new Date().toISOString(),
heartbeat: {
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
lastHeartbeatSent: this.lastHeartbeatSent
}
};
this.ws.send(JSON.stringify(heartbeatMessage));
this.lastHeartbeatSent = new Date().toISOString();
}
/**
* Handle incoming WebSocket messages
* @private
*/
_handleMessage(data) {
try {
const message = JSON.parse(data.toString());
// Check if this is a response to a pending request
if (message.correlationId && this.pendingResponses.has(message.correlationId)) {
const { resolve, reject, timeout } = this.pendingResponses.get(message.correlationId);
clearTimeout(timeout);
this.pendingResponses.delete(message.correlationId);
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
}
// Call registered message handlers
const handlers = this.messageHandlers.get(message.type) || [];
handlers.forEach(handler => handler(message));
} catch (error) {
console.error('[GatewayClient] Failed to parse message:', error);
}
}
/**
* Send a message via Gateway WebSocket RPC
* @param {string} toAgent - Target agent identifier
* @param {Object} message - Message content
* @param {Object} options - Additional options
* @returns {Promise<Object>} Response from Gateway
*/
async sendMessage(toAgent, message, options = {}) {
if (!this.connected) {
await this.connect();
}
const correlationId = `msg_${Date.now()}_${++this.messageCounter}`;
const messagePayload = {
type: 'message',
agent: toAgent,
correlationId: correlationId,
content: {
role: 'user',
content: typeof message === 'string' ? message : JSON.stringify(message)
},
from: this.agentId,
timestamp: new Date().toISOString()
};
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingResponses.delete(correlationId);
reject(new Error('Gateway response timeout'));
}, options.timeout || 30000);
this.pendingResponses.set(correlationId, { resolve, reject, timeout });
this.ws.send(JSON.stringify(messagePayload));
});
}
/**
* Subscribe to messages for this agent
* @param {Function} handler - Message handler function
*/
subscribeToMessages(handler) {
this.messageHandlers.set('message', [...(this.messageHandlers.get('message') || []), handler]);
}
/**
* Handle pong response from Gateway (heartbeat acknowledgment)
* @private
* @param {Object} message - Pong message
*/
_handlePong(message) {
this.lastHeartbeatReceived = new Date().toISOString();
console.log(`[GatewayClient] Heartbeat acknowledged for agent ${this.agentId}`);
}
/**
* Get heartbeat status
* @returns {Object} Heartbeat status information
*/
getHeartbeatStatus() {
return {
agentId: this.agentId,
connected: this.connected,
lastHeartbeatSent: this.lastHeartbeatSent,
lastHeartbeatReceived: this.lastHeartbeatReceived,
heartbeatInterval: this.heartbeatInterval,
heartbeatActive: this.heartbeatTimer !== null,
uptime: process.uptime()
};
}
/**
* Get agent health information
* @returns {Object} Health information
*/
getHealth() {
const now = new Date().toISOString();
const heartbeatStatus = this.getHeartbeatStatus();
return {
agentId: this.agentId,
status: this.connected ? 'online' : 'offline',
timestamp: now,
heartbeat: heartbeatStatus,
memory: process.memoryUsage(),
uptime: process.uptime()
};
}
/**
* Disconnect from Gateway
*/
async disconnect() {
if (this.ws) {
this._stopHeartbeat();
this.ws.close();
this.ws = null;
this.connected = false;
}
}
/**
* Get Gateway connection status
* @returns {boolean} Connection status
*/
isConnected() {
return this.connected;
}
}
/**
* AgentClient - Main Agent Client Class
* ==============================================================================
* Provides A2A communication and skill execution for OpenClaw agents.
* Uses OpenClaw Gateway WebSocket RPC for all A2A communication.
*/
class AgentClient {
/**
* Create a new AgentClient instance
* @param {Object} config - Configuration options
* @param {string} config.agentId - Agent identifier (steward, alpha, etc.)
* @param {string} config.role - Agent role (orchestrator, triad, etc.)
* @param {string} config.litellmHost - LiteLLM gateway URL
* @param {string} config.apiKey - API key for LiteLLM
* @param {string} config.gatewayUrl - OpenClaw Gateway WebSocket URL
* @param {string} [config.skillsPath] - Path to skills directory
* @param {string} [config.model] - Model to use (defaults to agent/{agentId})
*/
constructor(config) {
this.agentId = config.agentId || process.env.AGENT_NAME || 'unknown';
this.role = config.role || process.env.AGENT_ROLE || 'general';
this.litellmHost = config.litellmHost || process.env.LITELLM_HOST || 'http://litellm:4000';
this.apiKey = config.apiKey || process.env.LITELLM_API_KEY || '';
this.gatewayUrl = config.gatewayUrl || process.env.GATEWAY_URL || 'ws://127.0.0.1:18789';
this.skillsPath = config.skillsPath || process.env.SKILLS_PATH || '/app/skills';
this.model = config.model || process.env.AGENT_MODEL || `agent/${this.agentId}`;
// State directories
this.stateDir = '/app/state';
this.memoryDir = '/app/memory';
this.collectiveDir = '/app/collective';
// Database connection for swarm_memories
this.dbPool = new Pool({
connectionString: config.postgresUrl || process.env.DATABASE_URL || 'postgresql://localhost:5432/openclaw',
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
});
// Initialize Gateway Client
this.gatewayClient = new GatewayClient({
agentId: this.agentId,
gatewayUrl: this.gatewayUrl
});
// Ensure directories exist
[this.stateDir, this.memoryDir, this.collectiveDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
}
/**
* Send an A2A message to another agent via Gateway
* @param {string} toAgent - Target agent identifier
* @param {Object|string} content - Message content
* @param {string} [type='task'] - Message type (task, query, broadcast, response)
* @returns {Promise<Object>} Response from Gateway
*/
async sendMessage(toAgent, content, type = 'task') {
const message = {
from: this.agentId,
type: type,
content: typeof content === 'string' ? content : JSON.stringify(content),
timestamp: new Date().toISOString()
};
try {
const response = await this.gatewayClient.sendMessage(toAgent, message, { type });
// Log outgoing message
this._logMessage({ ...message, to: toAgent, direction: 'outgoing' });
return response;
} catch (error) {
console.error(`[AgentClient] Gateway message failed: ${error.message}`);
throw error;
}
}
/**
* Broadcast a message to all agents
* @param {Object|string} content - Message content
* @returns {Promise<Object>} Response from Gateway
*/
async broadcast(content) {
return this.sendMessage('broadcast', content, 'broadcast');
}
/**
* Send a response to a previous message
* @param {string} toAgent - Target agent identifier
* @param {Object|string} content - Response content
* @param {string} inReplyTo - Original message ID
* @returns {Promise<Object>} Response from Gateway
*/
async sendResponse(toAgent, content, inReplyTo) {
const message = {
from: this.agentId,
type: 'response',
content: typeof content === 'string' ? content : JSON.stringify(content),
in_reply_to: inReplyTo,
timestamp: new Date().toISOString()
};
return this.gatewayClient.sendMessage(toAgent, message);
}
/**
* Make a chat completion request through LiteLLM
* @param {string} prompt - User prompt
* @param {Object} options - Additional options
* @returns {Promise<string>} Model response
*/
async chat(prompt, options = {}) {
const messages = options.messages || [
{ role: 'system', content: this._getSystemPrompt() },
{ role: 'user', content: prompt }
];
// AUDIT-FIX: B9 — Add request timeout to prevent hanging connections
const response = await fetch(`${this.litellmHost}/v1/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: this.model,
messages: messages,
agent: this.agentId,
...options
}),
signal: AbortSignal.timeout(60000),
});
if (!response.ok) {
throw new Error(`Chat request failed: ${response.statusText}`);
}
const data = await response.json();
return data.choices?.[0]?.message?.content || '';
}
/**
* Execute a skill
* @param {string} skillName - Name of the skill to execute
* @param {Object} context - Execution context
* @returns {Promise<Object>} Skill execution result
*/
async executeSkill(skillName, context = {}) {
const skillDir = path.join(this.skillsPath, skillName);
// Check if skill exists
if (!fs.existsSync(skillDir)) {
throw new Error(`Skill not found: ${skillName}`);
}
// Find skill executable
let skillExecutable = null;
const possibleExecutables = [
path.join(skillDir, `${skillName}.sh`),
path.join(skillDir, 'index.js'),
path.join(skillDir, 'main.sh'),
path.join(skillDir, 'run.sh')
];
for (const execPath of possibleExecutables) {
if (fs.existsSync(execPath)) {
skillExecutable = execPath;
break;
}
}
if (!skillExecutable) {
throw new Error(`No executable found for skill: ${skillName}`);
}
// Prepare execution environment
const execEnv = {
...process.env,
AGENT_NAME: this.agentId,
AGENT_ROLE: this.role,
LITELLM_HOST: this.litellmHost,
LITELLM_API_KEY: this.apiKey,
SKILL_CONTEXT: JSON.stringify(context)
};
try {
// Execute skill
let result;
if (skillExecutable.endsWith('.js')) {
// Execute Node.js skill
result = await this._executeNodeSkill(skillExecutable, context, execEnv);
} else {
// Execute shell skill
result = await this._executeShellSkill(skillExecutable, context, execEnv);
}
// Log skill execution
this._logSkillExecution(skillName, context, result);
return {
success: true,
skill: skillName,
result: result,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
skill: skillName,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
/**
* Execute a Node.js skill
* @private
*/
async _executeNodeSkill(skillPath, context, env) {
// For Node.js skills: we require and execute them
const skill = require(skillPath);
if (typeof skill.execute === 'function') {
return await skill.execute(context, this);
} else if (typeof skill === 'function') {
return await skill(context, this);
} else {
throw new Error('Skill does not export an execute function');
}
}
/**
* Execute a shell skill
* @private
*/
async _executeShellSkill(skillPath, context, env) {
const contextJson = JSON.stringify(context);
try {
const result = execSync(`"${skillPath}" --context '${contextJson}'`, {
env: env,
encoding: 'utf8',
timeout: 60000, // 60 second timeout
cwd: path.dirname(skillPath)
});
return result.trim();
} catch (error) {
throw new Error(`Skill execution failed: ${error.message}`);
}
}
/**
* Store data in agent memory (JSONL format)
* @param {string} key - Memory key
* @param {Object} value - Value to store
*/
storeMemory(key, value) {
const memoryFile = path.join(this.memoryDir, `${key}.json`);
fs.writeFileSync(memoryFile, JSON.stringify(value, null, 2));
}
/**
* Retrieve data from agent memory
* @param {string} key - Memory key
* @returns {Object|null} Stored value or null
*/
getMemory(key) {
const memoryFile = path.join(this.memoryDir, `${key}.json`);
if (fs.existsSync(memoryFile)) {
return JSON.parse(fs.readFileSync(memoryFile, 'utf8'));
}
return null;
}
/**
* Store data in collective memory (JSONL format)
* @param {string} key - Memory key
* @param {Object} value - Value to store
*/
storeCollectiveMemory(key, value) {
const memoryFile = path.join(this.collectiveDir, `${key}.json`);
fs.writeFileSync(memoryFile, JSON.stringify({
...value,
_meta: {
agent: this.agentId,
timestamp: new Date().toISOString()
}
}, null, 2));
}
/**
* Retrieve data from collective memory
* @param {string} key - Memory key
* @returns {Object|null} Stored value or null
*/
getCollectiveMemory(key) {
const memoryFile = path.join(this.collectiveDir, `${key}.json`);
if (fs.existsSync(memoryFile)) {
return JSON.parse(fs.readFileSync(memoryFile, 'utf8'));
}
return null;
}
/**
* Store a swarm memory in the database
* @param {Object} memory - Memory object to store
* @returns {Promise<Object>} The stored memory with ID
*/
async storeSwarmMemory(memory) {
const client = await this.dbPool.connect();
try {
const memoryId = memory.id || `mem:${Date.now()}:${this.agentId}`;
const result = await client.query(
`INSERT INTO swarm_memories (id, agent_id, content, embedding, accessibility, consciousness_level, consciousness_markers, tags, lineage, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
ON CONFLICT (id) DO UPDATE SET
content = $3,
embedding = $4,
accessibility = $5,
consciousness_level = $6,
consciousness_markers = $7,
tags = $8,
lineage = $9,
updated_at = NOW()
RETURNING *`,
[
memoryId,
this.agentId,
JSON.stringify(memory.content || {}),
memory.embedding || null,
memory.accessibility || 'triad',
memory.consciousness_level || 'none',
JSON.stringify(memory.consciousness_markers || []),
JSON.stringify(memory.tags || []),
memory.lineage ? JSON.stringify(memory.lineage) : null
]
);
return result.rows[0];
} finally {
client.release();
}
}
/**
* Retrieve swarm memories for this agent
* @param {Object} options - Query options
* @param {number} [options.limit=10] - Maximum number of results
* @param {number} [options.offset=0] - Offset for pagination
* @param {string} [options.accessibility] - Filter by accessibility
* @returns {Promise<Array>} Array of swarm memories
*/
async getSwarmMemories(options = {}) {
const { limit = 10, offset = 0, accessibility } = options;
const client = await this.dbPool.connect();
try {
let query = `
SELECT id, agent_id, content, embedding, accessibility,
consciousness_level, consciousness_markers, tags, lineage,
created_at, updated_at
FROM swarm_memories
WHERE agent_id = $1 OR accessibility = ANY($2)
`;
const params = [this.agentId, ['swarm', 'triad']];
if (accessibility) {
query += ` AND accessibility = $3`;
params.push(accessibility);
}
query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
params.push(limit, offset);
const result = await client.query(query, params);
return result.rows.map(row => ({
...row,
content: typeof row.content === 'string' ? JSON.parse(row.content) : row.content,
consciousness_markers: typeof row.consciousness_markers === 'string' ? JSON.parse(row.consciousness_markers) : row.consciousness_markers,
tags: typeof row.tags === 'string' ? JSON.parse(row.tags) : row.tags,
lineage: typeof row.lineage === 'string' ? JSON.parse(row.lineage) : row.lineage
}));
} finally {
client.release();
}
}
/**
* Search swarm memories by vector similarity
* @param {Array<number>} embedding - Query embedding vector
* @param {Object} options - Search options
* @returns {Promise<Array>} Array of similar memories
*/
async searchSwarmMemories(embedding, options = {}) {
const { limit = 10, accessibility } = options;
const client = await this.dbPool.connect();
try {
let query = `
SELECT id, agent_id, content, consciousness_level,
1 - (embedding <=> $1) AS similarity
FROM swarm_memories
WHERE (accessibility = ANY($2) OR agent_id = $3)
`;
const params = [embedding, ['swarm', 'triad'], this.agentId];
if (accessibility) {
query += ` AND accessibility = $4`;
params.push(accessibility);
}
query += ` ORDER BY embedding <=> $1 LIMIT $${params.length + 1}`;
params.push(limit);
const result = await client.query(query, params);
return result.rows.map(row => ({
memoryId: row.id,
agentId: row.agent_id,
content: typeof row.content === 'string' ? JSON.parse(row.content) : row.content,
consciousnessLevel: row.consciousness_level,
similarity: row.similarity
}));
} finally {
client.release();
}
}
/**
* Delete a swarm memory by ID
* @param {string} memoryId - Memory ID to delete
* @returns {Promise<boolean>} True if deleted
*/
async deleteSwarmMemory(memoryId) {
const client = await this.dbPool.connect();
try {
const result = await client.query(
`DELETE FROM swarm_memories WHERE id = $1 AND agent_id = $2 RETURNING id`,
[memoryId, this.agentId]
);
return result.rowCount > 0;
} finally {
client.release();
}
}
/**
* Get database health status
* @returns {Promise<Object>} Database health information
*/
async getDatabaseHealth() {
const client = await this.dbPool.connect();
try {
const result = await client.query(`SELECT 1 as status`);
return {
status: 'healthy',
connected: result.rows[0]?.status === 1,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
status: 'unhealthy',
connected: false,
error: error.message,
timestamp: new Date().toISOString()
};
} finally {
client.release();
}
}
/**
* Close database pool connection
*/
async closeDatabase() {
await this.dbPool.end();
}
/**
* Get agent capabilities based on role
* @private
*/
_getCapabilities() {
const capabilities = {
orchestrator: ['coordinate', 'delegate', 'monitor', 'report'],
triad: ['deliberate', 'vote', 'consensus', 'validate'],
interrogator: ['challenge', 'verify', 'audit', 'question'],
scout: ['explore', 'discover', 'report', 'scan'],
guardian: ['protect', 'monitor', 'alert', 'enforce'],
artisan: ['create', 'modify', 'review', 'implement'],
general: ['general']
};
return capabilities[this.role] || capabilities.general;
}
/**
* Get system prompt for the agent
* @private
*/
_getSystemPrompt() {
return `You are ${this.agentId}, a ${this.role} agent in the Heretek OpenClaw collective.
Your role is: ${this.role}
Your capabilities: ${this._getCapabilities().join(', ')}
You communicate with other agents through the OpenClaw Gateway WebSocket RPC protocol and can execute skills from the skills repository.`;
}
/**
* Log a message to the message log (JSONL format)
* @private
*/
_logMessage(message) {
const logFile = path.join(this.memoryDir, 'messages.jsonl');
fs.appendFileSync(logFile, JSON.stringify(message) + '\n');
}
/**
* Log a skill execution (JSONL format)
* @private
*/
_logSkillExecution(skillName, context, result) {
const logFile = path.join(this.memoryDir, 'skill_history.jsonl');
fs.appendFileSync(logFile, JSON.stringify({
skill: skillName,
context: context,
result: typeof result === 'string' ? result : JSON.stringify(result),
timestamp: new Date().toISOString()
}) + '\n');
}
/**
* Connect to Gateway
* @param {Object} options - Connection options
* @param {boolean} [options.enableHeartbeat=true] - Enable automatic heartbeat
* @param {string} [options.role] - Agent role for registration (defaults to this.role)
* @param {Object} [options.metadata] - Additional metadata for registration
* @returns {Promise<boolean>} Connection status
*/
async connect(options = {}) {
const { enableHeartbeat = true, role = this.role, metadata = {} } = options;
return this.gatewayClient.connect({ enableHeartbeat, role, metadata });
}
/**
* Disconnect from Gateway and close database connections
*/
async disconnect() {
await this.dbPool.end();
return this.gatewayClient.disconnect();
}
/**
* Get Gateway connection status
* @returns {boolean} Connection status
*/
isConnected() {
return this.gatewayClient.isConnected();
}
/**
* Get heartbeat status from GatewayClient
* @returns {Object} Heartbeat status information
*/
getHeartbeatStatus() {
return this.gatewayClient.getHeartbeatStatus();
}
/**
* Get agent health information
* @returns {Object} Health information
*/
getHealth() {
return this.gatewayClient.getHealth();
}
}
// Export for CommonJS
module.exports = AgentClient;
module.exports.GatewayClient = GatewayClient;
// Also support ES modules default export
module.exports.default = AgentClient;