This repository has been archived on 2026-08-23. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
pi-web/src/server/sessiond.ts
T
Federico Jaramillo Martinez 20d424c48f feat(sessions): reject extension-scoped provider registrations
PI WEB only supports globally configured providers (Pi built-ins,
agent-dir models.json, environment credentials). A daemon-wide shim on
the shared ModelRuntime swallows extension registerProvider calls and
makes unregisterProvider a no-op, so one workspace's extensions can no
longer corrupt the provider set of concurrent sessions (issue #76).

Rejections during a services load surface as session warnings through
the existing diagnostics pipeline; late registrations from session
event handlers broadcast a notification to active sessions. Everything
else extensions register keeps working.

Requires manual restart of pi-web-sessiond.service (daemon wiring changed).
2026-07-22 08:57:05 +02:00

143 lines
6.8 KiB
JavaScript

#!/usr/bin/env node
import { mkdir, rm } from "node:fs/promises";
import { dirname } from "node:path";
import Fastify from "fastify";
import fastifyWebsocket from "@fastify/websocket";
import { WorkspaceActivityService } from "./activity/workspaceActivityService.js";
import { registerWorkspaceActivityRoutes } from "./activity/workspaceActivityRoutes.js";
import { SessionEventHub } from "./realtime/sessionEventHub.js";
import { AuthService } from "./sessions/authService.js";
import { installGlobalProviderPolicy } from "./sessions/globalProviderPolicy.js";
import { registerAuthRoutes } from "./sessions/authRoutes.js";
import { PiSessionService } from "./sessions/piSessionService.js";
import { createPiSessionManagerGateway } from "./sessions/piSessionManagerGateway.js";
import { registerSessionRoutes } from "./sessions/sessionRoutes.js";
import { SessionNotificationStore } from "./sessions/sessionNotificationStore.js";
import { FileSessionUnreadPersistence, SessionUnreadStore } from "./sessions/sessionUnreadStore.js";
import { ProjectScopedSpawnTargetResolver } from "./sessions/spawnTargetResolver.js";
import { ProjectService } from "./projects/projectService.js";
import { ProjectStore } from "./storage/projectStore.js";
import { WorkspaceService } from "./workspaces/workspaceService.js";
import { sessiondSocketPath } from "../sessiond/config.js";
import { TerminalService } from "./terminals/terminalService.js";
import { registerTerminalRoutes } from "./terminals/terminalRoutes.js";
import { getPiWebRuntimeComponent } from "./piWebStatus.js";
import { SESSIOND_RUNTIME_CAPABILITIES } from "../shared/capabilities.js";
import { agentSessionDirEnvKeys, effectivePiWebConfig, maxUploadBytes } from "../config.js";
import { createActiveAgentProfileDescriptor } from "../sessiond/activeAgentProfile.js";
import { runSessionDaemonStartup } from "./sessiond/sessionDaemonStartup.js";
const daemonEnvironment: NodeJS.ProcessEnv = Object.freeze({ ...process.env });
const { config } = effectivePiWebConfig({ env: daemonEnvironment });
const activeAgentProfile = createActiveAgentProfileDescriptor({
command: config.agent.command,
dir: config.agent.dir,
sessionDirEnvKeys: agentSessionDirEnvKeys(config.agent.command),
});
const app = Fastify({ logger: true, bodyLimit: maxUploadBytes(daemonEnvironment, config) });
await app.register(fastifyWebsocket);
await runSessionDaemonStartup({
logger: app.log,
async createRuntime() {
const eventHub = new SessionEventHub();
const notificationStore = new SessionNotificationStore();
const unreadStore = new SessionUnreadStore({
persistence: new FileSessionUnreadPersistence(),
onPersistenceError(operation, error) {
app.log.error({ err: error, operation }, "session unread persistence failed");
},
});
await unreadStore.load();
const workspaceActivity = new WorkspaceActivityService(eventHub);
const auth = await AuthService.create({ agentDir: activeAgentProfile.dir, logger: app.log });
const spawnTargets = config.spawnSessions
? new ProjectScopedSpawnTargetResolver({ projects: new ProjectService(new ProjectStore()), workspaces: new WorkspaceService() })
: undefined;
const sessions = new PiSessionService(eventHub, {
modelRuntime: auth.runtime,
agentDir: activeAgentProfile.dir,
workspaceActivity,
logger: app.log,
...(spawnTargets === undefined ? {} : { spawnTargets }),
subsessionsEnabled: spawnTargets !== undefined && config.subsessions,
notificationStore,
unreadStore,
sessionManager: createPiSessionManagerGateway({
agentDir: activeAgentProfile.dir,
env: daemonEnvironment,
sessionDirEnvKeys: activeAgentProfile.sessionDirEnvKeys,
}),
});
auth.subscribe((change) => { sessions.applyAuthChange(change); });
// PI WEB only supports globally configured providers: reject every
// extension provider registration against the shared daemon-wide runtime.
installGlobalProviderPolicy(auth.runtime, (providerId) => { sessions.noteRejectedProviderRegistration(providerId); });
const terminals = new TerminalService(eventHub, workspaceActivity);
const runtimeComponent = Object.freeze({
...getPiWebRuntimeComponent("sessiond", SESSIOND_RUNTIME_CAPABILITIES),
activeAgentProfile,
});
return { eventHub, workspaceActivity, auth, sessions, terminals, unreadStore, activeAgentProfile, runtimeComponent };
},
registerRoutes({ eventHub, workspaceActivity, auth, sessions, terminals, runtimeComponent }) {
registerWorkspaceActivityRoutes(app, workspaceActivity);
registerAuthRoutes(app, auth);
registerSessionRoutes(app, sessions, eventHub);
registerTerminalRoutes(app, terminals);
app.get("/health", () => ({
ok: true,
activeSessions: sessions.activeCount(),
checkedAt: new Date().toISOString(),
version: {
component: runtimeComponent.component,
label: runtimeComponent.label,
...(runtimeComponent.runtimeVersion === undefined ? {} : { runtimeVersion: runtimeComponent.runtimeVersion }),
stale: false,
available: runtimeComponent.available,
},
}));
app.get("/runtime", () => runtimeComponent);
},
async listen({ auth, sessions, terminals, unreadStore }) {
let shuttingDown = false;
async function shutdown(signal: NodeJS.Signals): Promise<void> {
if (shuttingDown) return;
shuttingDown = true;
app.log.info({ signal }, "shutting down session daemon");
const attempt = async (operation: string, run: () => void | Promise<void>): Promise<void> => {
try {
await run();
} catch (error: unknown) {
process.exitCode = 1;
app.log.error({ err: error, operation }, "session daemon shutdown operation failed");
}
};
await attempt("dispose terminals", () => { terminals.dispose(); });
await attempt("dispose auth", () => { auth.dispose(); });
await attempt("dispose sessions", () => sessions.dispose());
await attempt("flush session unread state", () => unreadStore.flush());
await attempt("close server", () => app.close());
}
process.once("SIGINT", (signal) => { void shutdown(signal); });
process.once("SIGTERM", (signal) => { void shutdown(signal); });
const portValue = daemonEnvironment["PI_WEB_SESSIOND_PORT"];
const port = portValue !== undefined && portValue !== "" ? Number(portValue) : undefined;
const host = daemonEnvironment["PI_WEB_SESSIOND_HOST"] ?? "127.0.0.1";
if (port !== undefined) {
await app.listen({ port, host });
} else {
const path = sessiondSocketPath();
await mkdir(dirname(path), { recursive: true });
await rm(path, { force: true });
await app.listen({ path });
process.on("exit", () => void rm(path, { force: true }));
}
},
});