1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3
4import { ProjectId, ThreadId, TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts";
5import { ProviderOptionDescriptor } from "./model.ts";
6import { ProviderInstanceId } from "./providerInstance.ts";
7
8const AcpRegistryAgentId = TrimmedNonEmptyString.check(
9 Schema.isMaxLength(128),
10 Schema.isPattern(/^[a-z0-9][a-z0-9._-]*$/),
11);
12
13const ACP_REGISTRY_CDN_HOSTNAME = "cdn.agentclientprotocol.com";
14const ACP_REGISTRY_AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
15
16/** Derives the official Registry icon URL without trusting stored URL metadata. */
17export function officialAcpRegistryIconUrlForAgentId(
18 agentId: string | null | undefined,
19): string | null {
20 if (agentId === null || agentId === undefined || !ACP_REGISTRY_AGENT_ID_PATTERN.test(agentId)) {
21 return null;
22 }
23 return `https://${ACP_REGISTRY_CDN_HOSTNAME}/registry/v1/latest/${agentId}.svg`;
24}
25
26/** Allows provider icons only from the credential-free official Registry CDN origin. */
27export function resolveOfficialAcpRegistryIconUrl(icon: string | null | undefined): string | null {
28 if (!icon) return null;
29 try {
30 const url = new URL(icon);
31 if (
32 url.protocol !== "https:" ||
33 url.hostname !== ACP_REGISTRY_CDN_HOSTNAME ||
34 url.port !== "" ||
35 url.username !== "" ||
36 url.password !== ""
37 ) {
38 return null;
39 }
40 return url.href;
41 } catch {
42 return null;
43 }
44}
45
46export const AcpRegistryDistribution = Schema.Literals(["binary", "npx", "uvx"]);
47export type AcpRegistryDistribution = typeof AcpRegistryDistribution.Type;
48
49export const AcpRegistryIntegrity = Schema.Literals(["sha256", "registry"]);
50export type AcpRegistryIntegrity = typeof AcpRegistryIntegrity.Type;
51
52export const AcpRegistrySearchInput = Schema.Struct({
53 query: TrimmedString.check(Schema.isMaxLength(120)),
54});
55export type AcpRegistrySearchInput = typeof AcpRegistrySearchInput.Type;
56
57const AcpRegistryUrl = Schema.String.check(Schema.isMaxLength(2_048));
58
59export const AcpRegistrySearchAgent = Schema.Struct({
60 id: AcpRegistryAgentId,
61 name: TrimmedNonEmptyString.check(Schema.isMaxLength(160)),
62 version: TrimmedNonEmptyString.check(Schema.isMaxLength(128)),
63 description: Schema.String.check(Schema.isMaxLength(1_024)),
64 authors: Schema.Array(Schema.String.check(Schema.isMaxLength(256))).check(Schema.isMaxLength(16)),
65 license: Schema.NullOr(Schema.String.check(Schema.isMaxLength(128))),
66 website: Schema.NullOr(AcpRegistryUrl),
67 repository: Schema.NullOr(AcpRegistryUrl),
68 icon: Schema.NullOr(AcpRegistryUrl),
69 distribution: AcpRegistryDistribution,
70 integrity: AcpRegistryIntegrity,
71});
72export type AcpRegistrySearchAgent = typeof AcpRegistrySearchAgent.Type;
73
74export const AcpRegistrySearchResult = Schema.Struct({
75 agents: Schema.Array(AcpRegistrySearchAgent).check(Schema.isMaxLength(20)),
76});
77export type AcpRegistrySearchResult = typeof AcpRegistrySearchResult.Type;
78
79export const AcpRegistryPrepareInput = Schema.Struct({
80 agentId: AcpRegistryAgentId,
81});
82export type AcpRegistryPrepareInput = typeof AcpRegistryPrepareInput.Type;
83
84export const AcpRegistryPrepareResult = Schema.Struct({
85 agentId: AcpRegistryAgentId,
86 version: TrimmedNonEmptyString.check(Schema.isMaxLength(128)),
87 distribution: AcpRegistryDistribution,
88 prepared: Schema.Literal(true),
89});
90export type AcpRegistryPrepareResult = typeof AcpRegistryPrepareResult.Type;
91
92export const AcpRegistryManagedBinaryUninstallInput = Schema.Struct({
93 agentId: AcpRegistryAgentId,
94});
95export type AcpRegistryManagedBinaryUninstallInput =
96 typeof AcpRegistryManagedBinaryUninstallInput.Type;
97
98export const AcpRegistryManagedBinaryUninstallResult = Schema.Struct({
99 agentId: AcpRegistryAgentId,
100 removed: Schema.Boolean,
101});
102export type AcpRegistryManagedBinaryUninstallResult =
103 typeof AcpRegistryManagedBinaryUninstallResult.Type;
104
105const AcpRegistryProbeText = TrimmedNonEmptyString.check(Schema.isMaxLength(256));
106const AcpRegistryProbeDescription = Schema.NullOr(Schema.String.check(Schema.isMaxLength(1_024)));
107
108export const AcpRegistryProbeAuthMethod = Schema.Struct({
109 id: AcpRegistryProbeText,
110 name: AcpRegistryProbeText,
111 description: AcpRegistryProbeDescription,
112 type: Schema.Literals(["agent", "env_var", "terminal"]),
113 // Terminal methods: the full command line the user runs in a thread
114 // terminal on the owning environment to complete interactive auth.
115 command: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2_048))),
116 // Env-var methods: variable names the user sets on the provider instance.
117 envVarNames: Schema.optionalKey(Schema.Array(AcpRegistryProbeText).check(Schema.isMaxLength(16))),
118 // Env-var methods may advertise where credentials can be created.
119 link: Schema.optionalKey(AcpRegistryUrl),
120});
121export type AcpRegistryProbeAuthMethod = typeof AcpRegistryProbeAuthMethod.Type;
122
123export const AcpRegistryUrlAuthAction = Schema.Struct({
124 elicitationId: AcpRegistryProbeText,
125 url: AcpRegistryUrl,
126 message: Schema.String.check(Schema.isMaxLength(1_024)),
127 // Optional for mixed-version clients. New servers include both timestamps
128 // so a pending browser login remains understandable on another device.
129 createdAt: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(128))),
130 expiresAt: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(128))),
131});
132export type AcpRegistryUrlAuthAction = typeof AcpRegistryUrlAuthAction.Type;
133
134export const AcpRegistryAcceptUrlAuthInput = Schema.Struct({
135 instanceId: ProviderInstanceId,
136 elicitationId: AcpRegistryProbeText,
137});
138export type AcpRegistryAcceptUrlAuthInput = typeof AcpRegistryAcceptUrlAuthInput.Type;
139
140export const AcpRegistryAcceptUrlAuthResult = Schema.Struct({
141 accepted: Schema.Boolean,
142});
143export type AcpRegistryAcceptUrlAuthResult = typeof AcpRegistryAcceptUrlAuthResult.Type;
144
145const AcpRegistrySessionId = TrimmedNonEmptyString.check(Schema.isMaxLength(1_024));
146const AcpRegistrySessionPath = TrimmedNonEmptyString.check(Schema.isMaxLength(4_096));
147
148export const AcpRegistrySession = Schema.Struct({
149 sessionId: AcpRegistrySessionId,
150 cwd: AcpRegistrySessionPath,
151 additionalDirectories: Schema.Array(AcpRegistrySessionPath).check(Schema.isMaxLength(32)),
152 title: Schema.NullOr(Schema.String.check(Schema.isMaxLength(1_024))),
153 updatedAt: Schema.NullOr(Schema.String.check(Schema.isMaxLength(128))),
154 importedThreadId: Schema.NullOr(ThreadId),
155});
156export type AcpRegistrySession = typeof AcpRegistrySession.Type;
157
158export const AcpRegistryListSessionsInput = Schema.Struct({
159 instanceId: ProviderInstanceId,
160 projectId: ProjectId,
161 cursor: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2_048))),
162});
163export type AcpRegistryListSessionsInput = typeof AcpRegistryListSessionsInput.Type;
164
165export const AcpRegistryListSessionsResult = Schema.Struct({
166 sessions: Schema.Array(AcpRegistrySession).check(Schema.isMaxLength(256)),
167 nextCursor: Schema.NullOr(Schema.String.check(Schema.isMaxLength(2_048))),
168 canLoad: Schema.Boolean,
169 canResume: Schema.Boolean,
170 canDelete: Schema.Boolean,
171});
172export type AcpRegistryListSessionsResult = typeof AcpRegistryListSessionsResult.Type;
173
174export const AcpRegistryImportSessionInput = Schema.Struct({
175 instanceId: ProviderInstanceId,
176 projectId: ProjectId,
177 sessionId: AcpRegistrySessionId,
178 title: Schema.optionalKey(Schema.NullOr(TrimmedNonEmptyString.check(Schema.isMaxLength(1_024)))),
179 updatedAt: Schema.optionalKey(
180 Schema.NullOr(TrimmedNonEmptyString.check(Schema.isMaxLength(128))),
181 ),
182});
183export type AcpRegistryImportSessionInput = typeof AcpRegistryImportSessionInput.Type;
184
185export const AcpRegistryImportSessionResult = Schema.Struct({
186 threadId: ThreadId,
187 imported: Schema.Boolean,
188});
189export type AcpRegistryImportSessionResult = typeof AcpRegistryImportSessionResult.Type;
190
191export const AcpRegistryDeleteSessionInput = Schema.Struct({
192 instanceId: ProviderInstanceId,
193 projectId: ProjectId,
194 sessionId: AcpRegistrySessionId,
195});
196export type AcpRegistryDeleteSessionInput = typeof AcpRegistryDeleteSessionInput.Type;
197
198export const AcpRegistryDeleteSessionResult = Schema.Struct({ deleted: Schema.Literal(true) });
199export type AcpRegistryDeleteSessionResult = typeof AcpRegistryDeleteSessionResult.Type;
200
201const AcpRegistryProviderId = TrimmedNonEmptyString.check(Schema.isMaxLength(256));
202const AcpRegistryProviderProtocol = TrimmedNonEmptyString.check(Schema.isMaxLength(64));
203
204export const AcpRegistryConfigurableProvider = Schema.Struct({
205 providerId: AcpRegistryProviderId,
206 supported: Schema.Array(AcpRegistryProviderProtocol).check(Schema.isMaxLength(16)),
207 required: Schema.Boolean,
208 current: Schema.NullOr(
209 Schema.Struct({
210 apiType: AcpRegistryProviderProtocol,
211 baseUrl: AcpRegistryUrl,
212 }),
213 ),
214});
215export type AcpRegistryConfigurableProvider = typeof AcpRegistryConfigurableProvider.Type;
216
217export const AcpRegistryListProvidersInput = Schema.Struct({
218 instanceId: ProviderInstanceId,
219 projectId: ProjectId,
220});
221export type AcpRegistryListProvidersInput = typeof AcpRegistryListProvidersInput.Type;
222
223export const AcpRegistryListProvidersResult = Schema.Struct({
224 providers: Schema.Array(AcpRegistryConfigurableProvider).check(Schema.isMaxLength(64)),
225});
226export type AcpRegistryListProvidersResult = typeof AcpRegistryListProvidersResult.Type;
227
228export const AcpRegistrySetProviderInput = Schema.Struct({
229 instanceId: ProviderInstanceId,
230 projectId: ProjectId,
231 providerId: AcpRegistryProviderId,
232 apiType: AcpRegistryProviderProtocol,
233 baseUrl: AcpRegistryUrl,
234 // Write-only secrets. Provider list responses intentionally cannot carry headers.
235 headers: Schema.optionalKey(
236 Schema.Record(
237 TrimmedNonEmptyString.check(Schema.isMaxLength(128)),
238 Schema.String.check(Schema.isMaxLength(8_192)),
239 ),
240 ),
241});
242export type AcpRegistrySetProviderInput = typeof AcpRegistrySetProviderInput.Type;
243
244export const AcpRegistrySetProviderResult = Schema.Struct({ configured: Schema.Literal(true) });
245export type AcpRegistrySetProviderResult = typeof AcpRegistrySetProviderResult.Type;
246
247export const AcpRegistryDisableProviderInput = Schema.Struct({
248 instanceId: ProviderInstanceId,
249 projectId: ProjectId,
250 providerId: AcpRegistryProviderId,
251});
252export type AcpRegistryDisableProviderInput = typeof AcpRegistryDisableProviderInput.Type;
253
254export const AcpRegistryDisableProviderResult = Schema.Struct({ disabled: Schema.Literal(true) });
255export type AcpRegistryDisableProviderResult = typeof AcpRegistryDisableProviderResult.Type;
256
257export const AcpRegistryLogoutInput = Schema.Struct({
258 instanceId: ProviderInstanceId,
259});
260export type AcpRegistryLogoutInput = typeof AcpRegistryLogoutInput.Type;
261
262export const AcpRegistryLogoutResult = Schema.Struct({
263 loggedOut: Schema.Literal(true),
264});
265export type AcpRegistryLogoutResult = typeof AcpRegistryLogoutResult.Type;
266
267export const AcpRegistryProbeModel = Schema.Struct({
268 id: AcpRegistryProbeText,
269 name: AcpRegistryProbeText,
270 description: AcpRegistryProbeDescription,
271});
272export type AcpRegistryProbeModel = typeof AcpRegistryProbeModel.Type;
273
274export const AcpRegistryProbeResult = Schema.Struct({
275 instanceId: ProviderInstanceId,
276 // `ready` is only returned after a disposable ACP session/new probe completes.
277 // It does not imply that authentication was passively detected.
278 ready: Schema.Literal(true),
279 icon: Schema.NullOr(AcpRegistryUrl),
280 authMethods: Schema.Array(AcpRegistryProbeAuthMethod).check(Schema.isMaxLength(32)),
281 models: Schema.Array(AcpRegistryProbeModel).check(Schema.isMaxLength(256)),
282 currentModelId: Schema.NullOr(AcpRegistryProbeText),
283 // Non-model session config options and session modes, pre-mapped onto T3's
284 // provider option descriptors so model capabilities can carry them directly.
285 configOptions: Schema.Array(ProviderOptionDescriptor).check(Schema.isMaxLength(16)),
286 sessionManagement: Schema.Struct({
287 canList: Schema.Boolean,
288 canLoad: Schema.Boolean,
289 canResume: Schema.Boolean,
290 canLogout: Schema.Boolean,
291 canDelete: Schema.Boolean,
292 canConfigureProviders: Schema.Boolean,
293 }).pipe(
294 Schema.withDecodingDefault(
295 Effect.succeed({
296 canList: false,
297 canLoad: false,
298 canResume: false,
299 canLogout: false,
300 canDelete: false,
301 canConfigureProviders: false,
302 }),
303 ),
304 ),
305});
306export type AcpRegistryProbeResult = typeof AcpRegistryProbeResult.Type;
307
308export const AcpRegistryOperationErrorReason = Schema.Literals([
309 "agent_not_configured",
310 "agent_not_found",
311 "archive_invalid",
312 "authentication_failed",
313 "checksum_mismatch",
314 "download_failed",
315 "install_failed",
316 "instance_not_found",
317 "logout_unsupported",
318 "logout_failed",
319 "probe_failed",
320 "project_not_found",
321 "registry_unavailable",
322 "runner_unavailable",
323 "session_import_failed",
324 "session_delete_unsupported",
325 "session_delete_failed",
326 "session_list_unsupported",
327 "session_resume_unsupported",
328 "providers_unsupported",
329 "providers_list_failed",
330 "provider_configuration_failed",
331 "unsupported_distribution",
332 "unsupported_platform",
333]);
334export type AcpRegistryOperationErrorReason = typeof AcpRegistryOperationErrorReason.Type;
335
336export class AcpRegistryOperationError extends Schema.TaggedError<AcpRegistryOperationError>()(
337 "AcpRegistryOperationError",
338 {
339 reason: AcpRegistryOperationErrorReason,
340 message: Schema.String,
341 authMethods: Schema.optionalKey(
342 Schema.Array(AcpRegistryProbeAuthMethod).check(Schema.isMaxLength(32)),
343 ),
344 authAction: Schema.optionalKey(AcpRegistryUrlAuthAction),
345 cause: Schema.optionalKey(Schema.Defect()),
346 },
347) {}