Files
OmniRoute/src/lib/env/runtimeEnv.ts
Diego Rodrigues de Sa e Souza cc3c5b98c0 feat(a2a): inbound delegation to the OmniConductor fleet via POST /api/a2a/tasks (PRD RF5) (#8223)
* feat(a2a): conductor bridge core — event mapping with canceled->cancelled

* feat(a2a): incremental SSE parser for conductor bridge

* feat(a2a): conductor bridge connection loop with persisted cursor and backoff

* feat(a2a): start conductor bridge at boot behind CONDUCTOR_HUB_URL

* fix(a2a): conductor bridge parses the hub's real SSE wire format (id/type in frame, data={ts,payload})

* docs(reference): document CONDUCTOR_HUB_URL/TOKEN in ENVIRONMENT.md (env-doc-sync gate)

* feat(a2a): fleet skills derived from the Conductor hub for the agent card

* feat(a2a): agent card announces Conductor fleet skills

* feat(dashboard): server-side hub proxy for the Conductor panel (whitelisted shapes, fail-open)

* feat(dashboard): /api/conductor proxy routes (fleet, task detail, cancel) behind management auth

* feat(dashboard): Conductor panel — fleet live view, task detail and cancel over /api/conductor proxy

* feat(dashboard): /api/conductor/ask — server-side proxy to Faro (spokesperson) with whitelisted {text,pending}

* chore(env): CONDUCTOR_SPOKESPERSON_URL declared in schema, .env.example and ENVIRONMENT.md

* feat(dashboard): Faro chat with push-to-talk voice on the Conductor panel

* feat(a2a): inbound delegation to the Conductor fleet — POST /api/a2a/tasks translating to the hub

---------

Co-authored-by: backryun <bakryun0718@proton.me>
2026-08-12 02:13:41 -03:00

172 lines
5.3 KiB
TypeScript

import { z } from "zod";
import { validateSecrets } from "@/shared/utils/secretsValidator";
const NODE_ENV_VALUES = ["development", "production", "test"] as const;
const BOOLEAN_ENV_VALUES = ["true", "false"] as const;
type RuntimeEnvIssue = {
name: string;
issue: string;
hint?: string;
};
export type RuntimeEnvValidationResult = {
valid: boolean;
errors: RuntimeEnvIssue[];
warnings: RuntimeEnvIssue[];
data?: WebRuntimeEnv;
};
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed === "" ? undefined : trimmed;
}
const optionalTrimmedString = z.preprocess(normalizeOptionalString, z.string().min(1).optional());
const optionalBooleanEnv = z.preprocess(
normalizeOptionalString,
z.enum(BOOLEAN_ENV_VALUES).optional()
);
const optionalHttpUrl = z.preprocess(
normalizeOptionalString,
z
.string()
.url()
.refine((value) => value.startsWith("http://") || value.startsWith("https://"), {
message: "must start with http:// or https://",
})
.optional()
);
const optionalPortEnv = z.preprocess(
normalizeOptionalString,
z
.string()
.regex(/^\d+$/, "must be an integer between 1 and 65535")
.refine((value) => {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed >= 1 && parsed <= 65535;
}, "must be an integer between 1 and 65535")
.optional()
);
export const webRuntimeEnvSchema = z.object({
NODE_ENV: z.preprocess(normalizeOptionalString, z.enum(NODE_ENV_VALUES).optional()),
DATA_DIR: optionalTrimmedString,
JWT_SECRET: optionalTrimmedString,
API_KEY_SECRET: optionalTrimmedString,
INITIAL_PASSWORD: optionalTrimmedString,
AUTH_COOKIE_SECURE: optionalBooleanEnv,
PRICING_SYNC_ENABLED: optionalBooleanEnv,
OMNIROUTE_DISABLE_BACKGROUND_SERVICES: optionalBooleanEnv,
CLOUD_URL: optionalHttpUrl,
NEXT_PUBLIC_CLOUD_URL: optionalHttpUrl,
OMNIROUTE_PUBLIC_BASE_URL: optionalHttpUrl,
OMNIROUTE_BASE_URL: optionalHttpUrl,
BASE_URL: optionalHttpUrl,
NEXT_PUBLIC_BASE_URL: optionalHttpUrl,
CONDUCTOR_HUB_URL: optionalHttpUrl,
CONDUCTOR_SPOKESPERSON_URL: optionalHttpUrl,
CONDUCTOR_ORCHESTRATOR_TOKEN: optionalTrimmedString,
CONDUCTOR_HUB_TOKEN: optionalTrimmedString,
OMNIROUTE_PORT: optionalPortEnv,
API_PORT: optionalPortEnv,
DASHBOARD_PORT: optionalPortEnv,
});
export type WebRuntimeEnv = z.infer<typeof webRuntimeEnvSchema>;
function formatZodPath(path: Array<string | number>): string {
return path.length > 0 ? String(path[0]) : "env";
}
function getSchemaIssues(error: z.ZodError): RuntimeEnvIssue[] {
return error.issues.map((issue) => ({
name: formatZodPath(issue.path),
issue: `Invalid environment variable "${formatZodPath(issue.path)}": ${issue.message}.`,
}));
}
export function validateWebRuntimeEnv(
env: NodeJS.ProcessEnv = process.env
): RuntimeEnvValidationResult {
const secretValidation = validateSecrets(env);
const schemaValidation = webRuntimeEnvSchema.safeParse(env);
const errors = [...secretValidation.errors];
const warnings = [...secretValidation.warnings];
if (!schemaValidation.success) {
errors.push(...getSchemaIssues(schemaValidation.error));
}
return {
valid: errors.length === 0,
errors,
warnings,
data: schemaValidation.success ? schemaValidation.data : undefined,
};
}
export function formatRuntimeEnvValidationErrors(
errors: RuntimeEnvIssue[],
warnings: RuntimeEnvIssue[] = []
): string {
const lines = ["Invalid web runtime environment configuration:"];
for (const error of errors) {
lines.push(`- ${error.issue}`);
if (error.hint) {
lines.push(` hint: ${error.hint}`);
}
}
for (const warning of warnings) {
lines.push(`- Warning: ${warning.issue}`);
}
return lines.join("\n");
}
export function getWebRuntimeEnv(env: NodeJS.ProcessEnv = process.env): WebRuntimeEnv {
const result = validateWebRuntimeEnv(env);
if (!result.valid || !result.data) {
throw new Error(formatRuntimeEnvValidationErrors(result.errors, result.warnings));
}
return result.data;
}
export function enforceWebRuntimeEnv(
env: NodeJS.ProcessEnv = process.env,
logger: Pick<Console, "error" | "warn"> = console
): void {
const result = validateWebRuntimeEnv(env);
for (const warning of result.warnings) {
logger.warn(`[STARTUP] ${warning.issue}`);
}
if (result.valid) return;
logger.error("");
logger.error("═══════════════════════════════════════════════════");
logger.error(" ❌ STARTUP: Invalid web runtime environment");
logger.error("═══════════════════════════════════════════════════");
for (const error of result.errors) {
logger.error(`${error.issue}`);
if (error.hint) {
logger.error(`${error.hint}`);
}
}
logger.error("");
logger.error(" Fix the environment and restart the server.");
logger.error(" Secrets are intentionally not printed.");
logger.error("═══════════════════════════════════════════════════");
logger.error("");
process.exit(1);
}