Skip to content
Source
Sign inSign up

ProviderInstanceRegistryHydration.ts

221 lines9.2 KB

1import {
2 defaultInstanceIdForDriver,
3 ProviderDriverKind,
4 ProviderInstanceId,
5 providerInstanceConfigEnabledFlag,
6 resolveProviderInstanceEnabled,
7 type ProviderInstanceConfig,
8 type ProviderInstanceConfigMap,
9 ServerSettings,
10} from "@t3tools/contracts";
11import * as NodeUtil from "node:util";
12​
13import * as HostProcess from "@t3tools/shared/HostProcess";
14import type * as SecureRandom from "effect/crypto/SecureRandom";
15import * as Effect from "effect/Effect";
16import * as FileSystem from "effect/FileSystem";
17import * as Layer from "effect/Layer";
18import type * as SqlClient from "effect/sql/SqlClient";
19import * as Path from "effect/Path";
20import * as Stream from "effect/Stream";
21​
22import * as ManagedProviders from "../policy/managedProviders.ts";
23import * as Settings from "../serverSettings.ts";
24import * as SecretResolver from "../secrets/SecretResolver.ts";
25import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "./builtInDrivers.ts";
26import * as ProviderInstanceRegistry from "./ProviderInstanceRegistry.ts";
27import * as ProviderInstanceRegistryMutator from "./ProviderInstanceRegistryMutator.ts";
28import * as ProviderOrchestrationAdapterInfrastructure from "./ProviderOrchestrationAdapterInfrastructure.ts";
29import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport";
30import * as ProviderHostLive from "./ProviderHostLive.ts";
31import type * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
32import type * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
33import * as ServerConfig from "../config.ts";
34import type * as PortScanner from "../preview/PortScanner.ts";
35​
36type ProviderInstanceRegistryHydrationEnv =
37 | Exclude<
38 BuiltInDriversEnv,
39 | ProviderOrchestrationAdapterInfrastructure.ProviderOrchestrationAdapterInfrastructure
40 | AcpRegistrySupport.AcpRegistryCatalog
41 | ProviderHost.ProviderHost
42 >
43 | Settings.ServerSettingsService
44 | BackgroundPolicy.BackgroundPolicy
45 | ServerConfig.ServerConfig
46 | Path.Path
47 | FileSystem.FileSystem
48 // Secret provider reads.
49 | SecureRandom.SecureRandom
50 // The Claude runner counts its CLIs as running agents for port discovery.
51 | PortScanner.PortDiscovery
52 // Adapter ids read which subagent threads predate short ids.
53 | SqlClient.SqlClient;
54​
55export const deriveProviderInstanceConfigMap = (
56 settings: ServerSettings,
57): ProviderInstanceConfigMap => {
58 const merged: Record<string, ProviderInstanceConfig> = { ...settings.providerInstances };
59​
60 for (const driver of BUILT_IN_DRIVERS) {
61 if (driver.metadata.hasDefaultInstance === false) continue;
62 const instanceId = defaultInstanceIdForDriver(driver.driverKind);
63 if (instanceId in merged) continue;
64 merged[instanceId] = { driver: driver.driverKind };
65 }
66​
67 // Probe project-enabled instances even if disabled machine-wide; project threads can still route to them.
68 for (const overrides of Object.values(settings.projectSettingsOverrides)) {
69 for (const [rawInstanceId, projectOverride] of Object.entries(
70 overrides?.providerInstances ?? {},
71 )) {
72 if (projectOverride.enabled !== true) continue;
73 const instanceId = rawInstanceId as ProviderInstanceId;
74 const existing = merged[instanceId];
75 if (existing === undefined || resolveProviderInstanceEnabled(existing)) continue;
76 merged[instanceId] = {
77 ...existing,
78 enabled: true,
79 ...(providerInstanceConfigEnabledFlag(existing.config) === false
80 ? { config: { ...(existing.config as Record<string, unknown>), enabled: true } }
81 : {}),
82 };
83 }
84 }
85​
86 return merged as ProviderInstanceConfigMap;
87};
88​
89export const DEV_REPLAY_INSTANCE_ID = ProviderInstanceId.make("devReplay");
90​
91/**
92 * Dev servers run from source keep a free ACP agent that replays recorded turns, so UI checks
93 * need no paid model. Bundled servers, such as desktop dev, have no agent script and skip it.
94 */
95export const seedDevReplayInstance = Effect.fn(
96 "ProviderInstanceRegistryHydration.seedDevReplayInstance",
97)(function* () {
98 const config = yield* ServerConfig.ServerConfig;
99 if (config.devUrl === undefined) return;
100 const path = yield* Path.Path;
101 const agentPath = yield* path.fromFileUrl(
102 new URL("../../scripts/dev-replay-agent.ts", import.meta.url),
103 );
104 if (!(yield* (yield* FileSystem.FileSystem).exists(agentPath))) return;
105 const commandPath = yield* HostProcess.ExecutablePath;
106 const serverSettings = yield* Settings.ServerSettingsService;
107 const existing = (yield* serverSettings.getSettings).providerInstances[DEV_REPLAY_INSTANCE_ID];
108 const existingConfig = existing?.config as Record<string, unknown> | undefined;
109 // Rewritten only when stale: the node and checkout paths can move.
110 if (
111 existingConfig?.commandPath === commandPath &&
112 (existingConfig.commandArgs as ReadonlyArray<unknown> | undefined)?.[0] === agentPath
113 ) {
114 return;
115 }
116 // Keeps user edits, such as turning the provider off.
117 const instance: ProviderInstanceConfig = {
118 displayName: "Replay (dev)",
119 ...existing,
120 driver: ProviderDriverKind.make("acpRegistry"),
121 config: { ...existingConfig, source: "local", commandPath, commandArgs: [agentPath] },
122 };
123 yield* serverSettings.updateProviderInstance({
124 operation: "upsert",
125 instanceId: DEV_REPLAY_INSTANCE_ID,
126 instance,
127 });
128});
129​
130// A label is display-only, so only kind and config can change what a vault returns.
131const vaultConfigs = (settings: ServerSettings) =>
132 Object.entries(settings.secretProviders).map(([id, { kind, config }]) => [id, kind, config]);
133​
134export const layerSettingsWatcher = (
135 settingsChanges: Stream.Stream<ServerSettings>,
136 // The settings the registry was built from: a change queued during startup differs from these.
137 initialSettings: ServerSettings | undefined,
138) =>
139 Layer.effectDiscard(
140 Effect.gen(function* () {
141 const mutator = yield* ProviderInstanceRegistryMutator.ProviderInstanceRegistryMutator;
142 const serverSettings = yield* Settings.ServerSettingsService;
143 let secretProviders = initialSettings && vaultConfigs(initialSettings);
144 yield* settingsChanges.pipe(
145 Stream.runForEach(() =>
146 // The subscription predates the seed read, so a queued change can be
147 // older than the seed. A failed current read must leave the registry intact.
148 serverSettings.getSettings.pipe(
149 Effect.flatMap((current) =>
150 Effect.gen(function* () {
151 yield* mutator.reconcile(deriveProviderInstanceConfigMap(current));
152 // Reconcile leaves instances parked on an unreadable secret alone,
153 // and a corrected secret provider is what can make them readable.
154 const next = vaultConfigs(current);
155 if (!NodeUtil.isDeepStrictEqual(secretProviders, next)) {
156 secretProviders = next;
157 yield* mutator.reloadSecrets();
158 }
159 }),
160 ),
161 Effect.catchCause((cause) =>
162 Effect.logError("ProviderInstanceRegistry reconcile failed", cause),
163 ),
164 ),
165 ),
166 Effect.forkScoped,
167 );
168 }),
169 );
170​
171export const layer: Layer.Layer<
172 | ProviderInstanceRegistry.ProviderInstanceRegistry
173 | ProviderInstanceRegistryMutator.ProviderInstanceRegistryMutator,
174 never,
175 ProviderInstanceRegistryHydrationEnv
176> = Layer.unwrap(
177 Effect.gen(function* () {
178 const serverSettings = yield* Settings.ServerSettingsService;
179 yield* seedDevReplayInstance().pipe(
180 Effect.catchCause((cause) => Effect.logWarning("Dev replay agent unavailable", cause)),
181 );
182 // Subscribe before the seed read so saves during registry construction are queued.
183 const settingsChanges = yield* serverSettings.subscribeChanges;
184 const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(
185 Effect.orElseSucceed(() => undefined),
186 );
187 const initialConfigMap =
188 initialSettings === undefined
189 ? ({} as ProviderInstanceConfigMap)
190 : deriveProviderInstanceConfigMap(initialSettings);
191​
192 // Drivers launch from the host environment, minus launch overrides a policy ignores.
193 const hostEnvironment = ManagedProviders.providerHostEnvironment(
194 serverSettings.managedPolicy,
195 yield* HostProcess.Environment,
196 );
197 // Instances of drivers the policy leaves out stay listed, as unavailable, so their threads
198 // explain why instead of losing their provider.
199 const layerMutable = ProviderInstanceRegistry.layer({
200 drivers: BUILT_IN_DRIVERS,
201 configMap: initialConfigMap,
202 blockedReason: (driver) =>
203 ManagedProviders.disallowedDriverReason(serverSettings.managedPolicy, driver),
204 }).pipe(
205 Layer.provide(ProviderOrchestrationAdapterInfrastructure.layer),
206 Layer.provide(AcpRegistrySupport.layerFromHost),
207 Layer.provide(ProviderHostLive.layer),
208 Layer.provide(Layer.succeed(HostProcess.Environment, hostEnvironment)),
209 Layer.provide(SecretResolver.layer),
210 );
211​
212 return layerSettingsWatcher(settingsChanges, initialSettings).pipe(
213 Layer.provideMerge(layerMutable),
214 );
215 }),
216) as Layer.Layer<
217 | ProviderInstanceRegistry.ProviderInstanceRegistry
218 | ProviderInstanceRegistryMutator.ProviderInstanceRegistryMutator,
219 never,
220 ProviderInstanceRegistryHydrationEnv
221>;