clawdie-ai/setup/hostd.ts

163 lines
5 KiB
TypeScript

/**
* setup/hostd.ts — Install and start the privileged host daemon (hostd).
*
* This is a root-only step. It installs an rc.d service that runs:
* node dist/hostd/index.js
*
* The daemon socket path is derived from AGENT_NAME in src/hostd/types.ts.
* This setup step exports AGENT_NAME explicitly for the rc.d service.
*/
import { execFileSync, execSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { AGENT_NAME } from '../src/config.js';
import { logger } from '../src/logger.js';
import { commandExists, getNodePath, getPlatform, isRoot } from './platform.js';
import { emitStatus } from './status.js';
const LOG = 'logs/setup.log';
function getSudoContext(): { user: string | null; uid: number | null; gid: number | null } {
const user = (process.env.SUDO_USER || '').trim() || null;
const uidRaw = (process.env.SUDO_UID || '').trim();
const gidRaw = (process.env.SUDO_GID || '').trim();
const uid = uidRaw ? Number(uidRaw) : null;
const gid = gidRaw ? Number(gidRaw) : null;
return {
user,
uid: Number.isFinite(uid) ? uid : null,
gid: Number.isFinite(gid) ? gid : null,
};
}
function buildProject(projectRoot: string): void {
const sudo = getSudoContext();
if (isRoot() && sudo.user) {
execFileSync(
'su',
['-m', sudo.user, '-c', `cd ${JSON.stringify(projectRoot)} && npm run build`],
{ cwd: projectRoot, stdio: 'ignore' },
);
return;
}
execSync('npm run build', { cwd: projectRoot, stdio: 'ignore' });
}
function chownIfSudoContext(filePath: string): void {
const sudo = getSudoContext();
if (!isRoot() || sudo.uid === null || sudo.gid === null) return;
try {
fs.chownSync(filePath, sudo.uid, sudo.gid);
} catch {
// best-effort only
}
}
function writeFile(filePath: string, contents: string, mode = 0o755): void {
fs.writeFileSync(filePath, contents, { mode });
}
function generateRunScript(agentName: string, nodePath: string, projectRoot: string): string {
return [
'#!/bin/sh',
'set -eu',
`export AGENT_NAME=${JSON.stringify(agentName)}`,
`export HOME=${JSON.stringify(os.homedir())}`,
'export PATH="/usr/local/bin:/usr/bin:/bin"',
`cd ${JSON.stringify(projectRoot)}`,
`exec ${JSON.stringify(nodePath)} ${JSON.stringify(path.join(projectRoot, 'dist/hostd/index.js'))}`,
'',
].join('\n');
}
function generateRcdService(agentName: string, projectRoot: string, runScriptPath: string): string {
const name = `${agentName}_hostd`;
const logPath = path.join(projectRoot, 'logs', `${agentName}-hostd.log`);
return [
'#!/bin/sh',
'#',
`# PROVIDE: ${name}`,
'# REQUIRE: NETWORKING LOGIN',
'# KEYWORD: shutdown',
'',
'. /etc/rc.subr',
'',
`name="${name}"`,
`rcvar="${name}_enable"`,
'command="/usr/sbin/daemon"',
`pidfile="/var/run/${agentName}-hostd-supervisor.pid"`,
'command_args="-P ${pidfile} -r -o ' +
JSON.stringify(logPath) +
' ' +
JSON.stringify(runScriptPath) +
'"',
'',
'load_rc_config $name',
': ${' + name + '_enable:="NO"}',
'',
'run_rc_command "$1"',
'',
].join('\n');
}
export async function run(_args: string[]): Promise<void> {
const projectRoot = process.cwd();
const platform = getPlatform();
const nodePath = getNodePath();
const agentName = AGENT_NAME;
if (platform !== 'freebsd') {
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'unsupported_platform', LOG });
process.exit(1);
}
if (!isRoot()) {
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'requires_root', LOG });
throw new Error('setup_hostd_requires_root');
}
if (!commandExists('daemon')) {
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'missing_daemon_bin', LOG });
throw new Error('missing_daemon_bin');
}
logger.info({ projectRoot, agentName }, 'Setting up hostd service');
// Ensure dist exists
try {
buildProject(projectRoot);
} catch {
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'build_failed', LOG });
process.exit(1);
}
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
chownIfSudoContext(path.join(projectRoot, 'logs'));
const runScriptPath = path.join(projectRoot, `run-${agentName}-hostd.sh`);
const rcdPath = `/usr/local/etc/rc.d/${agentName}_hostd`;
writeFile(runScriptPath, generateRunScript(agentName, nodePath, projectRoot));
chownIfSudoContext(runScriptPath);
writeFile(rcdPath, generateRcdService(agentName, projectRoot, runScriptPath));
// Enable + start
try {
execSync(`sysrc ${agentName}_hostd_enable=YES`, { stdio: 'ignore' });
} catch (err) {
logger.warn({ err }, 'sysrc hostd enable failed');
}
try {
execSync(`service ${agentName}_hostd restart`, { stdio: 'ignore' });
} catch (err) {
logger.warn({ err }, 'service hostd restart failed');
}
emitStatus('SETUP_HOSTD', {
STATUS: 'success',
SERVICE_NAME: `${agentName}_hostd`,
RCD_PATH: rcdPath,
RUN_SCRIPT: runScriptPath,
LOG,
});
}