mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add file download tokens over WS and HTTP
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import express, { type Express } from "express";
|
||||
import basicAuth from "express-basic-auth";
|
||||
import { createServer as createHTTPServer, type Server as HTTPServer } from "http";
|
||||
import { createReadStream } from "fs";
|
||||
import { stat } from "fs/promises";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
import { VoiceAssistantWebSocketServer } from "./websocket-server.js";
|
||||
import { DownloadTokenStore } from "./file-download/token-store.js";
|
||||
import { initializeSTT, type STTConfig } from "./agent/stt-openai.js";
|
||||
import { initializeTTS, type TTSConfig } from "./agent/tts-openai.js";
|
||||
import { listConversations, deleteConversation } from "./persistence.js";
|
||||
@@ -49,6 +52,7 @@ export type PaseoDaemonConfig = {
|
||||
agentRegistryPath: string;
|
||||
agentControlMcp: AgentControlMcpConfig;
|
||||
openai?: PaseoOpenAIConfig;
|
||||
downloadTokenTtlMs?: number;
|
||||
};
|
||||
|
||||
export type PaseoDaemonHandles = {
|
||||
@@ -67,9 +71,12 @@ export async function createPaseoDaemon(
|
||||
const basicAuthUsers = config.auth.basicUsers;
|
||||
const staticDir = config.staticDir;
|
||||
const authRealm = config.auth.realm ?? "Voice Assistant";
|
||||
const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
|
||||
|
||||
const agentMcpBearerToken = config.auth.agentMcpBearerToken;
|
||||
|
||||
const downloadTokenStore = new DownloadTokenStore({ ttlMs: downloadTokenTtlMs });
|
||||
|
||||
const app = express();
|
||||
|
||||
// Serve static files from public directory (no auth required for APK downloads)
|
||||
@@ -124,6 +131,56 @@ export async function createPaseoDaemon(
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/files/download", async (req, res) => {
|
||||
const token =
|
||||
typeof req.query.token === "string" && req.query.token.trim().length > 0
|
||||
? req.query.token.trim()
|
||||
: null;
|
||||
|
||||
if (!token) {
|
||||
res.status(400).json({ error: "Missing download token" });
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = downloadTokenStore.consumeToken(token);
|
||||
if (!entry) {
|
||||
res.status(403).json({ error: "Invalid or expired token" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileStats = await stat(entry.absolutePath);
|
||||
if (!fileStats.isFile()) {
|
||||
res.status(404).json({ error: "File not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const safeFileName = entry.fileName.replace(/["\r\n]/g, "_");
|
||||
res.setHeader("Content-Type", entry.mimeType);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${safeFileName}"`
|
||||
);
|
||||
res.setHeader("Content-Length", entry.size.toString());
|
||||
|
||||
const stream = createReadStream(entry.absolutePath);
|
||||
stream.on("error", (error) => {
|
||||
console.error("[API] Failed to stream download:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: "Failed to read file" });
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
stream.pipe(res);
|
||||
} catch (error) {
|
||||
console.error("[API] Failed to download file:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(404).json({ error: "File not found" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createHTTPServer(app);
|
||||
|
||||
const agentRegistry = new AgentRegistry(config.agentRegistryPath);
|
||||
@@ -252,6 +309,7 @@ export async function createPaseoDaemon(
|
||||
httpServer,
|
||||
agentManager,
|
||||
agentRegistry,
|
||||
downloadTokenStore,
|
||||
{
|
||||
agentMcpUrl: config.agentControlMcp.url,
|
||||
agentMcpHeaders: config.agentControlMcp.headers,
|
||||
|
||||
@@ -499,6 +499,136 @@ describe("daemon E2E", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("file download tokens", () => {
|
||||
test(
|
||||
"issues token over WS and downloads via HTTP",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
const filePath = path.join(cwd, "download.txt");
|
||||
const fileContents = "download test payload";
|
||||
writeFileSync(filePath, fileContents, "utf-8");
|
||||
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd,
|
||||
title: "Download Token Test Agent",
|
||||
});
|
||||
|
||||
expect(agent.id).toBeTruthy();
|
||||
|
||||
const tokenResponse = await ctx.client.requestDownloadToken(
|
||||
agent.id,
|
||||
"download.txt"
|
||||
);
|
||||
|
||||
expect(tokenResponse.error).toBeNull();
|
||||
expect(tokenResponse.token).toBeTruthy();
|
||||
expect(tokenResponse.fileName).toBe("download.txt");
|
||||
|
||||
const authHeader = ctx.daemon.agentMcpAuthHeader;
|
||||
expect(authHeader).toBeTruthy();
|
||||
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${ctx.daemon.port}/api/files/download?token=${tokenResponse.token}`,
|
||||
{ headers: { Authorization: authHeader! } }
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe(
|
||||
tokenResponse.mimeType
|
||||
);
|
||||
const disposition = response.headers.get("content-disposition") ?? "";
|
||||
expect(disposition).toContain("download.txt");
|
||||
|
||||
const body = await response.text();
|
||||
expect(body).toBe(fileContents);
|
||||
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
},
|
||||
60000
|
||||
);
|
||||
|
||||
test(
|
||||
"rejects invalid token",
|
||||
async () => {
|
||||
const authHeader = ctx.daemon.agentMcpAuthHeader;
|
||||
expect(authHeader).toBeTruthy();
|
||||
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${ctx.daemon.port}/api/files/download?token=invalid-token`,
|
||||
{ headers: { Authorization: authHeader! } }
|
||||
);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
},
|
||||
30000
|
||||
);
|
||||
|
||||
test(
|
||||
"rejects expired token",
|
||||
async () => {
|
||||
await ctx.cleanup();
|
||||
ctx = await createDaemonTestContext({ downloadTokenTtlMs: 50 });
|
||||
|
||||
const cwd = tmpCwd();
|
||||
const filePath = path.join(cwd, "expired.txt");
|
||||
writeFileSync(filePath, "expired", "utf-8");
|
||||
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd,
|
||||
title: "Expired Token Test Agent",
|
||||
});
|
||||
|
||||
const tokenResponse = await ctx.client.requestDownloadToken(
|
||||
agent.id,
|
||||
"expired.txt"
|
||||
);
|
||||
|
||||
expect(tokenResponse.error).toBeNull();
|
||||
expect(tokenResponse.token).toBeTruthy();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
|
||||
const authHeader = ctx.daemon.agentMcpAuthHeader;
|
||||
expect(authHeader).toBeTruthy();
|
||||
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${ctx.daemon.port}/api/files/download?token=${tokenResponse.token}`,
|
||||
{ headers: { Authorization: authHeader! } }
|
||||
);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
},
|
||||
60000
|
||||
);
|
||||
|
||||
test(
|
||||
"rejects paths outside the agent cwd",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd,
|
||||
title: "Outside Path Token Test Agent",
|
||||
});
|
||||
|
||||
const tokenResponse = await ctx.client.requestDownloadToken(
|
||||
agent.id,
|
||||
"../outside.txt"
|
||||
);
|
||||
|
||||
expect(tokenResponse.token).toBeNull();
|
||||
expect(tokenResponse.error).toBeTruthy();
|
||||
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
},
|
||||
60000
|
||||
);
|
||||
});
|
||||
|
||||
describe("persistence flow", () => {
|
||||
test(
|
||||
"persists and resumes Codex agent with conversation history",
|
||||
|
||||
65
packages/server/src/server/file-download/token-store.ts
Normal file
65
packages/server/src/server/file-download/token-store.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export type DownloadTokenEntry = {
|
||||
token: string;
|
||||
agentId: string;
|
||||
path: string;
|
||||
absolutePath: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
type DownloadTokenStoreOptions = {
|
||||
ttlMs: number;
|
||||
now?: () => number;
|
||||
};
|
||||
|
||||
export class DownloadTokenStore {
|
||||
private readonly ttlMs: number;
|
||||
private readonly now: () => number;
|
||||
private readonly tokens = new Map<string, DownloadTokenEntry>();
|
||||
|
||||
constructor(options: DownloadTokenStoreOptions) {
|
||||
this.ttlMs = options.ttlMs;
|
||||
this.now = options.now ?? (() => Date.now());
|
||||
}
|
||||
|
||||
issueToken(input: Omit<DownloadTokenEntry, "token" | "expiresAt">): DownloadTokenEntry {
|
||||
this.pruneExpired();
|
||||
const token = randomUUID();
|
||||
const expiresAt = this.now() + this.ttlMs;
|
||||
const entry: DownloadTokenEntry = {
|
||||
...input,
|
||||
token,
|
||||
expiresAt,
|
||||
};
|
||||
this.tokens.set(token, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
consumeToken(token: string): DownloadTokenEntry | null {
|
||||
const entry = this.tokens.get(token);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.tokens.delete(token);
|
||||
|
||||
if (entry.expiresAt <= this.now()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private pruneExpired(): void {
|
||||
const now = this.now();
|
||||
for (const [token, entry] of this.tokens) {
|
||||
if (entry.expiresAt <= now) {
|
||||
this.tokens.delete(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,39 @@ export async function readExplorerFile({
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDownloadableFileInfo({
|
||||
root,
|
||||
relativePath,
|
||||
}: ReadFileParams): Promise<{
|
||||
path: string;
|
||||
absolutePath: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
}> {
|
||||
const filePath = await resolveScopedPath({ root, relativePath });
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
if (!stats.isFile()) {
|
||||
throw new Error("Requested path is not a file");
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mimeType = TEXT_EXTENSIONS.has(ext)
|
||||
? "text/plain"
|
||||
: ext in IMAGE_MIME_TYPES
|
||||
? IMAGE_MIME_TYPES[ext]
|
||||
: "application/octet-stream";
|
||||
|
||||
return {
|
||||
path: normalizeRelativePath({ root, targetPath: filePath }),
|
||||
absolutePath: filePath,
|
||||
fileName: path.basename(filePath),
|
||||
mimeType,
|
||||
size: stats.size,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveScopedPath({
|
||||
root,
|
||||
relativePath = ".",
|
||||
|
||||
@@ -483,6 +483,12 @@ export const FileExplorerRequestSchema = z.object({
|
||||
mode: z.enum(["list", "file"]),
|
||||
});
|
||||
|
||||
export const FileDownloadTokenRequestSchema = z.object({
|
||||
type: z.literal("file_download_token_request"),
|
||||
agentId: z.string(),
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
export const ClearAgentAttentionMessageSchema = z.object({
|
||||
type: z.literal("clear_agent_attention"),
|
||||
agentId: z.union([z.string(), z.array(z.string())]),
|
||||
@@ -511,6 +517,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
AgentPermissionResponseMessageSchema,
|
||||
GitDiffRequestSchema,
|
||||
FileExplorerRequestSchema,
|
||||
FileDownloadTokenRequestSchema,
|
||||
ListPersistedAgentsRequestMessageSchema,
|
||||
GitRepoInfoRequestMessageSchema,
|
||||
ClearAgentAttentionMessageSchema,
|
||||
@@ -744,6 +751,19 @@ export const FileExplorerResponseSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const FileDownloadTokenResponseSchema = z.object({
|
||||
type: z.literal("file_download_token_response"),
|
||||
payload: z.object({
|
||||
agentId: z.string(),
|
||||
path: z.string(),
|
||||
token: z.string().nullable(),
|
||||
fileName: z.string().nullable(),
|
||||
mimeType: z.string().nullable(),
|
||||
size: z.number().nullable(),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
const GitBranchInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
isCurrent: z.boolean(),
|
||||
@@ -795,6 +815,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
ListPersistedAgentsResponseSchema,
|
||||
GitDiffResponseSchema,
|
||||
FileExplorerResponseSchema,
|
||||
FileDownloadTokenResponseSchema,
|
||||
GitRepoInfoResponseSchema,
|
||||
ListProviderModelsResponseMessageSchema,
|
||||
]);
|
||||
@@ -851,6 +872,8 @@ export type GitDiffRequest = z.infer<typeof GitDiffRequestSchema>;
|
||||
export type GitDiffResponse = z.infer<typeof GitDiffResponseSchema>;
|
||||
export type FileExplorerRequest = z.infer<typeof FileExplorerRequestSchema>;
|
||||
export type FileExplorerResponse = z.infer<typeof FileExplorerResponseSchema>;
|
||||
export type FileDownloadTokenRequest = z.infer<typeof FileDownloadTokenRequestSchema>;
|
||||
export type FileDownloadTokenResponse = z.infer<typeof FileDownloadTokenResponseSchema>;
|
||||
export type RestartServerRequestMessage = z.infer<typeof RestartServerRequestMessageSchema>;
|
||||
export type ClearAgentAttentionMessage = z.infer<typeof ClearAgentAttentionMessageSchema>;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type SessionInboundMessage,
|
||||
type SessionOutboundMessage,
|
||||
type FileExplorerRequest,
|
||||
type FileDownloadTokenRequest,
|
||||
type GitSetupOptions,
|
||||
} from "./messages.js";
|
||||
import { getSystemPrompt } from "./agent/system-prompt.js";
|
||||
@@ -54,7 +55,9 @@ import { expandTilde } from "./terminal-mcp/tmux.js";
|
||||
import {
|
||||
listDirectoryEntries,
|
||||
readExplorerFile,
|
||||
getDownloadableFileInfo,
|
||||
} from "./file-explorer/service.js";
|
||||
import { DownloadTokenStore } from "./file-download/token-store.js";
|
||||
import {
|
||||
generateAgentTitle,
|
||||
isTitleGeneratorInitialized,
|
||||
@@ -238,12 +241,14 @@ export class Session {
|
||||
private agentManager: AgentManager;
|
||||
private readonly agentRegistry: AgentRegistry;
|
||||
private readonly agentMcpConfig: AgentMcpClientConfig;
|
||||
private readonly downloadTokenStore: DownloadTokenStore;
|
||||
private agentTitleCache: Map<string, string | null> = new Map();
|
||||
private unsubscribeAgentEvents: (() => void) | null = null;
|
||||
|
||||
constructor(
|
||||
clientId: string,
|
||||
onMessage: (msg: SessionOutboundMessage) => void,
|
||||
downloadTokenStore: DownloadTokenStore,
|
||||
agentManager: AgentManager,
|
||||
agentRegistry: AgentRegistry,
|
||||
agentMcpConfig: AgentMcpClientConfig,
|
||||
@@ -255,6 +260,7 @@ export class Session {
|
||||
this.clientId = clientId;
|
||||
this.conversationId = options?.conversationId || uuidv4();
|
||||
this.onMessage = onMessage;
|
||||
this.downloadTokenStore = downloadTokenStore;
|
||||
this.agentManager = agentManager;
|
||||
this.agentRegistry = agentRegistry;
|
||||
this.agentMcpConfig = agentMcpConfig;
|
||||
@@ -842,6 +848,10 @@ export class Session {
|
||||
await this.handleFileExplorerRequest(msg);
|
||||
break;
|
||||
|
||||
case "file_download_token_request":
|
||||
await this.handleFileDownloadTokenRequest(msg);
|
||||
break;
|
||||
|
||||
case "list_persisted_agents_request":
|
||||
await this.handleListPersistedAgentsRequest(msg);
|
||||
break;
|
||||
@@ -2082,6 +2092,84 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file download token request scoped to an agent's cwd
|
||||
*/
|
||||
private async handleFileDownloadTokenRequest(
|
||||
request: FileDownloadTokenRequest
|
||||
): Promise<void> {
|
||||
const { agentId, path: requestedPath } = request;
|
||||
|
||||
console.log(
|
||||
`[Session ${this.clientId}] Handling file download token request for agent ${agentId} (${requestedPath})`
|
||||
);
|
||||
|
||||
try {
|
||||
const agents = this.agentManager.listAgents();
|
||||
const agent = agents.find((a) => a.id === agentId);
|
||||
|
||||
if (!agent) {
|
||||
this.emit({
|
||||
type: "file_download_token_response",
|
||||
payload: {
|
||||
agentId,
|
||||
path: requestedPath,
|
||||
token: null,
|
||||
fileName: null,
|
||||
mimeType: null,
|
||||
size: null,
|
||||
error: `Agent not found: ${agentId}`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const info = await getDownloadableFileInfo({
|
||||
root: agent.cwd,
|
||||
relativePath: requestedPath,
|
||||
});
|
||||
|
||||
const entry = this.downloadTokenStore.issueToken({
|
||||
agentId,
|
||||
path: info.path,
|
||||
absolutePath: info.absolutePath,
|
||||
fileName: info.fileName,
|
||||
mimeType: info.mimeType,
|
||||
size: info.size,
|
||||
});
|
||||
|
||||
this.emit({
|
||||
type: "file_download_token_response",
|
||||
payload: {
|
||||
agentId,
|
||||
path: info.path,
|
||||
token: entry.token,
|
||||
fileName: entry.fileName,
|
||||
mimeType: entry.mimeType,
|
||||
size: entry.size,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`[Session ${this.clientId}] Failed to issue download token for agent ${agentId}:`,
|
||||
error
|
||||
);
|
||||
this.emit({
|
||||
type: "file_download_token_response",
|
||||
payload: {
|
||||
agentId,
|
||||
path: requestedPath,
|
||||
token: null,
|
||||
fileName: null,
|
||||
mimeType: null,
|
||||
size: null,
|
||||
error: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send current session state (live agents and commands) to client
|
||||
*/
|
||||
|
||||
@@ -534,6 +534,45 @@ export class DaemonClient {
|
||||
);
|
||||
}
|
||||
|
||||
async requestDownloadToken(
|
||||
agentId: string,
|
||||
path: string
|
||||
): Promise<{
|
||||
agentId: string;
|
||||
path: string;
|
||||
token: string | null;
|
||||
fileName: string | null;
|
||||
mimeType: string | null;
|
||||
size: number | null;
|
||||
error: string | null;
|
||||
}> {
|
||||
const startPosition = this.messageQueue.length;
|
||||
|
||||
this.send({ type: "file_download_token_request", agentId, path });
|
||||
|
||||
return this.waitFor(
|
||||
(msg) => {
|
||||
if (
|
||||
msg.type === "file_download_token_response" &&
|
||||
msg.payload.agentId === agentId
|
||||
) {
|
||||
return {
|
||||
agentId: msg.payload.agentId,
|
||||
path: msg.payload.path,
|
||||
token: msg.payload.token,
|
||||
fileName: msg.payload.fileName,
|
||||
mimeType: msg.payload.mimeType,
|
||||
size: msg.payload.size,
|
||||
error: msg.payload.error,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
10000,
|
||||
{ skipQueueBefore: startPosition }
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Provider Models
|
||||
// ============================================================================
|
||||
|
||||
@@ -31,8 +31,10 @@ export interface DaemonTestContext {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function createDaemonTestContext(): Promise<DaemonTestContext> {
|
||||
const daemon = await createTestPaseoDaemon();
|
||||
export async function createDaemonTestContext(
|
||||
options?: Parameters<typeof createTestPaseoDaemon>[0]
|
||||
): Promise<DaemonTestContext> {
|
||||
const daemon = await createTestPaseoDaemon(options);
|
||||
const client = new DaemonClient({
|
||||
url: `ws://127.0.0.1:${daemon.port}/ws`,
|
||||
authHeader: daemon.agentMcpAuthHeader,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createPaseoDaemon, type PaseoDaemonConfig } from "../bootstrap.js";
|
||||
|
||||
type TestPaseoDaemonOptions = {
|
||||
basicUsers?: Record<string, string>;
|
||||
downloadTokenTtlMs?: number;
|
||||
};
|
||||
|
||||
export type TestPaseoDaemon = {
|
||||
@@ -73,6 +74,7 @@ export async function createTestPaseoDaemon(
|
||||
? { headers: { Authorization: agentMcpAuthHeader } }
|
||||
: {}),
|
||||
},
|
||||
downloadTokenTtlMs: options.downloadTokenTtlMs,
|
||||
};
|
||||
|
||||
const daemon = await createPaseoDaemon(config);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Session } from "./session.js";
|
||||
import { loadConversation } from "./persistence.js";
|
||||
import { AgentManager } from "./agent/agent-manager.js";
|
||||
import { AgentRegistry } from "./agent/agent-registry.js";
|
||||
import { DownloadTokenStore } from "./file-download/token-store.js";
|
||||
|
||||
type AgentMcpClientConfig = {
|
||||
agentMcpUrl: string;
|
||||
@@ -29,16 +30,19 @@ export class VoiceAssistantWebSocketServer {
|
||||
private clientIdCounter: number = 0;
|
||||
private agentManager: AgentManager;
|
||||
private agentRegistry: AgentRegistry;
|
||||
private downloadTokenStore: DownloadTokenStore;
|
||||
private readonly agentMcpConfig: AgentMcpClientConfig;
|
||||
|
||||
constructor(
|
||||
server: HTTPServer,
|
||||
agentManager: AgentManager,
|
||||
agentRegistry: AgentRegistry,
|
||||
downloadTokenStore: DownloadTokenStore,
|
||||
agentMcpConfig: AgentMcpClientConfig
|
||||
) {
|
||||
this.agentManager = agentManager;
|
||||
this.agentRegistry = agentRegistry;
|
||||
this.downloadTokenStore = downloadTokenStore;
|
||||
this.agentMcpConfig = agentMcpConfig;
|
||||
this.wss = new WebSocketServer({ server, path: "/ws" });
|
||||
|
||||
@@ -85,6 +89,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
(msg) => {
|
||||
this.sendToClient(ws, wrapSessionMessage(msg));
|
||||
},
|
||||
this.downloadTokenStore,
|
||||
this.agentManager,
|
||||
this.agentRegistry,
|
||||
this.agentMcpConfig,
|
||||
|
||||
Reference in New Issue
Block a user