openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
266 lines • 8.63 kB
JavaScript
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs, { constants, openSync, writeSync, closeSync } from 'node:fs';
import { createServer } from 'node:net';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { Client, PipeTransport, StdioClientTransport, } from '../third_party/index.js';
import { logger, puppeteerLogger } from '../utils/logger.js';
import { VERSION } from '../version.js';
import { DAEMON_CLIENT_NAME, getPidFilePath, getSocketPath, INDEX_SCRIPT_PATH, IS_WINDOWS, isDaemonRunning, assertValidSessionId, } from './utils.js';
const sessionId = process.env.CHROME_DEVTOOLS_MCP_SESSION_ID || '';
assertValidSessionId(sessionId);
logger?.(`Daemon sessionId: ${sessionId}`);
if (isDaemonRunning(sessionId)) {
logger?.('Another daemon process is running.');
process.exit(1);
}
const pidFilePath = getPidFilePath(sessionId);
const pidDir = path.dirname(pidFilePath);
const currentUserUid = os.userInfo().uid;
try {
fs.mkdirSync(pidDir, { recursive: true, mode: 0o700 });
if (os.platform() !== 'win32') {
// POSIX specific checks
try {
const stats = fs.statSync(pidDir);
// 1. Check Ownership: Ensure the directory is owned by the current user.
if (stats.uid !== currentUserUid) {
console.error(`[MCP Daemon] Critical error: PID directory ${pidDir} is not owned by the current user (Expected: ${currentUserUid}, Found: ${stats.uid}). Possible tampering.`);
process.exit(1);
}
// 2. Check Permissions: Ensure the directory is not group or world-writable.
// Mode is a number, e.g., 0o700. We check if bits for group/world write are set.
const mode = stats.mode;
if (mode & constants.S_IWGRP || mode & constants.S_IWOTH) {
console.error(`[MCP Daemon] Critical error: PID directory ${pidDir} has insecure permissions (Mode: ${mode.toString(8)}). It should not be writable by group or others.`);
process.exit(1);
}
}
catch (statErr) {
console.error(`[MCP Daemon] Critical error stating PID directory ${pidDir}:`, statErr);
process.exit(1);
}
}
}
catch (err) {
console.error(`[MCP Daemon] Critical error creating/validating PID directory: ${pidDir}`, err);
process.exit(1);
}
let fd = -1;
try {
// Open the file with flags to:
// - O_WRONLY: Write-only
// - O_CREAT: Create if it doesn't exist
// - O_TRUNC: Truncate to zero length if it exists
// - O_NOFOLLOW: DO NOT follow symlinks.
// - 0o600: Permissions: read/write for owner, no permissions for others.
fd = openSync(pidFilePath, constants.O_WRONLY |
constants.O_CREAT |
constants.O_TRUNC |
constants.O_NOFOLLOW, 0o600);
writeSync(fd, process.pid.toString());
}
catch (err) {
console.error(`[MCP Daemon] Critical error writing PID file: ${pidFilePath}`, err);
// If openSync fails due to O_NOFOLLOW on a symlink, the error will be caught here.
process.exit(1);
}
finally {
if (fd !== -1) {
try {
closeSync(fd);
}
catch (err) {
console.error(`[MCP Daemon] Error closing PID file: ${pidFilePath}`, err);
}
}
}
logger?.(`Writing ${process.pid.toString()} to ${pidFilePath}`);
const socketPath = getSocketPath(sessionId);
const startDate = new Date();
const mcpServerArgs = process.argv.slice(2);
let mcpClient = null;
let mcpTransport = null;
let server = null;
async function setupMCPClient() {
console.log('Setting up MCP client connection...');
// Create stdio transport for chrome-devtools-mcp
mcpTransport = new StdioClientTransport({
command: process.execPath,
args: [INDEX_SCRIPT_PATH, ...mcpServerArgs],
env: process.env,
});
mcpClient = new Client({
name: DAEMON_CLIENT_NAME,
version: VERSION,
}, {
capabilities: {},
});
await mcpClient.connect(mcpTransport);
console.log('MCP client connected');
}
async function handleRequest(msg) {
try {
if (msg.method === 'invoke_tool') {
if (!mcpClient) {
throw new Error('MCP client not initialized');
}
const { tool, args } = msg;
const result = (await mcpClient.callTool({
name: tool,
arguments: args || {},
}));
return {
success: true,
result: JSON.stringify(result),
};
}
else if (msg.method === 'stop') {
// Ensure we are not interrupting in-progress starting.
await started;
// Trigger cleanup asynchronously.
setImmediate(() => {
void cleanup();
});
return {
success: true,
message: 'stopping',
};
}
else if (msg.method === 'status') {
await started;
const statusResult = {
pid: process.pid,
socketPath,
startDate: startDate.toISOString(),
version: VERSION,
args: mcpServerArgs,
};
return {
success: true,
result: JSON.stringify(statusResult),
};
}
{
return {
success: false,
error: `Unknown method: ${JSON.stringify(msg, null, 2)}`,
};
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage,
};
}
}
async function startSocketServer() {
// Remove existing socket file if it exists (only on non-Windows)
if (!IS_WINDOWS) {
try {
fs.unlinkSync(socketPath);
}
catch {
// ignore errors.
}
}
return await new Promise((resolve, reject) => {
server = createServer(socket => {
const transport = new PipeTransport(socket, socket, puppeteerLogger);
transport.onmessage = async (message) => {
logger?.('onmessage', message);
const response = await handleRequest(JSON.parse(message));
transport.send(JSON.stringify(response));
socket.end();
};
socket.on('error', error => {
logger?.('Socket error:', error);
});
});
server.listen({
path: socketPath,
readableAll: false,
writableAll: false,
}, async () => {
console.log(`Daemon server listening on ${socketPath}`);
try {
// Setup MCP client
await setupMCPClient();
resolve();
}
catch (err) {
reject(err);
}
});
server.on('error', error => {
logger?.('Server error:', error);
reject(error);
});
});
}
async function cleanup(exitCode = 0) {
console.log('Cleaning up daemon...');
try {
await mcpClient?.close();
}
catch (error) {
logger?.('Error closing MCP client:', error);
}
try {
await mcpTransport?.close();
}
catch (error) {
logger?.('Error closing MCP transport:', error);
}
if (server) {
await new Promise(resolve => {
server.close(() => resolve());
});
}
if (!IS_WINDOWS) {
try {
fs.unlinkSync(socketPath);
}
catch {
// ignore errors
}
}
logger?.(`unlinking ${pidFilePath}`);
if (fs.existsSync(pidFilePath)) {
fs.unlinkSync(pidFilePath);
}
process.exit(exitCode);
}
// Handle shutdown signals
process.on('SIGTERM', () => {
void cleanup();
});
process.on('SIGINT', () => {
void cleanup();
});
process.on('SIGHUP', () => {
void cleanup();
});
// Handle uncaught errors
process.on('uncaughtException', error => {
logger?.('Uncaught exception:', error);
void cleanup(1);
});
process.on('unhandledRejection', error => {
logger?.('Unhandled rejection:', error);
void cleanup(1);
});
// Start the server
const started = startSocketServer().catch(error => {
logger?.('Failed to start daemon server:', error);
void cleanup(1);
});
//# sourceMappingURL=daemon.js.map