clawdie-ai/setup/env-audit.ts
Operator & Codex 54f612edf2 Fix browser jail registry slot
---
Build: pass | Tests: pass — 2383 passed (175 files)
2026-05-11 14:53:12 +02:00

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