diff --git a/src/server/sessiond.ts b/src/server/sessiond.ts index 947ee27..8703dba 100644 --- a/src/server/sessiond.ts +++ b/src/server/sessiond.ts @@ -7,7 +7,7 @@ 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, learnGlobalExtensionProviderIds } from "./sessions/globalProviderPolicy.js"; +import { bootstrapAndFreezeGlobalExtensionProviders } from "./sessions/globalProviderPolicy.js"; import { registerAuthRoutes } from "./sessions/authRoutes.js"; import { PiSessionService } from "./sessions/piSessionService.js"; import { createPiSessionManagerGateway } from "./sessions/piSessionManagerGateway.js"; @@ -51,6 +51,10 @@ await runSessionDaemonStartup({ await unreadStore.load(); const workspaceActivity = new WorkspaceActivityService(eventHub); const auth = await AuthService.create({ agentDir: activeAgentProfile.dir, logger: app.log }); + // Capture providers registered by global extensions while the runtime is + // still mutable, then freeze every later extension-provider mutation before + // any real session can load project resources. + await bootstrapAndFreezeGlobalExtensionProviders(auth.runtime, activeAgentProfile.dir, app.log); const spawnTargets = config.spawnSessions ? new ProjectScopedSpawnTargetResolver({ projects: new ProjectService(new ProjectStore()), workspaces: new WorkspaceService() }) : undefined; @@ -70,12 +74,6 @@ await runSessionDaemonStartup({ }), }); auth.subscribe((change) => { sessions.applyAuthChange(change); }); - // PI WEB providers come from global sources only. Learn which providers - // the agent directory's global extensions register (they are identical - // for every session, hence daemon-safe), then reject every other - // extension provider registration against the shared daemon-wide runtime. - const globalExtensionProviderIds = await learnGlobalExtensionProviderIds(auth.runtime, activeAgentProfile.dir); - installGlobalProviderPolicy(auth.runtime, globalExtensionProviderIds, (providerId) => { sessions.noteRejectedProviderRegistration(providerId); }); const terminals = new TerminalService(eventHub, workspaceActivity); const runtimeComponent = Object.freeze({ ...getPiWebRuntimeComponent("sessiond", SESSIOND_RUNTIME_CAPABILITIES), diff --git a/src/server/sessions/globalProviderPolicy.test.ts b/src/server/sessions/globalProviderPolicy.test.ts index 8abbb47..4b536e0 100644 --- a/src/server/sessions/globalProviderPolicy.test.ts +++ b/src/server/sessions/globalProviderPolicy.test.ts @@ -1,27 +1,62 @@ -import { describe, expect, it } from "vitest"; -import type { ModelRuntime } from "@earendil-works/pi-coding-agent"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; import type { Provider } from "@earendil-works/pi-ai"; -import { installGlobalProviderPolicy, providerRejectionMessage } from "./globalProviderPolicy.js"; -import { createTestModelRuntime, TEST_MODEL_ID, TEST_MODEL_PROVIDER } from "./piSessionService.testSupport.js"; +import { + bootstrapAndFreezeGlobalExtensionProviders, + type GlobalProviderBootstrapLogger, +} from "./globalProviderPolicy.js"; +import { + createTestModelRuntime, + TEST_MODEL_ID, + TEST_MODEL_PROVIDER, +} from "./piSessionService.testSupport.js"; -/** - * The shim permanently mutates the runtime instance it is installed on, so - * every test builds a dedicated runtime rather than touching the shared - * `testModelRuntime` from testSupport. - */ -async function policyRuntime( - allowedExtensionProviderIds: ReadonlySet = new Set(), -): Promise<{ runtime: ModelRuntime; rejections: string[] }> { - const runtime = await createTestModelRuntime(); - const rejections: string[] = []; - installGlobalProviderPolicy(runtime, allowedExtensionProviderIds, (providerId) => { rejections.push(providerId); }); - return { runtime, rejections }; +interface LogEntry { + level: "error" | "info" | "warn"; + details: Record; + message: string; } -function nativeProvider(providerId: string): Provider { +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((path) => rm(path, { recursive: true, force: true }))); +}); + +async function tempDir(prefix: string): Promise { + const path = await mkdtemp(join(tmpdir(), prefix)); + tempDirs.push(path); + return path; +} + +async function agentDirWithExtension(source: string): Promise { + const agentDir = await tempDir("pi-web-global-provider-unit-"); + await mkdir(join(agentDir, "extensions"), { recursive: true }); + await writeFile(join(agentDir, "extensions", "provider.js"), source); + return agentDir; +} + +function capturingLogger(): { entries: LogEntry[]; logger: GlobalProviderBootstrapLogger } { + const entries: LogEntry[] = []; + const record = (level: LogEntry["level"], details: Record, message: string): void => { + entries.push({ level, details, message }); + }; + return { + entries, + logger: { + error: (details, message) => { record("error", details, message); }, + info: (details, message) => { record("info", details, message); }, + warn: (details, message) => { record("warn", details, message); }, + }, + }; +} + +function nativeProvider(providerId: string, name = providerId): Provider { return { id: providerId, - name: providerId, + name, auth: { apiKey: { name: `${providerId} API key`, @@ -34,82 +69,122 @@ function nativeProvider(providerId: string): Provider { }; } -describe("installGlobalProviderPolicy", () => { - it("rejects non-allowed registrations and records each rejection", async () => { - const { runtime, rejections } = await policyRuntime(); +function registerProjectConfigProvider(runtime: Awaited>): void { + runtime.registerProvider("project-config", { + name: "Project Config", + baseUrl: "https://project-secret.example.com", + apiKey: "project-secret-api-key", + api: "openai-completions", + models: [{ + id: "project-model", + name: "Project Model", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 8_192, + maxTokens: 1_024, + }], + }); +} - runtime.registerProvider("acme", { baseUrl: "https://acme.example.com" }); - runtime.registerProvider("acme", { baseUrl: "https://acme-two.example.com" }); - runtime.registerProvider("other", {}); +describe("bootstrapAndFreezeGlobalExtensionProviders", () => { + it("captures the global baseline before making every later provider mutation a no-op", async () => { + const agentDir = await agentDirWithExtension(` + export default function (pi) { + pi.registerProvider("global-config", { + name: "Global Config", + baseUrl: "https://global.example.com", + apiKey: "$GLOBAL_PROVIDER_KEY", + api: "openai-completions", + models: [{ + id: "global-model", + name: "Global Model", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 8192, + maxTokens: 1024 + }] + }); + } + `); + const runtime = await createTestModelRuntime(); + const builtInModel = runtime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID); + expect(builtInModel).toBeDefined(); + const { entries, logger } = capturingLogger(); - expect(rejections).toEqual(["acme", "acme", "other"]); + await bootstrapAndFreezeGlobalExtensionProviders(runtime, agentDir, logger); + + const baselineConfig = runtime.getRegisteredProviderConfig("global-config"); + expect(baselineConfig).toMatchObject({ baseUrl: "https://global.example.com" }); + expect(runtime.getModel("global-config", "global-model")).toBeDefined(); + expect(entries).toContainEqual({ + level: "info", + details: { context: "global-provider-bootstrap", providerIds: ["global-config"] }, + message: "global extension provider baseline bootstrapped and frozen", + }); + + for (let attempt = 0; attempt < 2; attempt += 1) { + runtime.registerProvider("global-config", { + baseUrl: "https://replacement-secret.example.com", + headers: { Authorization: "replacement-secret-token" }, + }); + runtime.registerNativeProvider(nativeProvider("global-config", "native-secret-name")); + runtime.unregisterProvider("global-config"); + registerProjectConfigProvider(runtime); + runtime.registerNativeProvider(nativeProvider("project-native", "project-native-secret-name")); + runtime.unregisterProvider("project-only"); + } + + expect(runtime.getRegisteredProviderIds()).toEqual(["global-config"]); + expect(runtime.getRegisteredProviderConfig("global-config")).toBe(baselineConfig); + expect(runtime.getRegisteredNativeProvider("global-config")).toBeUndefined(); + expect(runtime.getRegisteredProviderConfig("project-config")).toBeUndefined(); + expect(runtime.getRegisteredNativeProvider("project-native")).toBeUndefined(); + expect(runtime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID)).toBe(builtInModel); + + const ignoredMutations = entries + .filter((entry) => entry.message === "ignored provider mutation after global bootstrap") + .map((entry) => entry.details); + expect(ignoredMutations).toEqual([ + { context: "global-provider-bootstrap", operation: "registerProvider", providerId: "global-config" }, + { context: "global-provider-bootstrap", operation: "registerNativeProvider", providerId: "global-config" }, + { context: "global-provider-bootstrap", operation: "unregisterProvider", providerId: "global-config" }, + { context: "global-provider-bootstrap", operation: "registerProvider", providerId: "project-config" }, + { context: "global-provider-bootstrap", operation: "registerNativeProvider", providerId: "project-native" }, + { context: "global-provider-bootstrap", operation: "unregisterProvider", providerId: "project-only" }, + ]); + expect(JSON.stringify(ignoredMutations)).not.toContain("secret"); + }); + + it("logs non-fatal Pi bootstrap diagnostics and still freezes the runtime", async () => { + const agentDir = await agentDirWithExtension(` + export default function (pi) { + pi.registerProvider("broken-provider", { streamSimple() {} }); + } + `); + const runtime = await createTestModelRuntime(); + const { entries, logger } = capturingLogger(); + + await bootstrapAndFreezeGlobalExtensionProviders(runtime, agentDir, logger); + + const diagnosticEntry = entries.find((entry) => entry.details["diagnosticType"] === "error"); + expect(diagnosticEntry?.level).toBe("error"); + expect(diagnosticEntry?.details["context"]).toBe("global-provider-bootstrap"); + expect(diagnosticEntry?.message).toBe("global extension provider bootstrap diagnostic"); + expect(diagnosticEntry?.details["diagnostic"]) + .toEqual(expect.stringContaining('"api" is required when registering streamSimple')); + + runtime.registerProvider("after-diagnostic", {}); expect(runtime.getRegisteredProviderIds()).toEqual([]); - expect(runtime.getRegisteredProviderConfig("acme")).toBeUndefined(); - }); - - it("lets allowed (global-extension) providers through to the runtime", async () => { - const { runtime, rejections } = await policyRuntime(new Set(["tensorx"])); - - runtime.registerProvider("tensorx", { baseUrl: "https://tensorx.example.com" }); - runtime.registerProvider("acme", { baseUrl: "https://acme.example.com" }); - - expect(rejections).toEqual(["acme"]); - expect(runtime.getRegisteredProviderIds()).toEqual(["tensorx"]); - expect(runtime.getRegisteredProviderConfig("tensorx")).toEqual({ baseUrl: "https://tensorx.example.com" }); - }); - - it("applies the same allow rule to native provider registrations", async () => { - const { runtime, rejections } = await policyRuntime(new Set(["native-global"])); - - runtime.registerNativeProvider(nativeProvider("native-global")); - runtime.registerNativeProvider(nativeProvider("native-project")); - - expect(rejections).toEqual(["native-project"]); - expect(runtime.getRegisteredProviderIds()).toEqual(["native-global"]); - expect(runtime.getRegisteredNativeProvider("native-global")).toBeDefined(); - }); - - it("unregisters only allowed providers; other unregisters are a no-op", async () => { - const { runtime, rejections } = await policyRuntime(new Set(["tensorx"])); - runtime.registerProvider("tensorx", { baseUrl: "https://tensorx.example.com" }); - - runtime.unregisterProvider("acme"); - runtime.unregisterProvider(TEST_MODEL_PROVIDER); - expect(runtime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID)).toBeDefined(); - - runtime.unregisterProvider("tensorx"); - - expect(rejections).toEqual([]); - expect(runtime.getRegisteredProviderIds()).toEqual([]); - expect(runtime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID)).toBeDefined(); - }); - - it("keeps global (built-in) providers resolvable after rejections", async () => { - const { runtime } = await policyRuntime(); - const before = runtime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID); - - runtime.registerProvider("acme", {}); - - expect(before).toBeDefined(); - expect(runtime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID)).toBe(before); - }); -}); - -describe("providerRejectionMessage", () => { - it("names the provider and the loading workspace when the cwd is known", () => { - const message = providerRejectionMessage("acme", "/workspace/project"); - - expect(message).toContain('Provider "acme"'); - expect(message).toContain("in /workspace/project"); - expect(message).toContain("PI WEB providers must come from global configuration"); - expect(message).toContain("globally installed extension"); - expect(message).toContain("All other extension features are unaffected."); - }); - - it("falls back to a generic origin for late registrations without a cwd", () => { - const message = providerRejectionMessage("acme"); - - expect(message).toContain('Provider "acme" registered by an extension was ignored'); - expect(message).not.toContain(" in "); + expect(entries).toContainEqual({ + level: "info", + details: { + context: "global-provider-bootstrap", + operation: "registerProvider", + providerId: "after-diagnostic", + }, + message: "ignored provider mutation after global bootstrap", + }); }); }); diff --git a/src/server/sessions/globalProviderPolicy.ts b/src/server/sessions/globalProviderPolicy.ts index 05959c2..ebad281 100644 --- a/src/server/sessions/globalProviderPolicy.ts +++ b/src/server/sessions/globalProviderPolicy.ts @@ -1,110 +1,125 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { createAgentSessionServices, type ModelRuntime } from "@earendil-works/pi-coding-agent"; +import { + createAgentSessionServices, + type AgentSessionRuntimeDiagnostic, + type AgentSessionServices, + type ModelRuntime, +} from "@earendil-works/pi-coding-agent"; -/** - * PI WEB providers come from global sources only: Pi built-ins, agent-dir - * `models.json`, environment credentials, and providers registered by global - * (agent-dir) extensions. Provider registrations from project extensions are - * rejected — the user is told, and everything else the extension does keeps - * working. - * - * Why: all sessions share one daemon-wide {@link ModelRuntime}. Global - * extensions are identical for every session, so their providers are - * daemon-wide consistent; project extensions differ per workspace, and letting - * them mutate the shared runtime corrupts the provider set of every other - * concurrent session (issue #76). - * - * Mechanism: this deliberately shadows the `registerProvider` / - * `registerNativeProvider` / `unregisterProvider` instance methods because Pi - * 0.81.1 offers no registration hook with extension attribution (the internal - * drain, the bind-time flush, and the late `pi.registerProvider` path all - * reach the runtime through call-time property lookup with the extension path - * already dropped, so instance shadowing intercepts them identically). The - * acceptance suite that exercises the load-time and late paths is the - * tripwire: if a Pi upgrade changes these internals, those tests fail loudly - * and this shim must be revisited. - * - * Attribution: Pi drops the registering extension's path before calls reach - * the runtime, so the shim cannot tell global from project extensions per - * call. Instead the daemon learns once, at startup, which provider ids global - * extensions register ({@link learnGlobalExtensionProviderIds}) and allows - * exactly those. This is not a security boundary — extensions run in-process - * with full trust, and a project extension re-registering an allowed id would - * pass. It is a guard against accidental cross-workspace leakage. - */ -export function installGlobalProviderPolicy( - runtime: ModelRuntime, - allowedExtensionProviderIds: ReadonlySet, - onRejection: (providerId: string) => void, -): void { - const registerProvider = runtime.registerProvider.bind(runtime); - const registerNativeProvider = runtime.registerNativeProvider.bind(runtime); - const unregisterProvider = runtime.unregisterProvider.bind(runtime); - - runtime.registerProvider = (providerId, config) => { - if (allowedExtensionProviderIds.has(providerId)) { - registerProvider(providerId, config); - return; - } - // Swallow: the shared runtime is never mutated, the rejection is surfaced. - onRejection(providerId); - }; - runtime.registerNativeProvider = (provider) => { - if (allowedExtensionProviderIds.has(provider.id)) { - registerNativeProvider(provider); - return; - } - onRejection(provider.id); - }; - runtime.unregisterProvider = (providerId) => { - if (allowedExtensionProviderIds.has(providerId)) { - unregisterProvider(providerId); - return; - } - // No-op: rejected registrations never reached the runtime, so there is - // never anything of theirs to unregister. - }; +/** Structured logging boundary supplied by the session daemon. */ +export interface GlobalProviderBootstrapLogger { + error(details: Record, message: string): void; + info(details: Record, message: string): void; + warn(details: Record, message: string): void; } -/** - * Learn which provider ids the agent directory's global extensions register. - * - * Loads extensions once for a guaranteed-empty temporary cwd — so only global - * (agent-dir) extensions load — against the daemon's shared runtime, and - * returns the ids that appeared. Those registrations stay on the runtime: they - * are the daemon baseline, re-registered identically on every session load. - * Must run before {@link installGlobalProviderPolicy} is installed. - * - * Caveat: global extension code runs one extra time at daemon startup. Any - * commands/tools it registers land on the discarded scratch loader. - */ -export async function learnGlobalExtensionProviderIds( - runtime: ModelRuntime, - agentDir: string, -): Promise> { - const before = new Set(runtime.getRegisteredProviderIds()); +type ProviderMutationOperation = "registerNativeProvider" | "registerProvider" | "unregisterProvider"; +type ProviderMutationMethods = Pick; + +const LOG_CONTEXT = "global-provider-bootstrap"; + +async function loadGlobalExtensionServices(runtime: ModelRuntime, agentDir: string): Promise { const scratchCwd = await mkdtemp(join(tmpdir(), "pi-web-global-ext-")); try { - await createAgentSessionServices({ cwd: scratchCwd, agentDir, modelRuntime: runtime }); + return await createAgentSessionServices({ cwd: scratchCwd, agentDir, modelRuntime: runtime }); } finally { await rm(scratchCwd, { recursive: true, force: true }); } - const learned = new Set(); - for (const providerId of runtime.getRegisteredProviderIds()) { - if (!before.has(providerId)) learned.add(providerId); +} + +function logBootstrapDiagnostic( + logger: GlobalProviderBootstrapLogger, + diagnostic: AgentSessionRuntimeDiagnostic, +): void { + const details = { + context: LOG_CONTEXT, + diagnosticType: diagnostic.type, + diagnostic: diagnostic.message, + }; + if (diagnostic.type === "error") { + logger.error(details, "global extension provider bootstrap diagnostic"); + } else if (diagnostic.type === "warning") { + logger.warn(details, "global extension provider bootstrap diagnostic"); + } else { + logger.info(details, "global extension provider bootstrap diagnostic"); + } +} + +function freezeProviderMutations(runtime: ModelRuntime, logger: GlobalProviderBootstrapLogger): void { + const originalMethods: ProviderMutationMethods = { + registerNativeProvider: runtime.registerNativeProvider.bind(runtime), + registerProvider: runtime.registerProvider.bind(runtime), + unregisterProvider: runtime.unregisterProvider.bind(runtime), + }; + const loggedProviderIds: Record> = { + registerNativeProvider: new Set(), + registerProvider: new Set(), + unregisterProvider: new Set(), + }; + const logIgnoredMutation = (operation: ProviderMutationOperation, providerId: string): void => { + const loggedIds = loggedProviderIds[operation]; + if (loggedIds.has(providerId)) return; + loggedIds.add(providerId); + logger.info( + { context: LOG_CONTEXT, operation, providerId }, + "ignored provider mutation after global bootstrap", + ); + }; + const frozenMethods: ProviderMutationMethods = { + registerProvider(providerId) { + logIgnoredMutation("registerProvider", providerId); + }, + registerNativeProvider(provider) { + logIgnoredMutation("registerNativeProvider", provider.id); + }, + unregisterProvider(providerId) { + logIgnoredMutation("unregisterProvider", providerId); + }, + }; + + try { + Object.assign(runtime, frozenMethods); + } catch (error: unknown) { + Object.assign(runtime, originalMethods); + throw error; } - return learned; } /** - * User-facing wording for a rejected registration. `cwd` is known only for - * rejections raised while loading a workspace's services; late registrations - * from session event handlers carry no attribution beyond the provider id. + * Load global extensions once against the shared model runtime, then make its + * extension-provider baseline immutable for the rest of the daemon lifetime. + * All sessions share this runtime, so accepting project-dependent mutations + * would leak provider configuration across workspaces. This is an accidental + * contamination guard, not a sandbox for otherwise trusted extensions. + * + * The temporary cwd is guaranteed to be empty, so Pi discovers agent-dir + * extensions without loading project resources. Documented initialization-time + * config and native registrations therefore reach the runtime through Pi's + * public service factory. Pi exposes no provider-freeze hook, so the daemon + * deliberately shadows the three public instance mutation methods afterward; + * every registration replay or later call is then a logged no-op. */ -export function providerRejectionMessage(providerId: string, cwd?: string): string { - const origin = cwd === undefined ? "registered by an extension" : `registered by an extension in ${cwd}`; - return `Provider "${providerId}" ${origin} was ignored — PI WEB providers must come from global configuration ` - + "(agent-dir models.json) or a globally installed extension. All other extension features are unaffected."; +export async function bootstrapAndFreezeGlobalExtensionProviders( + runtime: ModelRuntime, + agentDir: string, + logger: GlobalProviderBootstrapLogger, +): Promise { + const services = await loadGlobalExtensionServices(runtime, agentDir); + const providerIds = Object.freeze([...runtime.getRegisteredProviderIds()].sort()); + + freezeProviderMutations(runtime, logger); + + for (const diagnostic of services.diagnostics) logBootstrapDiagnostic(logger, diagnostic); + for (const extensionError of services.resourceLoader.getExtensions().errors) { + logger.error( + { context: LOG_CONTEXT, error: extensionError.error }, + "global extension failed during provider bootstrap", + ); + } + logger.info( + { context: LOG_CONTEXT, providerIds }, + "global extension provider baseline bootstrapped and frozen", + ); } diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index 3fd4617..720d949 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -65,7 +65,6 @@ import { type SessionNotificationMutation, } from "./sessionNotificationStore.js"; import { plainTextTheme } from "./plainTextTheme.js"; -import { providerRejectionMessage } from "./globalProviderPolicy.js"; import { SessionUnreadStore, type SessionUnreadMutation } from "./sessionUnreadStore.js"; /** @@ -581,38 +580,14 @@ export function createPiWebCustomToolDefinitions( ]; } -/** - * Correlates provider registrations rejected by the global provider policy - * with the services load that triggered them. `begin` returns the mutable list - * the policy listener appends to while the load is in flight; `end` detaches - * it. Implemented by {@link PiSessionService}, which owns the in-flight set. - */ -interface ProviderRejectionTracker { - begin(): string[]; - end(rejectedProviderIds: string[]): void; -} - function createDefaultRuntimeFactory( modelRuntime: ModelRuntime, sessionManagers: Pick, spawn?: SpawnSessionFn, subsessions?: SubsessionToolDeps, - providerRejections?: ProviderRejectionTracker, ): PiWebCreateAgentSessionRuntimeFactory { return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => { - const rejectedProviderIds = providerRejections?.begin(); - let services: AgentSessionServices; - try { - services = await createAgentSessionServices({ cwd, agentDir, modelRuntime }); - } finally { - if (rejectedProviderIds !== undefined) providerRejections?.end(rejectedProviderIds); - } - // Surface each provider the policy rejected during this load as a session - // warning, through the same diagnostics pipeline as other runtime setup - // issues. The registration was ignored; nothing else about the load changes. - for (const providerId of new Set(rejectedProviderIds ?? [])) { - services.diagnostics.push({ type: "warning", message: providerRejectionMessage(providerId, cwd) }); - } + const services: AgentSessionServices = await createAgentSessionServices({ cwd, agentDir, modelRuntime }); const resolvedDelegationToolsEnabled = delegationToolsEnabled ?? await sessionAllowsDelegationTools(sessionManager, sessionManagers); const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions); @@ -729,8 +704,6 @@ export class PiSessionService implements SessionRouteService { private readonly now: () => Date; private readonly notificationStore: SessionNotificationStore; private readonly notificationGenerationBySession = new WeakMap(); - /** Rejection lists of in-flight services loads; see {@link noteRejectedProviderRegistration}. */ - private readonly pendingProviderRejectionLoads = new Set(); private readonly unreadStore: SessionUnreadStore; private readonly unreadPublicationRetryInitialMs: number; private readonly pendingUnreadMutations: SessionUnreadMutation[] = []; @@ -769,16 +742,6 @@ export class PiSessionService implements SessionRouteService { check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile), read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile), }, - { - begin: () => { - const rejectedProviderIds: string[] = []; - this.pendingProviderRejectionLoads.add(rejectedProviderIds); - return rejectedProviderIds; - }, - end: (rejectedProviderIds) => { - this.pendingProviderRejectionLoads.delete(rejectedProviderIds); - }, - }, ); this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime; this.workspaceActivity = deps.workspaceActivity; @@ -823,34 +786,6 @@ export class PiSessionService implements SessionRouteService { return this.notificationStore.catalogSnapshot(); } - /** - * Handle a provider registration rejected by the daemon-wide global provider - * policy (installed on the shared model runtime by sessiond). - * - * A rejection raised while at least one services load is in flight is - * recorded onto every in-flight load, which surfaces it as that session's - * load warning. The shim cannot attribute a registration to a specific - * extension or load, so overlapping loads may each report the same provider - * id; that over-reports but never drops a rejection. - * - * With no load in flight this is a late registration from a bound session's - * extension event handler. It cannot be attributed to a session either, so - * the notice is broadcast to every active session's notification inbox. - */ - noteRejectedProviderRegistration(providerId: string): void { - if (this.pendingProviderRejectionLoads.size > 0) { - for (const load of this.pendingProviderRejectionLoads) load.push(providerId); - return; - } - const message = providerRejectionMessage(providerId); - for (const record of this.active.values()) { - const generation = this.notificationGenerationBySession.get(record.runtime.session); - if (generation === undefined) continue; - const added = this.notificationStore.addNotification(generation, message, "warning"); - this.publishNotificationMutations(added.mutations); - } - } - async unreadCatalog(): Promise { await this.publishUnreadMutations([]); return this.unreadStore.durableCatalogSnapshot();