1import {
2 officialAcpRegistryIconUrlForAgentId,
3 ProviderDriverKind,
4 resolveOfficialAcpRegistryIconUrl,
5 TextGenerationError,
6 type AcpRegistryOperationError,
7 type ServerProvider,
8 type ServerProviderModel,
9} from "@t3tools/contracts";
10import { AcpRegistrySettings } from "../settings.ts";
11import * as HostProcess from "@t3tools/shared/HostProcess";
12import { createModelCapabilities } from "@t3tools/shared/model";
13import * as Clock from "effect/Clock";
14import * as Crypto from "effect/Crypto";
15import * as DateTime from "effect/DateTime";
16import * as Effect from "effect/Effect";
17import * as Option from "effect/Option";
18import * as Ref from "effect/Ref";
19import * as Result from "effect/Result";
20import * as Schema from "effect/Schema";
21import * as Semaphore from "effect/Semaphore";
22import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
23
24import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
25import { AcpRegistryAdapterV2Driver, type AcpRegistryAdapterV2DriverEnv } from "./adapter.ts";
26import type { ProviderTextGeneration } from "@t3tools/provider-core/server/textGeneration";
27import { ProviderDriverError } from "@t3tools/provider-core/server/errors";
28import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
29import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
30import {
31 defaultProviderContinuationIdentity,
32 type ProviderDriver,
33 type ProviderInstance,
34} from "@t3tools/provider-core/server/driver";
35import { providerModelsFromSettings } from "@t3tools/provider-core/server/snapshotProbe";
36import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver";
37import {
38 haveProviderSnapshotSettingsChanged,
39 makeProviderSnapshotSettingsSource,
40 type ProviderSnapshotSettings,
41} from "@t3tools/provider-core/server/snapshotSettings";
42import {
43 type AcpRegistryAvailableCommands,
44 type AcpRegistryConfigurationProbe,
45 type AcpRegistryConfigurationProbeResult,
46 type AcpRegistryLiveConfiguration,
47 deleteAcpRegistrySession,
48 disableAcpRegistryProvider,
49 listAcpRegistrySessions,
50 listAcpRegistryProviders,
51 logoutAcpRegistry,
52 probeAcpRegistryConfiguration,
53 setAcpRegistryProvider,
54} from "./probe.ts";
55import * as AcpRegistrySupport from "./AcpRegistrySupport.ts";
56import * as AcpRegistryRuntimeCoordinator from "./AcpRegistryRuntimeCoordinator.ts";
57import * as AcpRegistryAuth from "./auth.ts";
58import * as AcpRegistryAuthenticationState from "./authenticationState.ts";
59
60const DRIVER_KIND = ProviderDriverKind.make("acpRegistry");
61const decodeSettings = Schema.decodeSync(AcpRegistrySettings);
62// How long a successful discovery probe stays valid. Periodic health
63// refreshes reuse it instead of spawning a fresh disposable ACP session;
64// failed or unauthenticated probes are never cached so a completed sign-in
65// is detected on the next refresh.
66const PROBE_SUCCESS_TTL_MS = 15 * 60 * 1_000;
67const EMPTY_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });
68const MAINTENANCE = makeManualOnlyProviderMaintenanceCapabilities({
69 provider: DRIVER_KIND,
70 packageName: null,
71});
72
73const makeUnsupportedTextGeneration = (): ProviderTextGeneration => {
74 const unsupported = (operation: string) =>
75 Effect.fail(
76 new TextGenerationError({
77 operation,
78 detail: "ACP Registry instances do not provide application text generation.",
79 }),
80 );
81 return {
82 generateCommitMessage: () => unsupported("generateCommitMessage"),
83 generatePrContent: () => unsupported("generatePrContent"),
84 generateBranchName: () => unsupported("generateBranchName"),
85 generateThreadTitle: () => unsupported("generateThreadTitle"),
86 generateReviewGuide: () => unsupported("generateReviewGuide"),
87 };
88};
89
90function modelsFromDiscovery(
91 discovery:
92 | Pick<AcpRegistryLiveConfiguration, "models" | "currentModelId" | "configOptions">
93 | undefined,
94 customModels: ReadonlyArray<string>,
95): ReadonlyArray<ServerProviderModel> {
96 const discovered = discovery?.models ?? [];
97 // Discovered session config options and modes ride on every model so the
98 // composer's generic option controls can drive them per thread.
99 const capabilities =
100 discovery === undefined || discovery.configOptions.length === 0
101 ? EMPTY_CAPABILITIES
102 : createModelCapabilities({ optionDescriptors: discovery.configOptions });
103 const builtInModels: ReadonlyArray<ServerProviderModel> =
104 discovered.length === 0
105 ? [
106 {
107 slug: "default",
108 name: "Default",
109 isCustom: false,
110 isDefault: true,
111 capabilities,
112 },
113 ]
114 : discovered.map((model) => ({
115 slug: model.id,
116 name: model.name,
117 isCustom: false,
118 ...(model.id === discovery?.currentModelId ? { isDefault: true } : {}),
119 capabilities,
120 }));
121 return providerModelsFromSettings(builtInModels, customModels, capabilities);
122}
123
124export function acpRegistrySnapshotReadiness(
125 inspection:
126 | AcpRegistrySupport.AcpRegistryInspection
127 | { readonly status: "failed"; readonly message: string },
128): Pick<ServerProvider, "installed" | "version" | "status" | "message"> {
129 switch (inspection.status) {
130 case "ready":
131 return { installed: true, version: inspection.version, status: "ready" };
132 case "unconfigured":
133 return {
134 installed: false,
135 version: null,
136 status: "warning",
137 message:
138 "Select an ACP Registry agent or configure a local ACP executable before starting a thread.",
139 };
140 case "not_found":
141 return {
142 installed: false,
143 version: null,
144 status: "error",
145 message: `ACP Registry does not contain agent '${inspection.agentId}'.`,
146 };
147 case "unsupported":
148 return {
149 installed: false,
150 version: inspection.version,
151 status: "error",
152 message: `ACP Registry agent '${inspection.agentId}' has no compatible distribution for this environment.`,
153 };
154 case "missing_runner":
155 return {
156 installed: false,
157 version: inspection.version,
158 status: "error",
159 message:
160 inspection.distribution === "local"
161 ? "Local ACP executable is not available on this environment's PATH."
162 : `ACP executable '${inspection.runner}' is not available on this environment's PATH.`,
163 };
164 case "unprepared":
165 return {
166 installed: false,
167 version: inspection.version,
168 status: "warning",
169 message: `ACP Registry agent '${inspection.agentId}' has not been prepared on this environment.`,
170 };
171 case "failed":
172 return {
173 installed: false,
174 version: null,
175 status: "error",
176 message: inspection.message,
177 };
178 }
179}
180
181interface SnapshotIdentity {
182 readonly instanceId: ProviderInstance["instanceId"];
183 readonly displayName: string | undefined;
184 readonly accentColor: string | undefined;
185 readonly continuationKey: string;
186}
187
188function baseSnapshot(
189 input: SnapshotIdentity & {
190 readonly settings: AcpRegistrySettings;
191 readonly checkedAt: string;
192 readonly installed: boolean;
193 readonly version: string | null;
194 readonly status: ServerProvider["status"];
195 readonly auth: ServerProvider["auth"];
196 readonly documentationUrl?: string;
197 readonly message?: string;
198 readonly probe?: AcpRegistryConfigurationProbeResult;
199 readonly probeError?: AcpRegistryOperationError;
200 },
201): ServerProvider {
202 const iconUrl =
203 input.settings.source === "local"
204 ? null
205 : (resolveOfficialAcpRegistryIconUrl(input.probe?.probe.icon) ??
206 officialAcpRegistryIconUrlForAgentId(input.settings.agentId));
207 return {
208 instanceId: input.instanceId,
209 driver: DRIVER_KIND,
210 ...(input.displayName ? { displayName: input.displayName } : {}),
211 ...(input.accentColor ? { accentColor: input.accentColor } : {}),
212 ...(iconUrl ? { iconUrl } : {}),
213 continuation: { groupKey: input.continuationKey },
214 // The registry driver rejects every application text-generation operation,
215 // so selectors must not offer these instances for commit, PR, branch, or
216 // title generation.
217 supportsTextGeneration: false,
218 enabled: input.settings.enabled,
219 installed: input.installed,
220 version: input.version,
221 status: input.settings.enabled ? input.status : "disabled",
222 auth: { ...input.auth, canLogout: input.auth.canLogout ?? false },
223 checkedAt: input.checkedAt,
224 setup: {
225 canInstall: false,
226 ...(input.documentationUrl ? { documentationUrl: input.documentationUrl } : {}),
227 canAuthenticate:
228 input.installed &&
229 (input.probe
230 ? input.probe.probe.authMethods.length > 0
231 : input.settings.source === "local"
232 ? (input.probeError?.authMethods?.length ?? 0) > 0
233 : input.settings.agentId.length > 0),
234 },
235 ...(input.message ? { message: input.message } : {}),
236 models: modelsFromDiscovery(input.probe?.probe, input.settings.customModels),
237 ...(input.probe === undefined
238 ? {}
239 : {
240 nativeSessions: {
241 canList: input.probe.probe.sessionManagement.canList,
242 canLoad: input.probe.probe.sessionManagement.canLoad,
243 canResume: input.probe.probe.sessionManagement.canResume,
244 canDelete: input.probe.probe.sessionManagement.canDelete,
245 },
246 configurableProviders: input.probe.probe.sessionManagement.canConfigureProviders,
247 }),
248 slashCommands: input.probe?.slashCommands ?? [],
249 skills: input.probe?.skills ?? [],
250 };
251}
252
253export function applyAcpRegistryAvailableCommands(
254 provider: ServerProvider,
255 commands: Option.Option<AcpRegistryAvailableCommands>,
256): ServerProvider {
257 return Option.match(commands, {
258 onNone: () => provider,
259 onSome: ({ slashCommands, skills }) => ({ ...provider, slashCommands, skills }),
260 });
261}
262
263export function applyAcpRegistryLiveConfiguration(
264 provider: ServerProvider,
265 configuration: AcpRegistryLiveConfiguration,
266 customModels: ReadonlyArray<string>,
267): ServerProvider {
268 const { message: _staleProbeMessage, ...snapshot } = provider;
269 return {
270 ...snapshot,
271 status: provider.enabled ? "ready" : provider.status,
272 models: modelsFromDiscovery(configuration, customModels),
273 };
274}
275
276function applyAcpRegistryUrlAuthAction(
277 provider: ServerProvider,
278 action: Option.Option<NonNullable<ServerProvider["auth"]["action"]>>,
279): ServerProvider {
280 const { action: _previousAction, ...auth } = provider.auth;
281 return {
282 ...provider,
283 auth: Option.match(action, {
284 onNone: () => auth,
285 onSome: (nextAction) => ({ ...auth, action: nextAction }),
286 }),
287 };
288}
289
290const buildInitialAcpRegistrySnapshot = Effect.fn("AcpRegistryDriver.buildInitialSnapshot")(
291 function* (input: SnapshotIdentity & { readonly settings: AcpRegistrySettings }) {
292 const checkedAt = DateTime.formatIso(yield* DateTime.now);
293 return baseSnapshot({
294 ...input,
295 checkedAt,
296 installed: false,
297 version: null,
298 status: "warning",
299 auth: { status: "unknown" },
300 message: input.settings.enabled
301 ? "Checking ACP Registry agent readiness..."
302 : "ACP Registry is disabled in T3 Code settings.",
303 });
304 },
305);
306
307export function buildCheckedAcpRegistrySnapshot(
308 input: SnapshotIdentity & {
309 readonly settings: AcpRegistrySettings;
310 readonly checkedAt: string;
311 readonly inspection:
312 | AcpRegistrySupport.AcpRegistryInspection
313 | { readonly status: "failed"; readonly message: string };
314 readonly probe?: AcpRegistryConfigurationProbeResult;
315 readonly probeError?: AcpRegistryOperationError;
316 },
317): ServerProvider {
318 const readiness = acpRegistrySnapshotReadiness(input.inspection);
319 const probeFailed = input.probeError !== undefined;
320 const advertisedAuthMethod =
321 input.probeError?.reason === "authentication_failed"
322 ? (input.probeError.authMethods?.find(
323 (method) => method.id === input.settings.authMethodId,
324 ) ?? input.probeError.authMethods?.[0])
325 : undefined;
326 const authenticationMessage = advertisedAuthMethod
327 ? advertisedAuthMethod.type === "terminal" && advertisedAuthMethod.command
328 ? `Sign in in provider settings using "${advertisedAuthMethod.name}". The login terminal runs on this environment.`
329 : advertisedAuthMethod.type === "env_var" &&
330 (advertisedAuthMethod.envVarNames?.length ?? 0) > 0
331 ? `Set ${advertisedAuthMethod.envVarNames!.join(", ")} under this instance's environment variables in provider settings. T3 Code will detect it on the next provider refresh.`
332 : `Sign in in provider settings using "${advertisedAuthMethod.name}".`
333 : undefined;
334 return baseSnapshot({
335 ...input,
336 ...(input.inspection.status === "ready" && input.inspection.documentationUrl
337 ? { documentationUrl: input.inspection.documentationUrl }
338 : {}),
339 installed: readiness.installed,
340 version: readiness.version,
341 // A failed discovery probe on a ready installation is a warning, not an
342 // outage: the agent binary is present and a real turn may still work
343 // (for example after the user completes authentication).
344 status: probeFailed ? "warning" : readiness.status,
345 auth: input.probe
346 ? {
347 // Discovery sessions and an empty auth-method list do not prove sign-in.
348 status: "unknown",
349 canLogout: input.probe.probe.sessionManagement.canLogout,
350 }
351 : input.probeError?.reason === "authentication_failed"
352 ? {
353 status: "unauthenticated",
354 ...(advertisedAuthMethod
355 ? { type: advertisedAuthMethod.type, label: advertisedAuthMethod.name }
356 : {}),
357 ...(input.probeError.authAction === undefined
358 ? {}
359 : { action: input.probeError.authAction }),
360 }
361 : { status: "unknown" },
362 ...(input.probeError
363 ? { message: authenticationMessage ?? input.probeError.message }
364 : readiness.message
365 ? { message: readiness.message }
366 : {}),
367 });
368}
369
370export const checkAcpRegistryProviderStatus = Effect.fn("AcpRegistryDriver.checkProviderStatus")(
371 function* <Requirements>(
372 input: SnapshotIdentity & {
373 readonly settings: AcpRegistrySettings;
374 readonly cwd: string;
375 readonly environment: NodeJS.ProcessEnv;
376 },
377 probeConfiguration: AcpRegistryConfigurationProbe<Requirements>,
378 ): Effect.fn.Return<ServerProvider, never, AcpRegistrySupport.AcpRegistryCatalog | Requirements> {
379 const checkedAt = DateTime.formatIso(yield* DateTime.now);
380 if (!input.settings.enabled) {
381 return yield* buildInitialAcpRegistrySnapshot(input);
382 }
383 const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog;
384 const inspected = yield* Effect.result(catalog.inspect(input.settings, input.environment));
385 if (Result.isFailure(inspected)) {
386 return buildCheckedAcpRegistrySnapshot({
387 ...input,
388 checkedAt,
389 inspection: {
390 status: "failed",
391 message: `Could not inspect ACP Registry agent: ${inspected.failure.message}`,
392 },
393 });
394 }
395 if (inspected.success.status !== "ready") {
396 return buildCheckedAcpRegistrySnapshot({
397 ...input,
398 checkedAt,
399 inspection: inspected.success,
400 });
401 }
402 const probed = yield* Effect.result(
403 probeConfiguration({
404 instanceId: input.instanceId,
405 settings: input.settings,
406 cwd: input.cwd,
407 environment: input.environment,
408 }),
409 );
410 return Result.isFailure(probed)
411 ? buildCheckedAcpRegistrySnapshot({
412 ...input,
413 checkedAt,
414 inspection: inspected.success,
415 probeError: probed.failure,
416 })
417 : buildCheckedAcpRegistrySnapshot({
418 ...input,
419 checkedAt,
420 inspection: inspected.success,
421 probe: probed.success,
422 });
423 },
424);
425
426/** Publishes local executable readiness without starting an ACP process. */
427export const checkAcpRegistryProviderReadiness = Effect.fn(
428 "AcpRegistryDriver.checkProviderReadiness",
429)(function* (
430 input: SnapshotIdentity & {
431 readonly settings: AcpRegistrySettings;
432 readonly environment: NodeJS.ProcessEnv;
433 },
434): Effect.fn.Return<ServerProvider, never, AcpRegistrySupport.AcpRegistryCatalog> {
435 const checkedAt = DateTime.formatIso(yield* DateTime.now);
436 if (!input.settings.enabled) {
437 return yield* buildInitialAcpRegistrySnapshot(input);
438 }
439 const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog;
440 const inspected = yield* Effect.result(catalog.inspect(input.settings, input.environment));
441 const snapshot = buildCheckedAcpRegistrySnapshot({
442 ...input,
443 checkedAt,
444 inspection: Result.isFailure(inspected)
445 ? {
446 status: "failed",
447 message: `Could not inspect ACP Registry agent: ${inspected.failure.message}`,
448 }
449 : inspected.success,
450 });
451 return inspected._tag === "Success" && inspected.success.status === "ready"
452 ? {
453 ...snapshot,
454 message: "Checking ACP authentication, models, and commands in the background...",
455 }
456 : snapshot;
457});
458
459export type AcpRegistryDriverEnv = AcpRegistryAdapterV2DriverEnv | ProviderHost.ProviderHost;
460
461/** Canonical provider-instance wrapper for ACP Registry orchestration adapters. */
462export const AcpRegistryDriver: ProviderDriver<AcpRegistrySettings, AcpRegistryDriverEnv> = {
463 driverKind: DRIVER_KIND,
464 metadata: {
465 displayName: "ACP Registry",
466 supportsMultipleInstances: true,
467 hasDefaultInstance: false,
468 },
469 configSchema: AcpRegistrySettings,
470 defaultConfig: () => decodeSettings({}),
471 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
472 Effect.gen(function* () {
473 const catalog = yield* AcpRegistrySupport.AcpRegistryCatalog;
474 const runtimeCoordinator = yield* Effect.serviceOption(
475 AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator,
476 );
477 if (Option.isSome(runtimeCoordinator)) {
478 yield* runtimeCoordinator.value.clearAvailableCommands(instanceId);
479 yield* runtimeCoordinator.value.clearLiveConfiguration(instanceId);
480 yield* Effect.addFinalizer(() =>
481 runtimeCoordinator.value
482 .clearAvailableCommands(instanceId)
483 .pipe(Effect.andThen(runtimeCoordinator.value.clearLiveConfiguration(instanceId))),
484 );
485 }
486 const crypto = yield* Crypto.Crypto;
487 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
488 const hostEnvironment = yield* HostProcess.Environment;
489 const host = yield* ProviderHost.ProviderHost;
490 const continuationIdentity = defaultProviderContinuationIdentity({
491 driverKind: DRIVER_KIND,
492 instanceId,
493 });
494 const identity = {
495 instanceId,
496 displayName,
497 accentColor,
498 continuationKey: continuationIdentity.continuationKey,
499 };
500 const effectiveConfig = { ...config, enabled } satisfies AcpRegistrySettings;
501 const processEnvironment = yield* mergeProviderInstanceEnvironment(
502 environment,
503 hostEnvironment,
504 );
505 const orchestrationAdapter = yield* AcpRegistryAdapterV2Driver.create({
506 instanceId,
507 displayName,
508 accentColor,
509 environment,
510 enabled,
511 config,
512 }).pipe(
513 Effect.mapError(
514 (cause) =>
515 new ProviderDriverError({
516 driver: DRIVER_KIND,
517 instanceId,
518 detail: "Failed to build ACP Registry orchestration adapter.",
519 cause,
520 }),
521 ),
522 );
523 const readinessInput = {
524 ...identity,
525 settings: effectiveConfig,
526 environment: processEnvironment,
527 };
528 const confirmedAuthentication =
529 yield* AcpRegistryAuthenticationState.makeAcpRegistryAuthenticationState({
530 cacheDir: host.paths.providerStatusCacheDir,
531 instanceId,
532 settings: effectiveConfig,
533 environment,
534 processEnvironment,
535 });
536 const withLiveRuntimeState = (input: ServerProvider) =>
537 Effect.gen(function* () {
538 if (input.auth.status === "unauthenticated") yield* confirmedAuthentication.set(false);
539 const provider =
540 input.enabled &&
541 input.installed &&
542 input.auth.status === "unknown" &&
543 (yield* confirmedAuthentication.get)
544 ? { ...input, auth: { ...input.auth, status: "authenticated" as const } }
545 : input;
546 return yield* Option.isNone(runtimeCoordinator)
547 ? Effect.succeed(provider)
548 : Effect.all({
549 commands: runtimeCoordinator.value.getAvailableCommands(instanceId),
550 configuration: runtimeCoordinator.value.getLiveConfiguration(instanceId),
551 authAction: runtimeCoordinator.value.getUrlAuthAction(instanceId),
552 }).pipe(
553 Effect.map(({ commands, configuration, authAction }) => {
554 const withCommands = applyAcpRegistryAvailableCommands(provider, commands);
555 const withConfiguration = Option.match(configuration, {
556 onNone: () => withCommands,
557 onSome: (liveConfiguration) =>
558 applyAcpRegistryLiveConfiguration(
559 withCommands,
560 liveConfiguration,
561 effectiveConfig.customModels,
562 ),
563 });
564 return applyAcpRegistryUrlAuthAction(withConfiguration, authAction);
565 }),
566 );
567 });
568 const checkProvider = checkAcpRegistryProviderReadiness(readinessInput).pipe(
569 Effect.provideService(AcpRegistrySupport.AcpRegistryCatalog, catalog),
570 Effect.flatMap(withLiveRuntimeState),
571 );
572 const enrichProvider = checkAcpRegistryProviderStatus(
573 {
574 ...readinessInput,
575 cwd: host.paths.cwd,
576 },
577 probeAcpRegistryConfiguration,
578 ).pipe(
579 Effect.provideService(AcpRegistrySupport.AcpRegistryCatalog, catalog),
580 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
581 Effect.provideService(Crypto.Crypto, crypto),
582 );
583 const enrichmentCache = yield* Ref.make<{
584 readonly generation: number;
585 readonly entry: {
586 readonly provider: ServerProvider;
587 readonly expiresAt: number;
588 } | null;
589 }>({ generation: 0, entry: null });
590 const liveSnapshotSemaphore = yield* Semaphore.make(1);
591 const invalidateEnrichmentCache = Ref.update(enrichmentCache, (current) => ({
592 generation: current.generation + 1,
593 entry: null,
594 }));
595 const enrichProviderCached = (baseSnapshot: ServerProvider) =>
596 Effect.gen(function* () {
597 const now = yield* Clock.currentTimeMillis;
598 const cacheState = yield* Ref.get(enrichmentCache);
599 const cached = cacheState.entry;
600 if (
601 cached !== null &&
602 cached.expiresAt > now &&
603 cached.provider.version === baseSnapshot.version
604 ) {
605 return {
606 provider: { ...cached.provider, checkedAt: baseSnapshot.checkedAt },
607 generation: cacheState.generation,
608 };
609 }
610 const enriched = yield* enrichProvider;
611 if (enriched.status === "ready") {
612 yield* Ref.update(enrichmentCache, (current) =>
613 current.generation === cacheState.generation
614 ? {
615 ...current,
616 entry: {
617 provider: enriched,
618 expiresAt: now + PROBE_SUCCESS_TTL_MS,
619 },
620 }
621 : current,
622 );
623 }
624 return { provider: enriched, generation: cacheState.generation };
625 });
626 const snapshotSettings = yield* makeProviderSnapshotSettingsSource(effectiveConfig);
627 const snapshot = yield* makeManagedServerProvider<
628 ProviderSnapshotSettings<AcpRegistrySettings>
629 >({
630 resolveMaintenance: () => Effect.succeed(MAINTENANCE),
631 getSettings: snapshotSettings.getSettings,
632 streamSettings: snapshotSettings.streamSettings,
633 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
634 initialSnapshot: () => checkProvider,
635 checkProvider,
636 enrichSnapshot: ({ snapshot, getSnapshot, publishSnapshot }) => {
637 if (!snapshot.installed) return Effect.void;
638 const publishEnrichment = (
639 Option.isSome(runtimeCoordinator)
640 ? runtimeCoordinator.value.runBackgroundProbe(
641 effectiveConfig.source === "local"
642 ? `local:${instanceId}`
643 : effectiveConfig.agentId,
644 enrichProviderCached(snapshot),
645 )
646 : enrichProviderCached(snapshot).pipe(Effect.map(Option.some))
647 ).pipe(
648 Effect.flatMap(
649 Option.match({
650 onNone: () => Effect.void,
651 onSome: ({ provider, generation }) =>
652 liveSnapshotSemaphore.withPermit(
653 Ref.get(enrichmentCache).pipe(
654 Effect.flatMap((current) =>
655 current.generation === generation
656 ? withLiveRuntimeState(provider).pipe(Effect.flatMap(publishSnapshot))
657 : Effect.void,
658 ),
659 ),
660 ),
661 }),
662 ),
663 );
664 if (Option.isNone(runtimeCoordinator)) return publishEnrichment;
665
666 const publishLiveCommands = runtimeCoordinator.value.watchAvailableCommands(
667 instanceId,
668 ({ slashCommands, skills }) =>
669 liveSnapshotSemaphore.withPermit(
670 getSnapshot.pipe(
671 Effect.flatMap((current) =>
672 publishSnapshot({
673 ...current,
674 slashCommands,
675 skills,
676 }),
677 ),
678 ),
679 ),
680 );
681 const publishLiveConfiguration = runtimeCoordinator.value.watchLiveConfiguration(
682 instanceId,
683 (configuration) =>
684 liveSnapshotSemaphore.withPermit(
685 getSnapshot.pipe(
686 Effect.flatMap((current) =>
687 publishSnapshot(
688 applyAcpRegistryLiveConfiguration(
689 current,
690 configuration,
691 effectiveConfig.customModels,
692 ),
693 ),
694 ),
695 ),
696 ),
697 );
698 const publishUrlAuthAction = runtimeCoordinator.value.watchUrlAuthAction(
699 instanceId,
700 (action) =>
701 liveSnapshotSemaphore.withPermit(
702 getSnapshot.pipe(
703 Effect.flatMap((current) =>
704 publishSnapshot(
705 applyAcpRegistryUrlAuthAction(current, Option.fromNullishOr(action)),
706 ),
707 ),
708 ),
709 ),
710 );
711 return Effect.all(
712 [
713 publishEnrichment,
714 publishLiveCommands,
715 publishLiveConfiguration,
716 publishUrlAuthAction,
717 ],
718 {
719 concurrency: "unbounded",
720 discard: true,
721 },
722 );
723 },
724 }).pipe(
725 Effect.mapError(
726 (cause) =>
727 new ProviderDriverError({
728 driver: DRIVER_KIND,
729 instanceId,
730 detail: "Failed to build the ACP Registry provider snapshot.",
731 cause,
732 }),
733 ),
734 );
735
736 // The management probes read the coordinator via serviceOption, so it is
737 // provided only when this driver instance actually has one.
738 const provideAcpManagementServices = <A, E>(
739 effect: Effect.Effect<
740 A,
741 E,
742 | AcpRegistrySupport.AcpRegistryCatalog
743 | ChildProcessSpawner.ChildProcessSpawner
744 | Crypto.Crypto
745 >,
746 ): Effect.Effect<A, E> => {
747 const provided = effect.pipe(
748 Effect.provideService(AcpRegistrySupport.AcpRegistryCatalog, catalog),
749 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
750 Effect.provideService(Crypto.Crypto, crypto),
751 );
752 return Option.isSome(runtimeCoordinator)
753 ? provided.pipe(
754 Effect.provideService(
755 AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator,
756 runtimeCoordinator.value,
757 ),
758 )
759 : provided;
760 };
761
762 const clearLiveState = Option.isSome(runtimeCoordinator)
763 ? runtimeCoordinator.value
764 .clearLiveConfiguration(instanceId)
765 .pipe(Effect.andThen(runtimeCoordinator.value.clearAvailableCommands(instanceId)))
766 : Effect.void;
767 const controller = yield* AcpRegistryAuth.makeAcpRegistryAuth({
768 instanceId,
769 settings: effectiveConfig,
770 cwd: host.paths.cwd,
771 environment: processEnvironment,
772 onChanged: (authenticated) =>
773 confirmedAuthentication
774 .set(authenticated)
775 .pipe(
776 Effect.andThen(authenticated ? Effect.void : clearLiveState),
777 Effect.andThen(liveSnapshotSemaphore.withPermit(invalidateEnrichmentCache)),
778 Effect.andThen(snapshot.refresh),
779 Effect.asVoid,
780 ),
781 }).pipe(
782 Effect.provideService(AcpRegistrySupport.AcpRegistryCatalog, catalog),
783 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
784 Effect.provideService(Crypto.Crypto, crypto),
785 );
786 const auth = {
787 ...controller,
788 invalidate: (controller.invalidate ?? Effect.void).pipe(
789 Effect.andThen(confirmedAuthentication.set(false)),
790 Effect.andThen(clearLiveState),
791 Effect.andThen(liveSnapshotSemaphore.withPermit(invalidateEnrichmentCache)),
792 Effect.andThen(snapshot.refresh),
793 Effect.asVoid,
794 ),
795 };
796
797 return {
798 instanceId,
799 driverKind: DRIVER_KIND,
800 continuationIdentity,
801 displayName,
802 accentColor,
803 enabled,
804 auth,
805 snapshot: {
806 ...snapshot,
807 refresh: snapshot.refresh.pipe(
808 Effect.tap(() => controller.refreshMethods ?? Effect.void),
809 ),
810 },
811 orchestrationAdapter,
812 textGeneration: makeUnsupportedTextGeneration(),
813 acpSessionManagement: {
814 listSessions: ({ cwd, cursor }) =>
815 provideAcpManagementServices(
816 listAcpRegistrySessions({
817 instanceId,
818 settings: effectiveConfig,
819 cwd,
820 environment: processEnvironment,
821 ...(cursor === undefined ? {} : { cursor }),
822 }),
823 ),
824 deleteSession: ({ cwd, sessionId }) =>
825 provideAcpManagementServices(
826 deleteAcpRegistrySession({
827 instanceId,
828 settings: effectiveConfig,
829 cwd,
830 environment: processEnvironment,
831 sessionId,
832 }),
833 ),
834 listProviders: (cwd) =>
835 provideAcpManagementServices(
836 listAcpRegistryProviders({
837 instanceId,
838 settings: effectiveConfig,
839 cwd,
840 environment: processEnvironment,
841 }),
842 ),
843 setProvider: ({ cwd, providerId, apiType, baseUrl, headers }) =>
844 provideAcpManagementServices(
845 setAcpRegistryProvider({
846 instanceId,
847 settings: effectiveConfig,
848 cwd,
849 environment: processEnvironment,
850 providerId,
851 apiType,
852 baseUrl,
853 ...(headers === undefined ? {} : { headers }),
854 }),
855 ).pipe(Effect.tap(() => liveSnapshotSemaphore.withPermit(invalidateEnrichmentCache))),
856 disableProvider: ({ cwd, providerId }) =>
857 provideAcpManagementServices(
858 disableAcpRegistryProvider({
859 instanceId,
860 settings: effectiveConfig,
861 cwd,
862 environment: processEnvironment,
863 providerId,
864 }),
865 ).pipe(Effect.tap(() => liveSnapshotSemaphore.withPermit(invalidateEnrichmentCache))),
866 logout: (cwd) => {
867 const logout = logoutAcpRegistry({
868 instanceId,
869 settings: effectiveConfig,
870 cwd,
871 environment: processEnvironment,
872 }).pipe(
873 Effect.provideService(AcpRegistrySupport.AcpRegistryCatalog, catalog),
874 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
875 Effect.provideService(Crypto.Crypto, crypto),
876 Effect.tap(() => confirmedAuthentication.set(false)),
877 Effect.tap(() =>
878 liveSnapshotSemaphore.withPermit(
879 invalidateEnrichmentCache.pipe(
880 Effect.andThen(
881 Option.isSome(runtimeCoordinator)
882 ? Effect.all(
883 [
884 runtimeCoordinator.value.clearAvailableCommands(instanceId),
885 runtimeCoordinator.value.clearLiveConfiguration(instanceId),
886 ],
887 { concurrency: "unbounded", discard: true },
888 )
889 : Effect.void,
890 ),
891 ),
892 ),
893 ),
894 );
895 return Option.isSome(runtimeCoordinator)
896 ? logout.pipe(
897 Effect.provideService(
898 AcpRegistryRuntimeCoordinator.AcpRegistryRuntimeCoordinator,
899 runtimeCoordinator.value,
900 ),
901 )
902 : logout;
903 },
904 },
905 } satisfies ProviderInstance;
906 }),
907};