clawdie-ai/setup/env-audit.ts
Clawdie AI 0056d49e62 Add host Postgres runtime option
---
Build: pass | Tests: FAIL — Tests  10 failed | 928 passed (938)
2026-04-12 12:07:08 +00:00

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;
}
}