Merge branch 'plan-paseo-config-schema'

This commit is contained in:
Mohamed Boudra
2026-02-04 10:18:58 +07:00
20 changed files with 880 additions and 198 deletions
@@ -127,14 +127,6 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
detectionGracePeriod: 200, detectionGracePeriod: 200,
}); });
// Update voice detection flags whenever they change
useEffect(() => {
activeSession?.methods?.setVoiceDetectionFlags(
realtimeAudio.isDetecting,
realtimeAudio.isSpeaking
);
}, [activeSession?.methods, realtimeAudio.isDetecting, realtimeAudio.isSpeaking]);
useEffect(() => { useEffect(() => {
realtimeSessionRef.current = activeSession; realtimeSessionRef.current = activeSession;
}, [activeSession]); }, [activeSession]);
@@ -196,7 +188,6 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
const session = realtimeSessionRef.current; const session = realtimeSessionRef.current;
session?.audioPlayer?.stop(); session?.audioPlayer?.stop();
await realtimeAudio.stop(); await realtimeAudio.stop();
session?.methods?.setVoiceDetectionFlags(false, false);
setIsVoiceMode(false); setIsVoiceMode(false);
setActiveServerId(null); setActiveServerId(null);
console.log("[Voice] Mode disabled"); console.log("[Voice] Mode disabled");
+35 -10
View File
@@ -7,12 +7,14 @@ import {
createRootLogger, createRootLogger,
loadPersistedConfig, loadPersistedConfig,
} from '@paseo/server' } from '@paseo/server'
import type { CliConfigOverrides } from '@paseo/server'
interface StartOptions { interface StartOptions {
port?: string port?: string
home?: string home?: string
foreground?: boolean foreground?: boolean
noRelay?: boolean noRelay?: boolean
noMcp?: boolean
allowedHosts?: string allowedHosts?: string
} }
@@ -23,7 +25,11 @@ export function startCommand(): Command {
.option('--home <path>', 'Paseo home directory (default: ~/.paseo)') .option('--home <path>', 'Paseo home directory (default: ~/.paseo)')
.option('--foreground', 'Run in foreground (don\'t daemonize)') .option('--foreground', 'Run in foreground (don\'t daemonize)')
.option('--no-relay', 'Disable relay connection') .option('--no-relay', 'Disable relay connection')
.option('--allowed-hosts <hosts>', 'Comma-separated list of allowed MCP hosts (e.g., "localhost:6767,127.0.0.1:6767")') .option('--no-mcp', 'Disable the Agent MCP HTTP endpoint')
.option(
'--allowed-hosts <hosts>',
'Comma-separated list of allowed Host header values (Vite-style; e.g., "localhost,.example.com" or "true")'
)
.action(async (options: StartOptions) => { .action(async (options: StartOptions) => {
await runStart(options) await runStart(options)
}) })
@@ -34,22 +40,41 @@ async function runStart(options: StartOptions): Promise<void> {
if (options.home) { if (options.home) {
process.env.PASEO_HOME = options.home process.env.PASEO_HOME = options.home
} }
let paseoHome: string
let logger: ReturnType<typeof createRootLogger>
let config: ReturnType<typeof loadConfig>
const cliOverrides: CliConfigOverrides = {}
if (options.port) { if (options.port) {
process.env.PASEO_LISTEN = `127.0.0.1:${options.port}` cliOverrides.listen = `127.0.0.1:${options.port}`
} }
const paseoHome = resolvePaseoHome()
const persistedConfig = loadPersistedConfig(paseoHome)
const logger = createRootLogger(persistedConfig)
const config = loadConfig(paseoHome)
// Apply CLI overrides
if (options.noRelay) { if (options.noRelay) {
config.relayEnabled = false cliOverrides.relayEnabled = false
} }
if (options.allowedHosts) { if (options.allowedHosts) {
config.agentMcpAllowedHosts = options.allowedHosts.split(',').map(h => h.trim()) const raw = options.allowedHosts.trim()
cliOverrides.allowedHosts =
raw.toLowerCase() === 'true'
? true
: raw.split(',').map(h => h.trim()).filter(Boolean)
}
if (options.noMcp) {
cliOverrides.mcpEnabled = false
}
try {
paseoHome = resolvePaseoHome()
const persistedConfig = loadPersistedConfig(paseoHome)
logger = createRootLogger(persistedConfig)
config = loadConfig(paseoHome, { cli: cliOverrides })
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
console.error(chalk.red(message))
process.exit(1)
} }
// For now, only foreground mode is supported // For now, only foreground mode is supported
+1
View File
@@ -13,6 +13,7 @@
"build": "tsc -p tsconfig.server.json", "build": "tsc -p tsconfig.server.json",
"start": "NODE_ENV=production node dist/server/index.js", "start": "NODE_ENV=production node dist/server/index.js",
"typecheck": "tsc -p tsconfig.server.typecheck.json --noEmit", "typecheck": "tsc -p tsconfig.server.typecheck.json --noEmit",
"generate:config-schema": "tsx scripts/generate-config-schema.ts",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
@@ -0,0 +1,27 @@
import { fileURLToPath } from "node:url";
import path from "node:path";
import fs from "node:fs";
import { zodToJsonSchema } from "zod-to-json-schema";
import { PersistedConfigSchema } from "../src/server/persisted-config.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function main() {
const repoRoot = path.resolve(__dirname, "../../..");
const outPath = path.join(
repoRoot,
"packages/website/public/schemas/paseo.config.v1.json"
);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
const schema = zodToJsonSchema(PersistedConfigSchema, {
name: "PaseoConfigV1",
});
fs.writeFileSync(outPath, JSON.stringify(schema, null, 2) + "\n", "utf8");
process.stdout.write(`Wrote ${outPath}\n`);
}
main();
@@ -84,12 +84,13 @@ describe("agent MCP end-to-end (offline)", () => {
listen: `127.0.0.1:${port}`, listen: `127.0.0.1:${port}`,
paseoHome, paseoHome,
corsAllowedOrigins: [], corsAllowedOrigins: [],
agentMcpRoute: "/mcp/agents", allowedHosts: true,
agentMcpAllowedHosts: [`127.0.0.1:${port}`, `localhost:${port}`], mcpEnabled: true,
staticDir, staticDir,
mcpDebug: false, mcpDebug: false,
agentClients: createTestAgentClients(), agentClients: createTestAgentClients(),
agentStoragePath: path.join(paseoHome, "agents"), agentStoragePath: path.join(paseoHome, "agents"),
openrouterApiKey: null,
}; };
const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" })); const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" }));
@@ -0,0 +1,46 @@
import { describe, it, expect } from "vitest";
import { isHostAllowed, mergeAllowedHosts, parseAllowedHostsEnv } from "./allowed-hosts.js";
describe("allowed hosts (vite-style)", () => {
it("allows localhost by default", () => {
expect(isHostAllowed("localhost:6767", undefined)).toBe(true);
});
it("allows subdomains of .localhost by default", () => {
expect(isHostAllowed("foo.localhost:6767", undefined)).toBe(true);
});
it("allows IP addresses by default", () => {
expect(isHostAllowed("127.0.0.1:6767", undefined)).toBe(true);
expect(isHostAllowed("[::1]:6767", undefined)).toBe(true);
});
it("rejects non-default hosts when no allowlist is provided", () => {
expect(isHostAllowed("evil.com:6767", undefined)).toBe(false);
});
it("allows any host when set to true", () => {
expect(isHostAllowed("evil.com:6767", true)).toBe(true);
});
it("supports leading-dot patterns", () => {
const allowed = [".example.com"];
expect(isHostAllowed("example.com:6767", allowed)).toBe(true);
expect(isHostAllowed("foo.example.com:6767", allowed)).toBe(true);
expect(isHostAllowed("foo.bar.example.com:6767", allowed)).toBe(true);
expect(isHostAllowed("notexample.com:6767", allowed)).toBe(false);
});
it("merges arrays (append + de-dupe) and short-circuits on true", () => {
expect(mergeAllowedHosts([["a"], ["a", "b"]])).toEqual(["a", "b"]);
expect(mergeAllowedHosts([["a"], true, ["b"]])).toBe(true);
});
it("parses env var values", () => {
expect(parseAllowedHostsEnv(undefined)).toBeUndefined();
expect(parseAllowedHostsEnv("")).toBeUndefined();
expect(parseAllowedHostsEnv("true")).toBe(true);
expect(parseAllowedHostsEnv("localhost,.example.com")).toEqual(["localhost", ".example.com"]);
});
});
+104
View File
@@ -0,0 +1,104 @@
import net from "node:net";
export type AllowedHostsConfig = true | string[] | undefined;
function normalizeHostname(hostname: string): string {
return hostname.trim().toLowerCase();
}
function parseHostnameFromHostHeader(hostHeader: string): string | null {
const trimmed = hostHeader.trim();
if (!trimmed) return null;
// IPv6 in brackets: [::1]:6767
if (trimmed.startsWith("[")) {
const end = trimmed.indexOf("]");
if (end === -1) return null;
return normalizeHostname(trimmed.slice(1, end));
}
// IPv4/hostname with optional port: localhost:6767
const colonIndex = trimmed.indexOf(":");
if (colonIndex === -1) {
return normalizeHostname(trimmed);
}
return normalizeHostname(trimmed.slice(0, colonIndex));
}
function matchesAllowedHostPattern(hostname: string, pattern: string): boolean {
const normalizedPattern = normalizeHostname(pattern);
if (!normalizedPattern) return false;
if (normalizedPattern.startsWith(".")) {
const base = normalizedPattern.slice(1);
if (!base) return false;
return hostname === base || hostname.endsWith(`.${base}`);
}
return hostname === normalizedPattern;
}
function isDefaultAllowedHostname(hostname: string): boolean {
// Vite-style defaults: localhost, *.localhost, and all IP addresses.
if (hostname === "localhost") return true;
if (hostname.endsWith(".localhost")) return true;
if (net.isIP(hostname) !== 0) return true;
return false;
}
/**
* Vite-style allowed hosts check, adapted to raw Host headers.
*
* Semantics:
* - `allowedHosts === true` => allow any host.
* - `allowedHosts === []` or `undefined` => allow localhost, *.localhost, and all IPs.
* - `allowedHosts === ['.example.com', 'myhost']` => allow those *in addition* to defaults.
*/
export function isHostAllowed(
hostHeader: string | undefined,
allowedHosts: AllowedHostsConfig
): boolean {
const hostname = hostHeader ? parseHostnameFromHostHeader(hostHeader) : null;
if (!hostname) return false;
if (allowedHosts === true) return true;
// Defaults are always allowed.
if (isDefaultAllowedHostname(hostname)) return true;
const patterns = allowedHosts ?? [];
for (const pattern of patterns) {
if (matchesAllowedHostPattern(hostname, pattern)) return true;
}
return false;
}
export function mergeAllowedHosts(
values: Array<AllowedHostsConfig>
): AllowedHostsConfig {
let merged: string[] = [];
for (const value of values) {
if (value === true) return true;
if (!value) continue;
merged = merged.concat(value);
}
const deduped = Array.from(
new Set(merged.map((v) => v.trim()).filter((v) => v.length > 0))
);
return deduped;
}
export function parseAllowedHostsEnv(
raw: string | undefined
): AllowedHostsConfig {
if (!raw) return undefined;
const trimmed = raw.trim();
if (!trimmed) return undefined;
if (trimmed.toLowerCase() === "true") return true;
return trimmed
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
+131 -106
View File
@@ -60,6 +60,7 @@ import type {
AgentProvider, AgentProvider,
} from "./agent/agent-sdk-types.js"; } from "./agent/agent-sdk-types.js";
import { acquirePidLock, releasePidLock } from "./pid-lock.js"; import { acquirePidLock, releasePidLock } from "./pid-lock.js";
import { isHostAllowed, type AllowedHostsConfig } from "./allowed-hosts.js";
type AgentMcpTransportMap = Map<string, StreamableHTTPServerTransport>; type AgentMcpTransportMap = Map<string, StreamableHTTPServerTransport>;
@@ -73,8 +74,8 @@ export type PaseoDaemonConfig = {
listen: string; listen: string;
paseoHome: string; paseoHome: string;
corsAllowedOrigins: string[]; corsAllowedOrigins: string[];
agentMcpRoute: string; allowedHosts?: AllowedHostsConfig;
agentMcpAllowedHosts: string[]; mcpEnabled?: boolean;
staticDir: string; staticDir: string;
mcpDebug: boolean; mcpDebug: boolean;
agentClients: Partial<Record<AgentProvider, AgentClient>>; agentClients: Partial<Record<AgentProvider, AgentClient>>;
@@ -83,6 +84,8 @@ export type PaseoDaemonConfig = {
relayEndpoint?: string; relayEndpoint?: string;
appBaseUrl?: string; appBaseUrl?: string;
openai?: PaseoOpenAIConfig; openai?: PaseoOpenAIConfig;
openrouterApiKey?: string | null;
voiceLlmModel?: string | null;
dictationFinalTimeoutMs?: number; dictationFinalTimeoutMs?: number;
downloadTokenTtlMs?: number; downloadTokenTtlMs?: number;
}; };
@@ -104,7 +107,6 @@ export async function createPaseoDaemon(
const connectionSessionId = randomUUID(); const connectionSessionId = randomUUID();
let relayTransport: RelayTransportController | null = null; let relayTransport: RelayTransportController | null = null;
const agentMcpRoute = config.agentMcpRoute;
const staticDir = config.staticDir; const staticDir = config.staticDir;
const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000; const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
@@ -114,6 +116,19 @@ export async function createPaseoDaemon(
const app = express(); const app = express();
// Host allowlist / DNS rebinding protection (vite-like semantics).
// For non-TCP (unix sockets), skip host validation.
if (listenTarget.type === "tcp") {
app.use((req, res, next) => {
const hostHeader = typeof req.headers.host === "string" ? req.headers.host : undefined;
if (!isHostAllowed(hostHeader, config.allowedHosts)) {
res.status(403).json({ error: "Invalid Host header" });
return;
}
next();
});
}
// CORS - allow same-origin + configured origins // CORS - allow same-origin + configured origins
const allowedOrigins = new Set([ const allowedOrigins = new Set([
...config.corsAllowedOrigins, ...config.corsAllowedOrigins,
@@ -227,45 +242,6 @@ export async function createPaseoDaemon(
`Agent registry loaded (${persistedRecords.length} record${persistedRecords.length === 1 ? "" : "s"}); agents will initialize on demand` `Agent registry loaded (${persistedRecords.length} record${persistedRecords.length === 1 ? "" : "s"}); agents will initialize on demand`
); );
const agentMcpTransports: AgentMcpTransportMap = new Map();
const allowedHosts = config.agentMcpAllowedHosts;
const createAgentMcpTransport = async (callerAgentId?: string) => {
const agentMcpServer = await createAgentMcpServer({
agentManager,
agentStorage,
paseoHome: config.paseoHome,
callerAgentId,
logger,
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
agentMcpTransports.set(sessionId, transport);
logger.debug({ sessionId }, "Agent MCP session initialized");
},
onsessionclosed: (sessionId) => {
agentMcpTransports.delete(sessionId);
logger.debug({ sessionId }, "Agent MCP session closed");
},
enableDnsRebindingProtection: true,
allowedHosts,
});
transport.onclose = () => {
if (transport.sessionId) {
agentMcpTransports.delete(transport.sessionId);
}
};
transport.onerror = (err) => {
logger.error({ err }, "Agent MCP transport error");
};
await agentMcpServer.connect(transport);
return transport;
};
// Create in-memory transport for Session's Agent MCP client (voice assistant tools) // Create in-memory transport for Session's Agent MCP client (voice assistant tools)
const createInMemoryAgentMcpTransport = async (): Promise<InMemoryTransport> => { const createInMemoryAgentMcpTransport = async (): Promise<InMemoryTransport> => {
const agentMcpServer = await createAgentMcpServer({ const agentMcpServer = await createAgentMcpServer({
@@ -282,76 +258,121 @@ export async function createPaseoDaemon(
return clientTransport; return clientTransport;
}; };
const handleAgentMcpRequest: express.RequestHandler = async (req, res) => { const mcpEnabled = config.mcpEnabled ?? true;
if (config.mcpDebug) { if (mcpEnabled) {
logger.debug( const agentMcpRoute = "/mcp/agents";
{ const agentMcpTransports: AgentMcpTransportMap = new Map();
method: req.method,
url: req.originalUrl, const createAgentMcpTransport = async (callerAgentId?: string) => {
sessionId: req.header("mcp-session-id"), const agentMcpServer = await createAgentMcpServer({
authorization: req.header("authorization"), agentManager,
body: req.body, agentStorage,
paseoHome: config.paseoHome,
callerAgentId,
logger,
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
agentMcpTransports.set(sessionId, transport);
logger.debug({ sessionId }, "Agent MCP session initialized");
}, },
"Agent MCP request" onsessionclosed: (sessionId) => {
); agentMcpTransports.delete(sessionId);
} logger.debug({ sessionId }, "Agent MCP session closed");
try { },
const sessionId = req.header("mcp-session-id"); // NOTE: We enforce a Vite-like host allowlist at the app/websocket layer.
let transport = sessionId ? agentMcpTransports.get(sessionId) : undefined; // StreamableHTTPServerTransport's built-in check requires exact Host header matches.
enableDnsRebindingProtection: false,
});
if (!transport) { transport.onclose = () => {
if (req.method !== "POST") { if (transport.sessionId) {
res.status(400).json({ agentMcpTransports.delete(transport.sessionId);
jsonrpc: "2.0",
error: {
code: -32000,
message: "Missing or invalid MCP session",
},
id: null,
});
return;
} }
if (!isInitializeRequest(req.body)) { };
res.status(400).json({ transport.onerror = (err) => {
jsonrpc: "2.0", logger.error({ err }, "Agent MCP transport error");
error: { };
code: -32000,
message: "Initialization request expected",
},
id: null,
});
return;
}
const callerAgentIdRaw = req.query.callerAgentId;
const callerAgentId =
typeof callerAgentIdRaw === "string"
? callerAgentIdRaw
: Array.isArray(callerAgentIdRaw) && typeof callerAgentIdRaw[0] === "string"
? callerAgentIdRaw[0]
: undefined;
transport = await createAgentMcpTransport(callerAgentId);
}
await transport.handleRequest(req as any, res as any, req.body); await agentMcpServer.connect(transport);
} catch (err) { return transport;
logger.error({ err }, "Failed to handle Agent MCP request"); };
if (!res.headersSent) {
res.status(500).json({ const handleAgentMcpRequest: express.RequestHandler = async (req, res) => {
jsonrpc: "2.0", if (config.mcpDebug) {
error: { logger.debug(
code: -32603, {
message: "Internal MCP server error", method: req.method,
url: req.originalUrl,
sessionId: req.header("mcp-session-id"),
authorization: req.header("authorization"),
body: req.body,
}, },
id: null, "Agent MCP request"
}); );
} }
} try {
}; const sessionId = req.header("mcp-session-id");
let transport = sessionId ? agentMcpTransports.get(sessionId) : undefined;
app.post(agentMcpRoute, handleAgentMcpRequest); if (!transport) {
app.get(agentMcpRoute, handleAgentMcpRequest); if (req.method !== "POST") {
app.delete(agentMcpRoute, handleAgentMcpRequest); res.status(400).json({
logger.info({ route: agentMcpRoute }, "Agent MCP server mounted on main app"); jsonrpc: "2.0",
error: {
code: -32000,
message: "Missing or invalid MCP session",
},
id: null,
});
return;
}
if (!isInitializeRequest(req.body)) {
res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Initialization request expected",
},
id: null,
});
return;
}
const callerAgentIdRaw = req.query.callerAgentId;
const callerAgentId =
typeof callerAgentIdRaw === "string"
? callerAgentIdRaw
: Array.isArray(callerAgentIdRaw) && typeof callerAgentIdRaw[0] === "string"
? callerAgentIdRaw[0]
: undefined;
transport = await createAgentMcpTransport(callerAgentId);
}
await transport.handleRequest(req as any, res as any, req.body);
} catch (err) {
logger.error({ err }, "Failed to handle Agent MCP request");
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal MCP server error",
},
id: null,
});
}
}
};
app.post(agentMcpRoute, handleAgentMcpRequest);
app.get(agentMcpRoute, handleAgentMcpRequest);
app.delete(agentMcpRoute, handleAgentMcpRequest);
logger.info({ route: agentMcpRoute }, "Agent MCP server mounted on main app");
} else {
logger.info("Agent MCP HTTP endpoint disabled");
}
let sttService: OpenAISTT | null = null; let sttService: OpenAISTT | null = null;
@@ -400,9 +421,13 @@ export async function createPaseoDaemon(
downloadTokenStore, downloadTokenStore,
config.paseoHome, config.paseoHome,
createInMemoryAgentMcpTransport, createInMemoryAgentMcpTransport,
{ allowedOrigins }, { allowedOrigins, allowedHosts: config.allowedHosts },
{ stt: sttService, tts: ttsService }, { stt: sttService, tts: ttsService },
terminalManager, terminalManager,
{
openrouterApiKey: config.openrouterApiKey ?? null,
voiceLlmModel: config.voiceLlmModel ?? null,
},
{ {
openaiApiKey: config.openai?.apiKey ?? null, openaiApiKey: config.openai?.apiKey ?? null,
finalTimeoutMs: config.dictationFinalTimeoutMs, finalTimeoutMs: config.dictationFinalTimeoutMs,
+90 -34
View File
@@ -4,9 +4,13 @@ import type { PaseoDaemonConfig } from "./bootstrap.js";
import type { STTConfig } from "./agent/stt-openai.js"; import type { STTConfig } from "./agent/stt-openai.js";
import type { TTSConfig } from "./agent/tts-openai.js"; import type { TTSConfig } from "./agent/tts-openai.js";
import { loadPersistedConfig } from "./persisted-config.js"; import { loadPersistedConfig } from "./persisted-config.js";
import {
mergeAllowedHosts,
parseAllowedHostsEnv,
type AllowedHostsConfig,
} from "./allowed-hosts.js";
const DEFAULT_PORT = 6767; const DEFAULT_PORT = 6767;
const DEFAULT_AGENT_MCP_ROUTE = "/mcp/agents";
const DEFAULT_RELAY_ENDPOINT = "relay.paseo.sh:443"; const DEFAULT_RELAY_ENDPOINT = "relay.paseo.sh:443";
const DEFAULT_APP_BASE_URL = "https://app.paseo.sh"; const DEFAULT_APP_BASE_URL = "https://app.paseo.sh";
@@ -15,15 +19,35 @@ function getDefaultListen(): string {
return `127.0.0.1:${DEFAULT_PORT}`; return `127.0.0.1:${DEFAULT_PORT}`;
} }
export type CliConfigOverrides = Partial<{
listen: string;
relayEnabled: boolean;
mcpEnabled: boolean;
allowedHosts: AllowedHostsConfig;
}>;
function parseOpenAIConfig(env: NodeJS.ProcessEnv) { function parseOpenAIConfig(
const apiKey = env.OPENAI_API_KEY; env: NodeJS.ProcessEnv,
configApiKey: string | undefined,
config: {
dictationSttModel?: string;
dictationSttConfidenceThreshold?: number;
voiceSttModel?: string;
voiceTtsVoice?: TTSConfig["voice"];
voiceTtsModel?: TTSConfig["model"];
}
) {
const apiKey = env.OPENAI_API_KEY ?? configApiKey;
if (!apiKey) return undefined; if (!apiKey) return undefined;
const sttConfidenceThreshold = env.STT_CONFIDENCE_THRESHOLD const sttConfidenceThreshold = env.STT_CONFIDENCE_THRESHOLD
? parseFloat(env.STT_CONFIDENCE_THRESHOLD) ? parseFloat(env.STT_CONFIDENCE_THRESHOLD)
: undefined; : config.dictationSttConfidenceThreshold;
const sttModel = env.STT_MODEL as STTConfig["model"]; const sttModel = (
env.STT_MODEL ??
config.voiceSttModel ??
config.dictationSttModel
) as STTConfig["model"] | undefined;
const ttsVoice = (env.TTS_VOICE || "alloy") as const ttsVoice = (env.TTS_VOICE || "alloy") as
| "alloy" | "alloy"
| "echo" | "echo"
@@ -31,7 +55,10 @@ function parseOpenAIConfig(env: NodeJS.ProcessEnv) {
| "onyx" | "onyx"
| "nova" | "nova"
| "shimmer"; | "shimmer";
const ttsModel = (env.TTS_MODEL || "tts-1") as "tts-1" | "tts-1-hd"; const ttsModel = (env.TTS_MODEL || config.voiceTtsModel || "tts-1") as
| "tts-1"
| "tts-1-hd";
const configuredVoice = config.voiceTtsVoice;
return { return {
apiKey, apiKey,
@@ -42,34 +69,21 @@ function parseOpenAIConfig(env: NodeJS.ProcessEnv) {
}, },
tts: { tts: {
apiKey, apiKey,
voice: ttsVoice, voice: configuredVoice ?? ttsVoice,
model: ttsModel, model: ttsModel,
responseFormat: "pcm" as TTSConfig["responseFormat"], responseFormat: "pcm" as TTSConfig["responseFormat"],
}, },
}; };
} }
function getListenForMcp(listen: string): string {
// For Unix sockets, MCP still needs HTTP - use localhost
if (listen.startsWith("/") || listen.startsWith("~") || listen.includes(".sock")) {
return "127.0.0.1:6767";
}
if (listen.startsWith("unix://")) {
return "127.0.0.1:6767";
}
// TCP: extract host:port
if (listen.includes(":")) {
const [host, port] = listen.split(":");
return `${host || "127.0.0.1"}:${port}`;
}
// Just port
return `127.0.0.1:${listen}`;
}
export function loadConfig( export function loadConfig(
paseoHome: string, paseoHome: string,
env: NodeJS.ProcessEnv = process.env options?: {
env?: NodeJS.ProcessEnv;
cli?: CliConfigOverrides;
}
): PaseoDaemonConfig { ): PaseoDaemonConfig {
const env = options?.env ?? process.env;
const persisted = loadPersistedConfig(paseoHome); const persisted = loadPersistedConfig(paseoHome);
// PASEO_LISTEN can be: // PASEO_LISTEN can be:
@@ -77,26 +91,68 @@ export function loadConfig(
// - /path/to/socket (Unix socket) // - /path/to/socket (Unix socket)
// - unix:///path/to/socket (Unix socket) // - unix:///path/to/socket (Unix socket)
// Default is TCP at 127.0.0.1:6767 // Default is TCP at 127.0.0.1:6767
const listen = env.PASEO_LISTEN ?? persisted.listen ?? getDefaultListen(); const listen =
const mcpListen = getListenForMcp(listen); options?.cli?.listen ??
env.PASEO_LISTEN ??
persisted.daemon?.listen ??
getDefaultListen();
const envCorsOrigins = env.PASEO_CORS_ORIGINS const envCorsOrigins = env.PASEO_CORS_ORIGINS
? env.PASEO_CORS_ORIGINS.split(",").map((s) => s.trim()) ? env.PASEO_CORS_ORIGINS.split(",").map((s) => s.trim())
: []; : [];
const persistedCorsOrigins = persisted.daemon?.cors?.allowedOrigins ?? [];
const allowedHosts = mergeAllowedHosts([
persisted.daemon?.allowedHosts,
parseAllowedHostsEnv(env.PASEO_ALLOWED_HOSTS),
options?.cli?.allowedHosts,
]);
const mcpEnabled =
options?.cli?.mcpEnabled ?? persisted.daemon?.mcp?.enabled ?? true;
const relayEnabled =
options?.cli?.relayEnabled ?? persisted.daemon?.relay?.enabled ?? true;
const relayEndpoint =
env.PASEO_RELAY_ENDPOINT ??
persisted.daemon?.relay?.endpoint ??
DEFAULT_RELAY_ENDPOINT;
const appBaseUrl =
env.PASEO_APP_BASE_URL ?? persisted.app?.baseUrl ?? DEFAULT_APP_BASE_URL;
const openai = parseOpenAIConfig(env, persisted.providers?.openai?.apiKey, {
dictationSttModel: persisted.features?.dictation?.stt?.model,
dictationSttConfidenceThreshold:
persisted.features?.dictation?.stt?.confidenceThreshold,
voiceSttModel: persisted.features?.voiceMode?.stt?.model,
voiceTtsModel: persisted.features?.voiceMode?.tts?.model,
voiceTtsVoice: persisted.features?.voiceMode?.tts?.voice,
});
const openrouterApiKey =
env.OPENROUTER_API_KEY ?? persisted.providers?.openrouter?.apiKey ?? null;
const voiceLlmModel = persisted.features?.voiceMode?.llm?.model ?? null;
return { return {
listen, listen,
paseoHome, paseoHome,
corsAllowedOrigins: [...persisted.cors.allowedOrigins, ...envCorsOrigins], corsAllowedOrigins: Array.from(
agentMcpRoute: DEFAULT_AGENT_MCP_ROUTE, new Set([...persistedCorsOrigins, ...envCorsOrigins].filter((s) => s.length > 0))
agentMcpAllowedHosts: [mcpListen, `localhost:${mcpListen.split(":")[1]}`], ),
allowedHosts,
mcpEnabled,
mcpDebug: env.MCP_DEBUG === "1", mcpDebug: env.MCP_DEBUG === "1",
agentStoragePath: path.join(paseoHome, "agents"), agentStoragePath: path.join(paseoHome, "agents"),
staticDir: "public", staticDir: "public",
agentClients: {}, agentClients: {},
relayEnabled: true, relayEnabled,
relayEndpoint: env.PASEO_RELAY_ENDPOINT ?? DEFAULT_RELAY_ENDPOINT, relayEndpoint,
appBaseUrl: env.PASEO_APP_BASE_URL ?? DEFAULT_APP_BASE_URL, appBaseUrl,
openai: parseOpenAIConfig(env), openai,
openrouterApiKey,
voiceLlmModel,
}; };
} }
+1 -1
View File
@@ -1,6 +1,6 @@
// CLI exports for @paseo/server // CLI exports for @paseo/server
export { createPaseoDaemon, type PaseoDaemon, type PaseoDaemonConfig } from "./bootstrap.js"; export { createPaseoDaemon, type PaseoDaemon, type PaseoDaemonConfig } from "./bootstrap.js";
export { loadConfig } from "./config.js"; export { loadConfig, type CliConfigOverrides } from "./config.js";
export { resolvePaseoHome } from "./paseo-home.js"; export { resolvePaseoHome } from "./paseo-home.js";
export { createRootLogger, type LogLevel, type LogFormat } from "./logger.js"; export { createRootLogger, type LogLevel, type LogFormat } from "./logger.js";
export { loadPersistedConfig, type PersistedConfig } from "./persisted-config.js"; export { loadPersistedConfig, type PersistedConfig } from "./persisted-config.js";
+22 -9
View File
@@ -12,14 +12,27 @@ import { loadPersistedConfig } from "./persisted-config.js";
import { PidLockError } from "./pid-lock.js"; import { PidLockError } from "./pid-lock.js";
async function main() { async function main() {
const paseoHome = resolvePaseoHome(); let paseoHome: string;
const persistedConfig = loadPersistedConfig(paseoHome); let logger: ReturnType<typeof createRootLogger>;
const logger = createRootLogger(persistedConfig); let config: ReturnType<typeof loadConfig>;
const config = loadConfig(paseoHome);
try {
paseoHome = resolvePaseoHome();
const persistedConfig = loadPersistedConfig(paseoHome);
logger = createRootLogger(persistedConfig);
config = loadConfig(paseoHome);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
process.stderr.write(`${message}\n`);
process.exit(1);
}
if (process.argv.includes("--no-relay")) { if (process.argv.includes("--no-relay")) {
config.relayEnabled = false; config.relayEnabled = false;
} }
if (process.argv.includes("--no-mcp")) {
config.mcpEnabled = false;
}
const daemon = await createPaseoDaemon(config, logger); const daemon = await createPaseoDaemon(config, logger);
try { try {
@@ -63,10 +76,10 @@ async function main() {
} }
main().catch((err) => { main().catch((err) => {
// Logger might not be initialized yet, so we need to handle this specially if (process.env.PASEO_DEBUG === "1") {
const paseoHome = resolvePaseoHome(); process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
const persistedConfig = loadPersistedConfig(paseoHome); } else {
const logger = createRootLogger(persistedConfig); process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
logger.error({ err }, "Failed to start server"); }
process.exit(1); process.exit(1);
}); });
+1 -1
View File
@@ -13,7 +13,7 @@ function expandHomeDir(input: string): string {
} }
export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env): string { export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env): string {
const raw = env.PASEO_HOME ?? env.PASEO_HOME_DIR ?? "~/.paseo"; const raw = env.PASEO_HOME ?? "~/.paseo";
const resolved = path.resolve(expandHomeDir(raw)); const resolved = path.resolve(expandHomeDir(raw));
mkdirSync(resolved, { recursive: true }); mkdirSync(resolved, { recursive: true });
return resolved; return resolved;
+114 -16
View File
@@ -2,22 +2,120 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { z } from "zod"; import { z } from "zod";
const PersistedConfigSchema = z.object({ const LogConfigSchema = z
listen: z.string().optional(), .object({
cors: z level: z
.object({ .enum(["trace", "debug", "info", "warn", "error", "fatal"])
allowedOrigins: z.array(z.string()).default([]), .optional(),
}) format: z.enum(["pretty", "json"]).optional(),
.default({}), })
log: z .strict();
.object({
level: z const ProviderCredentialsSchema = z
.enum(["trace", "debug", "info", "warn", "error", "fatal"]) .object({
.optional(), apiKey: z.string().min(1).optional(),
format: z.enum(["pretty", "json"]).optional(), })
}) .strict();
.optional(),
}); const ProvidersSchema = z
.object({
openai: ProviderCredentialsSchema.optional(),
openrouter: ProviderCredentialsSchema.optional(),
})
.strict();
const FeatureDictationSchema = z
.object({
stt: z
.object({
provider: z.enum(["openai"]).optional(),
model: z.string().min(1).optional(),
confidenceThreshold: z.number().optional(),
})
.strict()
.optional(),
})
.strict();
const FeatureVoiceModeSchema = z
.object({
llm: z
.object({
provider: z.enum(["openrouter"]).optional(),
model: z.string().min(1).optional(),
})
.strict()
.optional(),
stt: z
.object({
provider: z.enum(["openai"]).optional(),
model: z.string().min(1).optional(),
})
.strict()
.optional(),
tts: z
.object({
provider: z.enum(["openai"]).optional(),
model: z.enum(["tts-1", "tts-1-hd"]).optional(),
voice: z.enum(["alloy", "echo", "fable", "onyx", "nova", "shimmer"]).optional(),
})
.strict()
.optional(),
})
.strict();
export const PersistedConfigSchema = z
.object({
// v1 schema marker
version: z.literal(1).optional(),
// v1 config layout
daemon: z
.object({
listen: z.string().optional(),
allowedHosts: z.union([z.literal(true), z.array(z.string())]).optional(),
mcp: z
.object({
enabled: z.boolean().optional(),
})
.strict()
.optional(),
cors: z
.object({
allowedOrigins: z.array(z.string()).optional(),
})
.strict()
.optional(),
relay: z
.object({
enabled: z.boolean().optional(),
endpoint: z.string().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
app: z
.object({
baseUrl: z.string().optional(),
})
.strict()
.optional(),
providers: ProvidersSchema.optional(),
features: z
.object({
dictation: FeatureDictationSchema.optional(),
voiceMode: FeatureVoiceModeSchema.optional(),
})
.strict()
.optional(),
log: LogConfigSchema.optional(),
})
.strict();
export type PersistedConfig = z.infer<typeof PersistedConfigSchema>; export type PersistedConfig = z.infer<typeof PersistedConfigSchema>;
+14 -4
View File
@@ -312,6 +312,8 @@ export class Session {
} | null = null; } | null = null;
private readonly terminalManager: TerminalManager | null; private readonly terminalManager: TerminalManager | null;
private terminalSubscriptions: Map<string, () => void> = new Map(); private terminalSubscriptions: Map<string, () => void> = new Map();
private readonly openrouterApiKey: string | null;
private readonly voiceLlmModel: string | null;
constructor( constructor(
clientId: string, clientId: string,
@@ -327,6 +329,10 @@ export class Session {
tts: OpenAITTS | null, tts: OpenAITTS | null,
terminalManager: TerminalManager | null, terminalManager: TerminalManager | null,
voiceConversationStore: VoiceConversationStore, voiceConversationStore: VoiceConversationStore,
voice?: {
openrouterApiKey?: string | null;
voiceLlmModel?: string | null;
},
dictation?: { dictation?: {
openaiApiKey?: string | null; openaiApiKey?: string | null;
finalTimeoutMs?: number; finalTimeoutMs?: number;
@@ -343,6 +349,8 @@ export class Session {
this.createAgentMcpTransport = createAgentMcpTransport; this.createAgentMcpTransport = createAgentMcpTransport;
this.terminalManager = terminalManager; this.terminalManager = terminalManager;
this.voiceConversationStore = voiceConversationStore; this.voiceConversationStore = voiceConversationStore;
this.openrouterApiKey = voice?.openrouterApiKey ?? null;
this.voiceLlmModel = voice?.voiceLlmModel ?? null;
this.abortController = new AbortController(); this.abortController = new AbortController();
this.sessionLogger = logger.child({ this.sessionLogger = logger.child({
module: "session", module: "session",
@@ -4356,13 +4364,15 @@ export class Session {
// Debug: dump conversation before LLM call // Debug: dump conversation before LLM call
await this.dumpConversation(); await this.dumpConversation();
const openrouterApiKey =
this.openrouterApiKey ?? process.env.OPENROUTER_API_KEY ?? null;
invariant( invariant(
process.env.OPENROUTER_API_KEY, openrouterApiKey,
"OPENROUTER_API_KEY is required" "OpenRouter API key is required (set providers.openrouter.apiKey in config.json or OPENROUTER_API_KEY)"
); );
const openrouter = createOpenRouter({ const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY, apiKey: openrouterApiKey,
}); });
// Wait for agent MCP to initialize if needed // Wait for agent MCP to initialize if needed
@@ -4380,7 +4390,7 @@ export class Session {
const allTools = getAllTools(this.agentTools ?? undefined); const allTools = getAllTools(this.agentTools ?? undefined);
const result = await streamText({ const result = await streamText({
model: openrouter("anthropic/claude-haiku-4.5"), model: openrouter(this.voiceLlmModel ?? "anthropic/claude-haiku-4.5"),
system: getSystemPrompt(), system: getSystemPrompt(),
providerOptions: { providerOptions: {
openrouter: { openrouter: {
@@ -66,8 +66,8 @@ export async function createTestPaseoDaemon(
listen: `${listenHost}:${port}`, listen: `${listenHost}:${port}`,
paseoHome, paseoHome,
corsAllowedOrigins: options.corsAllowedOrigins ?? [], corsAllowedOrigins: options.corsAllowedOrigins ?? [],
agentMcpRoute: "/mcp/agents", allowedHosts: true,
agentMcpAllowedHosts: [`127.0.0.1:${port}`, `localhost:${port}`, `${listenHost}:${port}`], mcpEnabled: true,
staticDir, staticDir,
mcpDebug: false, mcpDebug: false,
agentClients: options.agentClients ?? createTestAgentClients(), agentClients: options.agentClients ?? createTestAgentClients(),
@@ -76,6 +76,7 @@ export async function createTestPaseoDaemon(
relayEndpoint: options.relayEndpoint ?? "relay.paseo.sh:443", relayEndpoint: options.relayEndpoint ?? "relay.paseo.sh:443",
appBaseUrl: "https://app.paseo.sh", appBaseUrl: "https://app.paseo.sh",
openai: options.openai, openai: options.openai,
openrouterApiKey: null,
dictationFinalTimeoutMs: options.dictationFinalTimeoutMs, dictationFinalTimeoutMs: options.dictationFinalTimeoutMs,
downloadTokenTtlMs: options.downloadTokenTtlMs, downloadTokenTtlMs: options.downloadTokenTtlMs,
}; };
+14 -1
View File
@@ -10,11 +10,14 @@ import type { TerminalManager } from "../terminal/terminal-manager.js";
import type pino from "pino"; import type pino from "pino";
import type { WSOutboundMessage } from "./messages.js"; import type { WSOutboundMessage } from "./messages.js";
import { WebSocketSessionBridge } from "./websocket-session-bridge.js"; import { WebSocketSessionBridge } from "./websocket-session-bridge.js";
import type { AllowedHostsConfig } from "./allowed-hosts.js";
import { isHostAllowed } from "./allowed-hosts.js";
export type AgentMcpTransportFactory = () => Promise<Transport>; export type AgentMcpTransportFactory = () => Promise<Transport>;
type WebSocketServerConfig = { type WebSocketServerConfig = {
allowedOrigins: Set<string>; allowedOrigins: Set<string>;
allowedHosts?: AllowedHostsConfig;
}; };
/** /**
@@ -36,6 +39,10 @@ export class VoiceAssistantWebSocketServer {
wsConfig: WebSocketServerConfig, wsConfig: WebSocketServerConfig,
speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null }, speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null },
terminalManager?: TerminalManager | null, terminalManager?: TerminalManager | null,
voice?: {
openrouterApiKey?: string | null;
voiceLlmModel?: string | null;
},
dictation?: { dictation?: {
openaiApiKey?: string | null; openaiApiKey?: string | null;
finalTimeoutMs?: number; finalTimeoutMs?: number;
@@ -51,16 +58,22 @@ export class VoiceAssistantWebSocketServer {
createAgentMcpTransport, createAgentMcpTransport,
speech, speech,
terminalManager, terminalManager,
voice,
dictation dictation
); );
const { allowedOrigins } = wsConfig; const { allowedOrigins, allowedHosts } = wsConfig;
this.wss = new WebSocketServer({ this.wss = new WebSocketServer({
server, server,
path: "/ws", path: "/ws",
verifyClient: ({ req }, callback) => { verifyClient: ({ req }, callback) => {
const origin = req.headers.origin; const origin = req.headers.origin;
const requestHost = typeof req.headers.host === "string" ? req.headers.host : null; const requestHost = typeof req.headers.host === "string" ? req.headers.host : null;
if (requestHost && !isHostAllowed(requestHost, allowedHosts)) {
this.logger.warn({ host: requestHost }, "Rejected connection from disallowed host");
callback(false, 403, "Host not allowed");
return;
}
const sameOrigin = const sameOrigin =
!!origin && !!origin &&
!!requestHost && !!requestHost &&
@@ -41,6 +41,10 @@ export class WebSocketSessionBridge {
openaiApiKey?: string | null; openaiApiKey?: string | null;
finalTimeoutMs?: number; finalTimeoutMs?: number;
} | null; } | null;
private readonly voice: {
openrouterApiKey?: string | null;
voiceLlmModel?: string | null;
} | null;
constructor( constructor(
logger: pino.Logger, logger: pino.Logger,
@@ -51,6 +55,10 @@ export class WebSocketSessionBridge {
createAgentMcpTransport: AgentMcpTransportFactory, createAgentMcpTransport: AgentMcpTransportFactory,
speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null }, speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null },
terminalManager?: TerminalManager | null, terminalManager?: TerminalManager | null,
voice?: {
openrouterApiKey?: string | null;
voiceLlmModel?: string | null;
},
dictation?: { dictation?: {
openaiApiKey?: string | null; openaiApiKey?: string | null;
finalTimeoutMs?: number; finalTimeoutMs?: number;
@@ -68,6 +76,7 @@ export class WebSocketSessionBridge {
this.voiceConversationStore = new VoiceConversationStore( this.voiceConversationStore = new VoiceConversationStore(
join(paseoHome, "voice-conversations") join(paseoHome, "voice-conversations")
); );
this.voice = voice ?? null;
this.dictation = dictation ?? null; this.dictation = dictation ?? null;
const pushLogger = this.logger.child({ module: "push" }); const pushLogger = this.logger.child({ module: "push" });
@@ -106,6 +115,7 @@ export class WebSocketSessionBridge {
this.tts, this.tts,
this.terminalManager, this.terminalManager,
this.voiceConversationStore, this.voiceConversationStore,
this.voice ?? undefined,
this.dictation ?? undefined this.dictation ?? undefined
); );
@@ -0,0 +1,223 @@
{
"$ref": "#/definitions/PaseoConfigV1",
"definitions": {
"PaseoConfigV1": {
"type": "object",
"properties": {
"version": {
"type": "number",
"const": 1
},
"daemon": {
"type": "object",
"properties": {
"listen": {
"type": "string"
},
"allowedHosts": {
"anyOf": [
{
"type": "boolean",
"const": true
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"mcp": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
},
"additionalProperties": false
},
"cors": {
"type": "object",
"properties": {
"allowedOrigins": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"relay": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"endpoint": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"app": {
"type": "object",
"properties": {
"baseUrl": {
"type": "string"
}
},
"additionalProperties": false
},
"providers": {
"type": "object",
"properties": {
"openai": {
"type": "object",
"properties": {
"apiKey": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": false
},
"openrouter": {
"$ref": "#/definitions/PaseoConfigV1/properties/providers/properties/openai"
}
},
"additionalProperties": false
},
"features": {
"type": "object",
"properties": {
"dictation": {
"type": "object",
"properties": {
"stt": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"enum": [
"openai"
]
},
"model": {
"type": "string",
"minLength": 1
},
"confidenceThreshold": {
"type": "number"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"voiceMode": {
"type": "object",
"properties": {
"llm": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"enum": [
"openrouter"
]
},
"model": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": false
},
"stt": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"enum": [
"openai"
]
},
"model": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": false
},
"tts": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"enum": [
"openai"
]
},
"model": {
"type": "string",
"enum": [
"tts-1",
"tts-1-hd"
]
},
"voice": {
"type": "string",
"enum": [
"alloy",
"echo",
"fable",
"onyx",
"nova",
"shimmer"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"log": {
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": [
"trace",
"debug",
"info",
"warn",
"error",
"fatal"
]
},
"format": {
"type": "string",
"enum": [
"pretty",
"json"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
+39 -3
View File
@@ -13,6 +13,8 @@ import { Route as DocsRouteImport } from './routes/docs'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index' import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices' import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices'
import { Route as DocsConfigurationRouteImport } from './routes/docs/configuration'
import { Route as DocsSecurityRouteImport } from './routes/docs/security'
const DocsRoute = DocsRouteImport.update({ const DocsRoute = DocsRouteImport.update({
id: '/docs', id: '/docs',
@@ -34,16 +36,30 @@ const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({
path: '/best-practices', path: '/best-practices',
getParentRoute: () => DocsRoute, getParentRoute: () => DocsRoute,
} as any) } as any)
const DocsConfigurationRoute = DocsConfigurationRouteImport.update({
id: '/configuration',
path: '/configuration',
getParentRoute: () => DocsRoute,
} as any)
const DocsSecurityRoute = DocsSecurityRouteImport.update({
id: '/security',
path: '/security',
getParentRoute: () => DocsRoute,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/docs': typeof DocsRouteWithChildren '/docs': typeof DocsRouteWithChildren
'/docs/best-practices': typeof DocsBestPracticesRoute '/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs/': typeof DocsIndexRoute '/docs/': typeof DocsIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/docs/best-practices': typeof DocsBestPracticesRoute '/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs': typeof DocsIndexRoute '/docs': typeof DocsIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@@ -51,14 +67,16 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/docs': typeof DocsRouteWithChildren '/docs': typeof DocsRouteWithChildren
'/docs/best-practices': typeof DocsBestPracticesRoute '/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs/': typeof DocsIndexRoute '/docs/': typeof DocsIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/docs' | '/docs/best-practices' | '/docs/' fullPaths: '/' | '/docs' | '/docs/best-practices' | '/docs/configuration' | '/docs/security' | '/docs/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/docs/best-practices' | '/docs' to: '/' | '/docs/best-practices' | '/docs/configuration' | '/docs/security' | '/docs'
id: '__root__' | '/' | '/docs' | '/docs/best-practices' | '/docs/' id: '__root__' | '/' | '/docs' | '/docs/best-practices' | '/docs/configuration' | '/docs/security' | '/docs/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
@@ -96,16 +114,34 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsBestPracticesRouteImport preLoaderRoute: typeof DocsBestPracticesRouteImport
parentRoute: typeof DocsRoute parentRoute: typeof DocsRoute
} }
'/docs/configuration': {
id: '/docs/configuration'
path: '/configuration'
fullPath: '/docs/configuration'
preLoaderRoute: typeof DocsConfigurationRouteImport
parentRoute: typeof DocsRoute
}
'/docs/security': {
id: '/docs/security'
path: '/security'
fullPath: '/docs/security'
preLoaderRoute: typeof DocsSecurityRouteImport
parentRoute: typeof DocsRoute
}
} }
} }
interface DocsRouteChildren { interface DocsRouteChildren {
DocsBestPracticesRoute: typeof DocsBestPracticesRoute DocsBestPracticesRoute: typeof DocsBestPracticesRoute
DocsConfigurationRoute: typeof DocsConfigurationRoute
DocsSecurityRoute: typeof DocsSecurityRoute
DocsIndexRoute: typeof DocsIndexRoute DocsIndexRoute: typeof DocsIndexRoute
} }
const DocsRouteChildren: DocsRouteChildren = { const DocsRouteChildren: DocsRouteChildren = {
DocsBestPracticesRoute: DocsBestPracticesRoute, DocsBestPracticesRoute: DocsBestPracticesRoute,
DocsConfigurationRoute: DocsConfigurationRoute,
DocsSecurityRoute: DocsSecurityRoute,
DocsIndexRoute: DocsIndexRoute, DocsIndexRoute: DocsIndexRoute,
} }
+2
View File
@@ -7,6 +7,8 @@ export const Route = createFileRoute('/docs')({
const navigation = [ const navigation = [
{ name: 'Getting started', href: '/docs' }, { name: 'Getting started', href: '/docs' },
{ name: 'Configuration', href: '/docs/configuration' },
{ name: 'Security', href: '/docs/security' },
{ name: 'Best practices', href: '/docs/best-practices' }, { name: 'Best practices', href: '/docs/best-practices' },
] ]