318 lines
12 KiB
TypeScript
318 lines
12 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 { SERVICE_NAME } from '../src/platform-identity.js';
|
|
import {
|
|
normalizeTenantId,
|
|
platformServiceDomain,
|
|
tenantInternalDomain,
|
|
tenantJailName,
|
|
} from '../src/platform-layout.js';
|
|
import { loadTenantRegistry, sharedJailName } from '../src/tenant-registry.js';
|
|
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')
|
|
: '';
|
|
|
|
let registryInternalBase = 'home.arpa';
|
|
let registryPublicBase = '';
|
|
let controlplaneExposure = 'internal';
|
|
let cmsAdminExposure = 'internal';
|
|
let codeAdminExposure = 'internal';
|
|
let publishingMode = 'disabled';
|
|
let reservedHostLabels = 'ai, cms, git, web, www, mail';
|
|
let loadedRegistry: ReturnType<typeof loadTenantRegistry> | null = null;
|
|
|
|
try {
|
|
const registry = loadTenantRegistry();
|
|
loadedRegistry = registry;
|
|
registryInternalBase = registry.platform.internalBase;
|
|
registryPublicBase = registry.platform.publicBase || '';
|
|
controlplaneExposure = registry.platform.controlplaneExposure;
|
|
cmsAdminExposure = registry.platform.cmsAdminExposure;
|
|
codeAdminExposure = registry.platform.codeAdminExposure;
|
|
publishingMode = registry.platform.publishingMode;
|
|
reservedHostLabels = registry.platform.reservedHostLabels.join(', ');
|
|
} catch {
|
|
// Registry may not exist yet during early onboarding.
|
|
}
|
|
|
|
const sharedOrTenantJail = (role: string): string => {
|
|
const shared = loadedRegistry ? sharedJailName(role, loadedRegistry) : null;
|
|
if (!tenantId) return shared || role;
|
|
return shared || tenantJailName(tenantId, role);
|
|
};
|
|
|
|
const rawTenantId = (envValue(content, 'TENANT_ID') || '').trim();
|
|
const tenantId = rawTenantId ? normalizeTenantId(rawTenantId) : '';
|
|
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') ||
|
|
registryPublicBase ||
|
|
registryInternalBase;
|
|
const internalDomain =
|
|
envValue(content, 'AGENT_INTERNAL_DOMAIN') ||
|
|
tenantInternalDomain(tenantId, registryInternalBase);
|
|
|
|
const subnetBase =
|
|
envValue(content, 'AGENT_SUBNET_BASE') ||
|
|
envValue(content, 'WARDEN_SUBNET_BASE') ||
|
|
'10.0.1';
|
|
const gateway = envValue(content, 'WARDEN_GATEWAY') || `${subnetBase}.1`;
|
|
const subnet = envValue(content, 'WARDEN_SUBNET') || `${subnetBase}.0/24`;
|
|
const dbRuntime = (envValue(content, 'DB_RUNTIME') || 'host').toLowerCase();
|
|
const dbHost =
|
|
envValue(content, 'DB_HOST') ||
|
|
(dbRuntime === 'host' ? `${subnetBase}.1` : '');
|
|
const dbIp = envValue(content, 'WARDEN_DB_IP') || `${subnetBase}.5`;
|
|
const cmsIp =
|
|
envValue(content, 'WARDEN_CMS_IP') ||
|
|
envValue(content, 'CMS_JAIL_IP') ||
|
|
`${subnetBase}.3`;
|
|
const gitIp = envValue(content, 'WARDEN_GIT_IP') || `${subnetBase}.2`;
|
|
const browserIp = envValue(content, 'WARDEN_BROWSER_IP') || `${subnetBase}.6`;
|
|
const ollamaIp = envValue(content, 'WARDEN_OLLAMA_IP') || `${subnetBase}.4`;
|
|
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') || sharedOrTenantJail('cms');
|
|
const gitJailName =
|
|
envValue(content, 'GIT_JAIL_NAME') || sharedOrTenantJail('git');
|
|
const ollamaJailName =
|
|
envValue(content, 'OLLAMA_JAIL_NAME') || sharedOrTenantJail('ollama');
|
|
const llamaCppJailName =
|
|
envValue(content, 'LLAMA_CPP_JAIL_NAME') || sharedOrTenantJail('llama-cpp');
|
|
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> = {
|
|
SERVICE_NAME,
|
|
TENANT_ID: tenantId || '(shared-platform)',
|
|
ASSISTANT_NAME: assistantName || '(missing)',
|
|
DISPLAY_LOCALE: displayLocale || '(unset)',
|
|
SYSTEM_LOCALE: systemLocale || '(unset)',
|
|
TZ: timeZone || '(unset)',
|
|
PLATFORM_INTERNAL_BASE: registryInternalBase,
|
|
PLATFORM_PUBLIC_BASE: registryPublicBase || '(disabled)',
|
|
CONTROLPLANE_HOST: platformServiceDomain('ai', registryInternalBase),
|
|
CONTROLPLANE_EXPOSURE: controlplaneExposure,
|
|
CMS_ADMIN_EXPOSURE: cmsAdminExposure,
|
|
CODE_ADMIN_EXPOSURE: codeAdminExposure,
|
|
PUBLISHING_MODE: publishingMode,
|
|
RESERVED_HOST_LABELS: reservedHostLabels,
|
|
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_BROWSER_IP: browserIp,
|
|
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]> = [
|
|
['SERVICE_NAME', result.values.SERVICE_NAME],
|
|
['TENANT_ID', result.values.TENANT_ID],
|
|
['ASSISTANT_NAME', result.values.ASSISTANT_NAME],
|
|
['PLATFORM_INTERNAL_BASE', result.values.PLATFORM_INTERNAL_BASE],
|
|
['PLATFORM_PUBLIC_BASE', result.values.PLATFORM_PUBLIC_BASE],
|
|
['CONTROLPLANE_HOST', result.values.CONTROLPLANE_HOST],
|
|
['CONTROLPLANE_EXPOSURE', result.values.CONTROLPLANE_EXPOSURE],
|
|
['CMS_ADMIN_EXPOSURE', result.values.CMS_ADMIN_EXPOSURE],
|
|
['CODE_ADMIN_EXPOSURE', result.values.CODE_ADMIN_EXPOSURE],
|
|
['PUBLISHING_MODE', result.values.PUBLISHING_MODE],
|
|
['RESERVED_HOST_LABELS', result.values.RESERVED_HOST_LABELS],
|
|
['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;
|
|
}
|
|
}
|