Merge branch 'plan-paseo-config-schema'
This commit is contained in:
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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#"
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user