mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 19:02:17 +03:00
* 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>
172 lines
5.3 KiB
TypeScript
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);
|
|
}
|