fix(preflight): default mounts config and flag root-only steps

This commit is contained in:
Sam & Claude 2026-03-15 00:37:43 +01:00
parent cfd327f285
commit f1f5cf7947
2 changed files with 81 additions and 19 deletions

View file

@ -24,6 +24,26 @@ function parseArgs(args: string[]): { empty: boolean; json: string } {
return { empty, json };
}
function writeAllowlist(
configFile: string,
parsed: { allowedRoots?: unknown[]; blockedPatterns?: unknown[]; nonMainReadOnly?: boolean },
): { allowedRoots: number; nonMainReadOnly: string } {
fs.writeFileSync(configFile, JSON.stringify(parsed, null, 2) + '\n');
return {
allowedRoots: Array.isArray(parsed.allowedRoots) ? parsed.allowedRoots.length : 0,
nonMainReadOnly: parsed.nonMainReadOnly === false ? 'false' : 'true',
};
}
function writeEmptyAllowlist(configFile: string): { allowedRoots: number; nonMainReadOnly: string } {
logger.info('Writing empty mount allowlist');
return writeAllowlist(configFile, {
allowedRoots: [],
blockedPatterns: [],
nonMainReadOnly: true,
});
}
export async function run(args: string[]): Promise<void> {
const { empty, json } = parseArgs(args);
const homeDir = os.homedir();
@ -42,13 +62,7 @@ export async function run(args: string[]): Promise<void> {
let nonMainReadOnly = 'true';
if (empty) {
logger.info('Writing empty mount allowlist');
const emptyConfig = {
allowedRoots: [],
blockedPatterns: [],
nonMainReadOnly: true,
};
fs.writeFileSync(configFile, JSON.stringify(emptyConfig, null, 2) + '\n');
({ allowedRoots, nonMainReadOnly } = writeEmptyAllowlist(configFile));
} else if (json) {
// Validate JSON with JSON.parse (not piped through shell)
let parsed: { allowedRoots?: unknown[]; nonMainReadOnly?: boolean };
@ -68,15 +82,38 @@ export async function run(args: string[]): Promise<void> {
return; // unreachable but satisfies TS
}
fs.writeFileSync(configFile, JSON.stringify(parsed, null, 2) + '\n');
allowedRoots = Array.isArray(parsed.allowedRoots)
? parsed.allowedRoots.length
: 0;
nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true';
({ allowedRoots, nonMainReadOnly } = writeAllowlist(configFile, parsed));
} else {
// Read from stdin
// Read from stdin when piped, otherwise create the default empty config.
if (process.stdin.isTTY) {
({ allowedRoots, nonMainReadOnly } = writeEmptyAllowlist(configFile));
logger.info('No stdin provided, wrote default empty mount allowlist');
emitStatus('CONFIGURE_MOUNTS', {
PATH: configFile,
ALLOWED_ROOTS: allowedRoots,
NON_MAIN_READ_ONLY: nonMainReadOnly,
DEFAULTED: 'yes',
STATUS: 'success',
LOG: 'logs/setup.log',
});
return;
}
logger.info('Reading mount allowlist from stdin');
const input = fs.readFileSync(0, 'utf-8');
if (!input.trim()) {
({ allowedRoots, nonMainReadOnly } = writeEmptyAllowlist(configFile));
logger.info('Empty stdin, wrote default empty mount allowlist');
emitStatus('CONFIGURE_MOUNTS', {
PATH: configFile,
ALLOWED_ROOTS: allowedRoots,
NON_MAIN_READ_ONLY: nonMainReadOnly,
DEFAULTED: 'yes',
STATUS: 'success',
LOG: 'logs/setup.log',
});
return;
}
let parsed: { allowedRoots?: unknown[]; nonMainReadOnly?: boolean };
try {
parsed = JSON.parse(input);
@ -94,11 +131,7 @@ export async function run(args: string[]): Promise<void> {
return;
}
fs.writeFileSync(configFile, JSON.stringify(parsed, null, 2) + '\n');
allowedRoots = Array.isArray(parsed.allowedRoots)
? parsed.allowedRoots.length
: 0;
nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true';
({ allowedRoots, nonMainReadOnly } = writeAllowlist(configFile, parsed));
}
logger.info(

View file

@ -1,6 +1,7 @@
import { spawnSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { AGENT_DOMAIN, AGENT_NAME } from '../src/config.js';
import { logger } from '../src/logger.js';
@ -18,6 +19,7 @@ interface StepDefinition {
command: string;
args: string[];
interactive?: boolean;
requiresRoot?: boolean;
}
interface StepResult {
@ -160,36 +162,41 @@ function buildSteps(opts: PreflightArgs): StepDefinition[] {
label: 'Jails',
command: 'npm',
args: ['run', 'setup', '--', '--step', 'jails', '--create'],
requiresRoot: true,
},
{
id: 'db',
label: 'DB',
command: 'npm',
args: ['run', 'setup', '--', '--step', 'db'],
requiresRoot: true,
},
{
id: 'git',
label: 'Git',
command: 'npm',
args: ['run', 'setup', '--', '--step', 'git'],
requiresRoot: true,
},
{
id: 'cms',
label: 'CMS',
command: 'npm',
args: ['run', 'setup', '--', '--step', 'cms'],
requiresRoot: true,
},
{
id: 'hosts',
label: 'Hosts',
command: 'npm',
args: ['run', 'setup', '--', '--step', 'hosts'],
requiresRoot: true,
},
{
id: 'mounts',
label: 'Mounts',
command: 'npm',
args: ['run', 'setup', '--', '--step', 'mounts'],
args: ['run', 'setup', '--', '--step', 'mounts', '--empty'],
},
{
id: 'telegram-auth',
@ -208,6 +215,7 @@ function buildSteps(opts: PreflightArgs): StepDefinition[] {
label: 'Verify',
command: 'npm',
args: ['run', 'setup', '--', '--step', 'verify'],
requiresRoot: true,
},
{
id: 'doctor',
@ -229,6 +237,27 @@ function runStep(
const commandLine = [step.command, ...step.args].map(quoteShell).join(' ');
const logFile = path.join(runDir, `${step.id}.log`);
const interactive = step.interactive === true;
const freebsdNeedsRoot = os.platform() === 'freebsd' && process.getuid?.() !== 0;
if (step.requiresRoot && freebsdNeedsRoot) {
const output = 'root_required\n';
fs.writeFileSync(logFile, output);
const finished = new Date();
return {
id: step.id,
label: step.label,
commandLine,
exitCode: 1,
status: 'failed',
startedAt: started.toISOString(),
finishedAt: finished.toISOString(),
logFile,
fields: {
ERROR: 'root_required',
HINT: 'rerun_root_required_steps_as_root',
},
};
}
if (interactive && (!process.stdin.isTTY || !process.stdout.isTTY)) {
const output = 'interactive_tty_required\n';