feat: route profile consumers through sessiond

This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 23:09:32 +02:00
parent 141cda93c8
commit 97e0afc6fa
15 changed files with 637 additions and 118 deletions
+34 -21
View File
@@ -1,7 +1,7 @@
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import Fastify, { type FastifyInstance, type FastifyServerOptions } from "fastify";
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyServerOptions } from "fastify";
import fastifyCompress from "@fastify/compress";
import fastifyStatic from "@fastify/static";
import fastifyWebsocket from "@fastify/websocket";
@@ -21,11 +21,16 @@ import { registerTerminalProxyRoutes } from "./terminalProxyRoutes.js";
import { registerWorkspaceDeletionRoutes } from "./workspaces/workspaceDeletionRoutes.js";
import { createFilePiWebConfigService, registerConfigRoutes, registerLocalMachineConfigRoutes, type PiWebConfigService } from "./configRoutes.js";
import { PiWebPluginService } from "./piWebPluginService.js";
import { createDefaultPiPackageService, type PiPackageService } from "./piPackageService.js";
import { createActiveProfilePiPackageService, type PiPackageService } from "./piPackageService.js";
import { registerPiPackageRoutes } from "./piPackageRoutes.js";
import { createPiWebStatusCache, type PiWebStatusCache } from "./piWebStatusCache.js";
import { getPiWebRuntime, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
import { effectiveAgentConfig, type EffectivePiWebAgentConfig } from "../config.js";
import {
ActiveAgentProfileAccessError,
requireActiveAgentProfile,
SessionDaemonActiveAgentProfileProvider,
type ActiveAgentProfileProvider,
} from "./activeAgentProfileProvider.js";
import { MachineService } from "./machines/machineService.js";
import { registerMachineRoutes } from "./machines/machineRoutes.js";
import { registerMachineProxyRoutes } from "./machines/machineProxyRoutes.js";
@@ -37,6 +42,7 @@ export interface AppDependencies {
workspaces?: WorkspaceService;
machines?: MachineService;
sessionDaemon?: SessionProxyDaemon;
agentProfileProvider?: ActiveAgentProfileProvider;
piWebPlugins?: Pick<PiWebPluginService, "manifest" | "plugins" | "readAsset">;
piPackages?: PiPackageService;
piWebStatusCache?: PiWebStatusCache;
@@ -125,10 +131,6 @@ async function readEffectiveConfig(config: Pick<PiWebConfigService, "read">) {
return (await config.read()).effectiveConfig;
}
async function readEffectiveAgentConfig(config: Pick<PiWebConfigService, "read">): Promise<EffectivePiWebAgentConfig> {
return effectiveAgentConfig(process.env, await readEffectiveConfig(config));
}
function invalidatePiWebStatusOnWrite(config: PiWebConfigService, statusCache: Pick<PiWebStatusCache, "invalidate">): PiWebConfigService {
return {
read: () => config.read(),
@@ -140,6 +142,15 @@ function invalidatePiWebStatusOnWrite(config: PiWebConfigService, statusCache: P
};
}
async function withProfileDependency<T>(reply: FastifyReply, operation: () => Promise<T>): Promise<T | FastifyReply> {
try {
return await operation();
} catch (error) {
if (!(error instanceof ActiveAgentProfileAccessError)) throw error;
return reply.code(503).send({ error: error.message });
}
}
export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInstance> {
const app = Fastify({ logger: deps.logger ?? true, ...(deps.bodyLimit === undefined ? {} : { bodyLimit: deps.bodyLimit }) });
// Vite proxies development API requests here, while production and machine-scoped
@@ -155,19 +166,19 @@ export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInsta
const workspaces = deps.workspaces ?? new WorkspaceService();
const configService = deps.config ?? createFilePiWebConfigService();
const readConfig = () => readEffectiveConfig(configService);
const readAgentConfig = () => readEffectiveAgentConfig(configService);
const sessionDaemon = deps.sessionDaemon ?? new SessionDaemonClient();
const agentProfileProvider = deps.agentProfileProvider ?? new SessionDaemonActiveAgentProfileProvider(sessionDaemon);
const piWebPlugins = deps.piWebPlugins ?? new PiWebPluginService({
configProvider: readConfig,
agentDirProvider: async () => (await requireActiveAgentProfile(agentProfileProvider)).dir,
});
const piPackages = deps.piPackages ?? createDefaultPiPackageService(process.cwd(), (await readAgentConfig()).dir);
const sessionDaemon = deps.sessionDaemon ?? new SessionDaemonClient();
const piPackages = deps.piPackages ?? createActiveProfilePiPackageService(agentProfileProvider);
const piWebStatusCache = deps.piWebStatusCache ?? createPiWebStatusCache(
async ({ force }) => {
const agent = await readAgentConfig();
const activeAgentProfile = await agentProfileProvider.getActiveAgentProfile();
return getPiWebStatus(sessionDaemon, {
forceReleaseCheck: force,
agentCommand: agent.command,
agentDir: agent.dir,
...(activeAgentProfile.status === "available" ? { activeAgentProfile: activeAgentProfile.profile } : {}),
});
},
{ onError: (error) => { app.log.warn({ err: error }, "failed to refresh PI WEB status cache"); } },
@@ -176,26 +187,28 @@ export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInsta
localRuntime: () => getPiWebRuntime(sessionDaemon),
});
app.get("/pi-web-plugins/manifest.json", async () => piWebPlugins.manifest());
app.get("/pi-web-plugins/manifest.json", async (_request, reply) => withProfileDependency(reply, () => piWebPlugins.manifest()));
app.get<{ Params: { pluginId: string; "*": string } }>("/pi-web-plugins/:pluginId/*", async (request, reply) => {
if (await proxyMachinePluginAsset(machines, request.params.pluginId, request.params["*"], request.url, reply)) return;
const asset = await piWebPlugins.readAsset(request.params.pluginId, request.params["*"]);
if (asset === undefined) return reply.code(404).send({ error: "Plugin asset not found" });
return reply.type(asset.contentType).send(asset.content);
return withProfileDependency(reply, async () => {
const asset = await piWebPlugins.readAsset(request.params.pluginId, request.params["*"]);
if (asset === undefined) return reply.code(404).send({ error: "Plugin asset not found" });
return reply.type(asset.contentType).send(asset.content);
});
});
app.get<{ Querystring: { refresh?: string } }>("/api/pi-web/status", async (request) => request.query.refresh === "1"
? piWebStatusCache.refresh({ force: true })
: piWebStatusCache.get());
app.get("/api/pi-web/version", async () => {
const agent = await readAgentConfig();
return getPiWebVersionStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir });
const activeAgentProfile = await agentProfileProvider.getActiveAgentProfile();
return getPiWebVersionStatus(sessionDaemon, activeAgentProfile.status === "available" ? { activeAgentProfile: activeAgentProfile.profile } : {});
});
app.get("/api/pi-web/runtime", async () => getPiWebRuntime(sessionDaemon));
app.get("/api/plugins", async () => piWebPlugins.plugins());
app.get("/api/machines/local/plugins", async () => piWebPlugins.plugins());
app.get("/api/plugins", async (_request, reply) => withProfileDependency(reply, () => piWebPlugins.plugins()));
app.get("/api/machines/local/plugins", async (_request, reply) => withProfileDependency(reply, () => piWebPlugins.plugins()));
registerPiPackageRoutes(app, piPackages);
registerPiPackageRoutes(app, piPackages, "/api/machines/local");
const invalidatingConfigService = invalidatePiWebStatusOnWrite(configService, piWebStatusCache);