clawdie-ai/setup/hostd.ts
Operator & Codex dcae5878fa Replace jail sudo path with hostd bastille-cmd
Route jail exec through a new hostd bastille-cmd operation, remove the agent-jail sudoers requirement, and fall back to repository ownership when elevation does not provide SUDO_* metadata.

---
Build: pass | Tests: n/a (Vitest not run in this Linux environment per repo policy)

---
Build: pass | Tests: pass — 2375 passed (704 files)
2026-05-10 22:48:04 +02:00

220 lines
6 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 the platform service identity in
* src/hostd/types.ts.
*/
import { SERVICE_NAME } from '../src/platform-identity.js';
import { execFileSync, execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import {
PLATFORM_RUNTIME_HOME,
} 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 lookupUserByUid(uid: number): string | null {
try {
const user = execFileSync('id', ['-nu', String(uid)], { encoding: 'utf-8' }).trim();
return user || null;
} catch {
return null;
}
}
function getInvokingContext(
fallbackPath: string,
): { 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;
if (user && Number.isFinite(uid) && Number.isFinite(gid)) {
return {
user,
uid,
gid,
};
}
try {
const stat = fs.statSync(fallbackPath);
return {
user: stat.uid === 0 ? null : lookupUserByUid(stat.uid),
uid: stat.uid,
gid: stat.gid,
};
} catch {
// fall through
}
return {
user: null,
uid: null,
gid: null,
};
}
function buildProject(projectRoot: string): void {
const invoking = getInvokingContext(projectRoot);
if (isRoot() && invoking.user && invoking.uid !== 0) {
execFileSync(
'su',
[
'-m',
invoking.user,
'-c',
`cd ${JSON.stringify(projectRoot)} && npm run build`,
],
{ cwd: projectRoot, stdio: 'ignore' },
);
return;
}
execSync('npm run build', { cwd: projectRoot, stdio: 'ignore' });
}
function chownIfInvokingContext(filePath: string, fallbackPath: string): void {
const invoking = getInvokingContext(fallbackPath);
if (!isRoot() || invoking.uid === null || invoking.gid === null || invoking.uid === 0) {
return;
}
try {
fs.chownSync(filePath, invoking.uid, invoking.gid);
} catch {
// best-effort only
}
}
function writeFile(filePath: string, contents: string, mode = 0o755): void {
fs.writeFileSync(filePath, contents, { mode });
}
function generateRunScript(
platformServiceName: string,
runtimeHome: string,
nodePath: string,
projectRoot: string,
): string {
return [
'#!/bin/sh',
'set -eu',
`export SERVICE_NAME=${JSON.stringify(platformServiceName)}`,
`export HOME=${JSON.stringify(runtimeHome)}`,
'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(
platformServiceName: string,
projectRoot: string,
runScriptPath: string,
): string {
const name = `${platformServiceName}_hostd`;
const logPath = path.join(projectRoot, 'logs', `${platformServiceName}-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/${platformServiceName}-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 platformServiceName = SERVICE_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, platformServiceName }, '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 });
chownIfInvokingContext(path.join(projectRoot, 'logs'), projectRoot);
const runScriptPath = path.join(projectRoot, `run-${platformServiceName}-hostd.sh`);
const rcdPath = `/usr/local/etc/rc.d/${platformServiceName}_hostd`;
writeFile(
runScriptPath,
generateRunScript(
platformServiceName,
PLATFORM_RUNTIME_HOME,
nodePath,
projectRoot,
),
);
chownIfInvokingContext(runScriptPath, projectRoot);
writeFile(
rcdPath,
generateRcdService(platformServiceName, projectRoot, runScriptPath),
);
// Enable + start
try {
execSync(`sysrc ${platformServiceName}_hostd_enable=YES`, { stdio: 'ignore' });
} catch (err) {
logger.warn({ err }, 'sysrc hostd enable failed');
}
try {
execSync(`service ${platformServiceName}_hostd restart`, { stdio: 'ignore' });
} catch (err) {
logger.warn({ err }, 'service hostd restart failed');
}
emitStatus('SETUP_HOSTD', {
STATUS: 'success',
SERVICE_NAME: `${platformServiceName}_hostd`,
RCD_PATH: rcdPath,
RUN_SCRIPT: runScriptPath,
LOG,
});
}