215 lines
9 KiB
TypeScript
215 lines
9 KiB
TypeScript
/**
|
|
* Step: env-audit — Summarize .env completeness (no secrets printed).
|
|
*
|
|
* This is a read-only check intended to reduce install friction by showing
|
|
* which environment variables are still unset and what defaults will be used.
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { extractEnvValue } from './profile.js';
|
|
import { emitStatus } from './status.js';
|
|
import { getPlatform } from './platform.js';
|
|
|
|
export interface EnvAuditResult {
|
|
envFile: string;
|
|
missing: string[];
|
|
warnings: string[];
|
|
values: Record<string, string>;
|
|
}
|
|
|
|
function envValue(envContent: string, key: string): string | null {
|
|
const value = extractEnvValue(envContent, key);
|
|
return value && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
function isTruthyFlag(raw: string | null): boolean {
|
|
if (!raw) return false;
|
|
return /^(YES|yes|true|TRUE|1)$/u.test(raw.trim());
|
|
}
|
|
|
|
export function auditEnvFile(envFile: string): EnvAuditResult {
|
|
const content = fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf-8') : '';
|
|
|
|
const agentName = (envValue(content, 'AGENT_NAME') || 'clawdie').toLowerCase();
|
|
const assistantName = envValue(content, 'ASSISTANT_NAME') || '';
|
|
const displayLocale = envValue(content, 'DISPLAY_LOCALE') || envValue(content, 'SETUP_LOCALE') || '';
|
|
const systemLocale = envValue(content, 'SYSTEM_LOCALE') || '';
|
|
const timeZone = envValue(content, 'TZ') || '';
|
|
const publicDomain = envValue(content, 'AGENT_DOMAIN') || 'home.arpa';
|
|
const internalDomain = envValue(content, 'AGENT_INTERNAL_DOMAIN') || `${agentName}.home.arpa`;
|
|
|
|
const subnetBase =
|
|
envValue(content, 'AGENT_SUBNET_BASE') ||
|
|
envValue(content, 'WARDEN_SUBNET_BASE') ||
|
|
'10.0.0';
|
|
const gateway = envValue(content, 'WARDEN_GATEWAY') || `${subnetBase}.1`;
|
|
const subnet = envValue(content, 'WARDEN_SUBNET') || `${subnetBase}.0/24`;
|
|
const dbRuntime = (envValue(content, 'DB_RUNTIME') || 'jail').toLowerCase();
|
|
const dbHost = envValue(content, 'DB_HOST') || (dbRuntime === 'host' ? `${subnetBase}.1` : '');
|
|
const dbIp = envValue(content, 'WARDEN_DB_IP') || `${subnetBase}.3`;
|
|
const cmsIp = envValue(content, 'WARDEN_CMS_IP') || envValue(content, 'CMS_JAIL_IP') || `${subnetBase}.4`;
|
|
const gitIp = envValue(content, 'WARDEN_GIT_IP') || `${subnetBase}.6`;
|
|
const ollamaIp = envValue(content, 'WARDEN_OLLAMA_IP') || `${subnetBase}.5`;
|
|
const llamaCppIp = envValue(content, 'WARDEN_LLAMA_CPP_IP') || ollamaIp;
|
|
|
|
const piProfile = envValue(content, 'PI_TUI_PROFILE') || 'operator';
|
|
const codeHostingMode = envValue(content, 'CODE_HOSTING_MODE') || 'git';
|
|
const remoteGitUrl = envValue(content, 'REMOTE_GIT_URL') || '';
|
|
const rawGitMirrorUrls = envValue(content, 'GIT_MIRROR_URLS') || '';
|
|
const gitMirrorUrls = rawGitMirrorUrls
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean);
|
|
const hasGitMirrors = gitMirrorUrls.length > 0;
|
|
const featureGit = isTruthyFlag(envValue(content, 'FEATURE_GIT'));
|
|
const featureGitea = isTruthyFlag(envValue(content, 'FEATURE_GITEA'));
|
|
const localLlmProvider = envValue(content, 'LOCAL_LLM_PROVIDER') || 'none';
|
|
const featureOllama = isTruthyFlag(envValue(content, 'FEATURE_OLLAMA'));
|
|
const featureLlamaCpp = isTruthyFlag(envValue(content, 'FEATURE_LLAMA_CPP'));
|
|
const featureTailscale = isTruthyFlag(envValue(content, 'FEATURE_TAILSCALE'));
|
|
const tailscaleAuthKey = envValue(content, 'TAILSCALE_AUTHKEY') || '';
|
|
|
|
// Setup step defaults (match setup/*.ts behavior).
|
|
const cmsJailName = envValue(content, 'CMS_JAIL_NAME') || 'cms';
|
|
const gitJailName = envValue(content, 'GIT_JAIL_NAME') || `${agentName}-git`;
|
|
const ollamaJailName = envValue(content, 'OLLAMA_JAIL_NAME') || `${agentName}-ollama`;
|
|
const llamaCppJailName = envValue(content, 'LLAMA_CPP_JAIL_NAME') || `${agentName}-llamacpp`;
|
|
const sshPublicKey = envValue(content, 'SSH_PUBLIC_KEY') || '';
|
|
|
|
const missing: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
if (!assistantName) missing.push('ASSISTANT_NAME');
|
|
if (!displayLocale) warnings.push('DISPLAY_LOCALE not set (will use detected/default)');
|
|
if (!systemLocale) warnings.push('SYSTEM_LOCALE not set (will be derived from DISPLAY_LOCALE)');
|
|
if (!timeZone) warnings.push('TZ not set (will use detected/default)');
|
|
if (!envValue(content, 'AGENT_SUBNET_BASE') && !envValue(content, 'WARDEN_SUBNET_BASE')) {
|
|
warnings.push(`AGENT_SUBNET_BASE not set (defaulting to ${subnetBase})`);
|
|
}
|
|
if (!remoteGitUrl && !hasGitMirrors && (codeHostingMode === 'git' || codeHostingMode === 'gitea')) {
|
|
warnings.push('REMOTE_GIT_URL not set (git jail will create an empty bare repo)');
|
|
}
|
|
if (!featureGit && (codeHostingMode === 'git' || codeHostingMode === 'gitea')) {
|
|
warnings.push('FEATURE_GIT is disabled (CODE_HOSTING_MODE indicates local git)');
|
|
}
|
|
if (featureGitea && !featureGit) {
|
|
warnings.push('FEATURE_GITEA=YES but FEATURE_GIT is not enabled');
|
|
}
|
|
if (featureTailscale && !tailscaleAuthKey) {
|
|
warnings.push('FEATURE_TAILSCALE=YES but TAILSCALE_AUTHKEY not set (jails will not auto-join)');
|
|
}
|
|
if (localLlmProvider === 'ollama' && !featureOllama) {
|
|
warnings.push('LOCAL_LLM_PROVIDER=ollama but FEATURE_OLLAMA is not enabled');
|
|
}
|
|
if (localLlmProvider === 'llama_cpp' && !featureLlamaCpp) {
|
|
warnings.push('LOCAL_LLM_PROVIDER=llama_cpp but FEATURE_LLAMA_CPP is not enabled');
|
|
}
|
|
if (!sshPublicKey) {
|
|
warnings.push('SSH_PUBLIC_KEY not set (required for Ansible jail SSH baseline)');
|
|
}
|
|
|
|
const values: Record<string, string> = {
|
|
AGENT_NAME: agentName,
|
|
ASSISTANT_NAME: assistantName || '(missing)',
|
|
DISPLAY_LOCALE: displayLocale || '(unset)',
|
|
SYSTEM_LOCALE: systemLocale || '(unset)',
|
|
TZ: timeZone || '(unset)',
|
|
AGENT_DOMAIN: publicDomain,
|
|
AGENT_INTERNAL_DOMAIN: internalDomain,
|
|
AGENT_SUBNET_BASE: subnetBase,
|
|
WARDEN_SUBNET: subnet,
|
|
WARDEN_GATEWAY: gateway,
|
|
DB_RUNTIME: dbRuntime,
|
|
DB_HOST: dbHost || '(unset)',
|
|
WARDEN_DB_IP: dbIp,
|
|
WARDEN_CMS_IP: cmsIp,
|
|
WARDEN_GIT_IP: gitIp,
|
|
WARDEN_OLLAMA_IP: ollamaIp,
|
|
WARDEN_LLAMA_CPP_IP: llamaCppIp,
|
|
PI_TUI_PROFILE: piProfile,
|
|
CODE_HOSTING_MODE: codeHostingMode,
|
|
REMOTE_GIT_URL: remoteGitUrl || '(unset)',
|
|
GIT_MIRROR_URLS: hasGitMirrors ? gitMirrorUrls.join(', ') : '(unset)',
|
|
FEATURE_GIT: featureGit ? 'YES' : 'NO',
|
|
FEATURE_GITEA: featureGitea ? 'YES' : 'NO',
|
|
FEATURE_TAILSCALE: featureTailscale ? 'YES' : 'NO',
|
|
TAILSCALE_AUTHKEY: tailscaleAuthKey ? '(set)' : '(unset)',
|
|
LOCAL_LLM_PROVIDER: localLlmProvider,
|
|
CMS_JAIL_NAME: cmsJailName,
|
|
GIT_JAIL_NAME: gitJailName,
|
|
OLLAMA_JAIL_NAME: ollamaJailName,
|
|
LLAMA_CPP_JAIL_NAME: llamaCppJailName,
|
|
SSH_PUBLIC_KEY: sshPublicKey ? '(set)' : '(unset)',
|
|
};
|
|
|
|
return { envFile, missing, warnings, values };
|
|
}
|
|
|
|
function printAudit(result: EnvAuditResult): void {
|
|
const rel = path.relative(process.cwd(), result.envFile);
|
|
console.log('');
|
|
console.log('△ Environment (.env)');
|
|
console.log(` file: ${rel || result.envFile}`);
|
|
|
|
const lines: Array<[string, string]> = [
|
|
['AGENT_NAME', result.values.AGENT_NAME],
|
|
['ASSISTANT_NAME', result.values.ASSISTANT_NAME],
|
|
['AGENT_DOMAIN', result.values.AGENT_DOMAIN],
|
|
['AGENT_INTERNAL_DOMAIN', result.values.AGENT_INTERNAL_DOMAIN],
|
|
['AGENT_SUBNET_BASE', result.values.AGENT_SUBNET_BASE],
|
|
['WARDEN_GATEWAY', result.values.WARDEN_GATEWAY],
|
|
['DB_RUNTIME', result.values.DB_RUNTIME],
|
|
['DB_HOST', result.values.DB_HOST],
|
|
['WARDEN_DB_IP', result.values.WARDEN_DB_IP],
|
|
['WARDEN_CMS_IP', result.values.WARDEN_CMS_IP],
|
|
['WARDEN_GIT_IP', result.values.WARDEN_GIT_IP],
|
|
['PI_TUI_PROFILE', result.values.PI_TUI_PROFILE],
|
|
['CODE_HOSTING_MODE', result.values.CODE_HOSTING_MODE],
|
|
['REMOTE_GIT_URL', result.values.REMOTE_GIT_URL],
|
|
['GIT_MIRROR_URLS', result.values.GIT_MIRROR_URLS],
|
|
['FEATURE_GIT', result.values.FEATURE_GIT],
|
|
['FEATURE_GITEA', result.values.FEATURE_GITEA],
|
|
['FEATURE_TAILSCALE', result.values.FEATURE_TAILSCALE],
|
|
['TAILSCALE_AUTHKEY', result.values.TAILSCALE_AUTHKEY],
|
|
['LOCAL_LLM_PROVIDER', result.values.LOCAL_LLM_PROVIDER],
|
|
['CMS_JAIL_NAME', result.values.CMS_JAIL_NAME],
|
|
['GIT_JAIL_NAME', result.values.GIT_JAIL_NAME],
|
|
['SSH_PUBLIC_KEY', result.values.SSH_PUBLIC_KEY],
|
|
];
|
|
|
|
for (const [key, value] of lines) {
|
|
console.log(` ${key.padEnd(18)} ${value}`);
|
|
}
|
|
|
|
if (result.missing.length) {
|
|
console.log('\n Missing:');
|
|
result.missing.forEach((k) => console.log(` - ${k}`));
|
|
}
|
|
|
|
if (result.warnings.length) {
|
|
console.log('\n Notes:');
|
|
result.warnings.forEach((w) => console.log(` - ${w}`));
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
export async function run(_args: string[]): Promise<void> {
|
|
const envFile = path.join(process.cwd(), '.env');
|
|
const result = auditEnvFile(envFile);
|
|
|
|
printAudit(result);
|
|
|
|
emitStatus('SETUP_ENV_AUDIT', {
|
|
PLATFORM: getPlatform(),
|
|
ENV_FILE: path.relative(process.cwd(), envFile),
|
|
MISSING_KEYS: result.missing.join(',') || 'none',
|
|
WARNINGS: result.warnings.join(' | ') || 'none',
|
|
STATUS: result.missing.length ? 'warn' : 'success',
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
|
|
if (result.missing.length) {
|
|
process.exitCode = 1;
|
|
}
|
|
}
|