Skip to content
Source
Sign inSign up

acpRegistry.ts

347 lines13.1 KB

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​
16export function officialAcpRegistryIconUrlForAgentId(
17 agentId: string | null | undefined,
18): string | null {
19 if (agentId === null || agentId === undefined || !ACP_REGISTRY_AGENT_ID_PATTERN.test(agentId)) {
20 return null;
21 }
22 return `https://${ACP_REGISTRY_CDN_HOSTNAME}/registry/v1/latest/${agentId}.svg`;
24​
25export function resolveOfficialAcpRegistryIconUrl(icon: string | null | undefined): string | null {
26 if (!icon) return null;
27 try {
28 const url = new URL(icon);
29 if (
30 url.protocol !== "https:" ||
31 url.hostname !== ACP_REGISTRY_CDN_HOSTNAME ||
32 url.port !== "" ||
33 url.username !== "" ||
34 url.password !== ""
35 ) {
36 return null;
37 }
38 return url.href;
39 } catch {
40 return null;
41 }
43​
44export const AcpRegistryDistribution = Schema.Literals(["binary", "npx", "uvx"]);
45export type AcpRegistryDistribution = typeof AcpRegistryDistribution.Type;
46​
47export const AcpRegistryIntegrity = Schema.Literals(["sha256", "registry"]);
48export type AcpRegistryIntegrity = typeof AcpRegistryIntegrity.Type;
49​
50export const ACP_REGISTRY_MAX_AGENTS = 512;
51​
52export const AcpRegistrySearchInput = Schema.Struct({
53 query: TrimmedString.check(Schema.isMaxLength(120)),
54 limit: Schema.optional(
55 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: ACP_REGISTRY_MAX_AGENTS })),
56 ),
57});
58export type AcpRegistrySearchInput = typeof AcpRegistrySearchInput.Type;
59​
60const AcpRegistryUrl = Schema.String.check(Schema.isMaxLength(2_048));
61​
62export const AcpRegistrySearchAgent = Schema.Struct({
63 id: AcpRegistryAgentId,
64 name: TrimmedNonEmptyString.check(Schema.isMaxLength(160)),
65 version: TrimmedNonEmptyString.check(Schema.isMaxLength(128)),
66 description: Schema.String.check(Schema.isMaxLength(1_024)),
67 authors: Schema.Array(Schema.String.check(Schema.isMaxLength(256))).check(Schema.isMaxLength(16)),
68 license: Schema.NullOr(Schema.String.check(Schema.isMaxLength(128))),
69 website: Schema.NullOr(AcpRegistryUrl),
70 repository: Schema.NullOr(AcpRegistryUrl),
71 icon: Schema.NullOr(AcpRegistryUrl),
72 distribution: AcpRegistryDistribution,
73 integrity: AcpRegistryIntegrity,
74});
75export type AcpRegistrySearchAgent = typeof AcpRegistrySearchAgent.Type;
76​
77export const AcpRegistrySearchResult = Schema.Struct({
78 agents: Schema.Array(AcpRegistrySearchAgent).check(Schema.isMaxLength(ACP_REGISTRY_MAX_AGENTS)),
79});
80export type AcpRegistrySearchResult = typeof AcpRegistrySearchResult.Type;
81​
82export const AcpRegistryPrepareInput = Schema.Struct({
83 agentId: AcpRegistryAgentId,
84});
85export type AcpRegistryPrepareInput = typeof AcpRegistryPrepareInput.Type;
86​
87export const AcpRegistryPrepareResult = Schema.Struct({
88 agentId: AcpRegistryAgentId,
89 version: TrimmedNonEmptyString.check(Schema.isMaxLength(128)),
90 distribution: AcpRegistryDistribution,
91 prepared: Schema.Literal(true),
92});
93export type AcpRegistryPrepareResult = typeof AcpRegistryPrepareResult.Type;
94​
95export const AcpRegistryManagedBinaryUninstallInput = Schema.Struct({
96 agentId: AcpRegistryAgentId,
97});
98export type AcpRegistryManagedBinaryUninstallInput =
99 typeof AcpRegistryManagedBinaryUninstallInput.Type;
100​
101export const AcpRegistryManagedBinaryUninstallResult = Schema.Struct({
102 agentId: AcpRegistryAgentId,
103 removed: Schema.Boolean,
104});
105export type AcpRegistryManagedBinaryUninstallResult =
106 typeof AcpRegistryManagedBinaryUninstallResult.Type;
107​
108const AcpRegistryProbeText = TrimmedNonEmptyString.check(Schema.isMaxLength(256));
109const AcpRegistryProbeDescription = Schema.NullOr(Schema.String.check(Schema.isMaxLength(1_024)));
110​
111export const AcpRegistryProbeAuthMethod = Schema.Struct({
112 id: AcpRegistryProbeText,
113 name: AcpRegistryProbeText,
114 description: AcpRegistryProbeDescription,
115 type: Schema.Literals(["agent", "env_var", "terminal"]),
116 command: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2_048))),
117 envVarNames: Schema.optionalKey(Schema.Array(AcpRegistryProbeText).check(Schema.isMaxLength(16))),
118 link: Schema.optionalKey(AcpRegistryUrl),
119});
120export type AcpRegistryProbeAuthMethod = typeof AcpRegistryProbeAuthMethod.Type;
121​
122export const AcpRegistryUrlAuthAction = Schema.Struct({
123 elicitationId: AcpRegistryProbeText,
124 url: AcpRegistryUrl,
125 message: Schema.String.check(Schema.isMaxLength(1_024)),
126 createdAt: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(128))),
127 expiresAt: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(128))),
128});
129export type AcpRegistryUrlAuthAction = typeof AcpRegistryUrlAuthAction.Type;
130​
131export const AcpRegistryAcceptUrlAuthInput = Schema.Struct({
132 instanceId: ProviderInstanceId,
133 elicitationId: AcpRegistryProbeText,
134});
135export type AcpRegistryAcceptUrlAuthInput = typeof AcpRegistryAcceptUrlAuthInput.Type;
136​
137export const AcpRegistryAcceptUrlAuthResult = Schema.Struct({
138 accepted: Schema.Boolean,
139});
140export type AcpRegistryAcceptUrlAuthResult = typeof AcpRegistryAcceptUrlAuthResult.Type;
141​
142const AcpRegistrySessionId = TrimmedNonEmptyString.check(Schema.isMaxLength(1_024));
143const AcpRegistrySessionPath = TrimmedNonEmptyString.check(Schema.isMaxLength(4_096));
144​
145export const AcpRegistrySession = Schema.Struct({
146 sessionId: AcpRegistrySessionId,
147 cwd: AcpRegistrySessionPath,
148 additionalDirectories: Schema.Array(AcpRegistrySessionPath).check(Schema.isMaxLength(32)),
149 title: Schema.NullOr(Schema.String.check(Schema.isMaxLength(1_024))),
150 updatedAt: Schema.NullOr(Schema.String.check(Schema.isMaxLength(128))),
151 importedThreadId: Schema.NullOr(ThreadId),
152 /** The imported thread still lacks the session's history, so importing again copies it. */
153 nativeHistoryPending: Schema.optionalKey(Schema.Boolean),
154});
155export type AcpRegistrySession = typeof AcpRegistrySession.Type;
156​
157export const AcpRegistryListSessionsInput = Schema.Struct({
158 instanceId: ProviderInstanceId,
159 projectId: ProjectId,
160 cursor: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2_048))),
161});
162export type AcpRegistryListSessionsInput = typeof AcpRegistryListSessionsInput.Type;
163​
164export const AcpRegistryListSessionsResult = Schema.Struct({
165 sessions: Schema.Array(AcpRegistrySession).check(Schema.isMaxLength(256)),
166 nextCursor: Schema.NullOr(Schema.String.check(Schema.isMaxLength(2_048))),
167 canLoad: Schema.Boolean,
168 canResume: Schema.Boolean,
169 canDelete: Schema.Boolean,
170});
171export type AcpRegistryListSessionsResult = typeof AcpRegistryListSessionsResult.Type;
172​
173export const AcpRegistryImportSessionInput = Schema.Struct({
174 instanceId: ProviderInstanceId,
175 projectId: ProjectId,
176 sessionId: AcpRegistrySessionId,
177 title: Schema.optionalKey(Schema.NullOr(TrimmedNonEmptyString.check(Schema.isMaxLength(1_024)))),
178 updatedAt: Schema.optionalKey(
179 Schema.NullOr(TrimmedNonEmptyString.check(Schema.isMaxLength(128))),
180 ),
181});
182export type AcpRegistryImportSessionInput = typeof AcpRegistryImportSessionInput.Type;
183​
184export const AcpRegistryImportSessionResult = Schema.Struct({
185 threadId: ThreadId,
186 imported: Schema.Boolean,
187});
188export type AcpRegistryImportSessionResult = typeof AcpRegistryImportSessionResult.Type;
189​
190export const AcpRegistryDeleteSessionInput = Schema.Struct({
191 instanceId: ProviderInstanceId,
192 projectId: ProjectId,
193 sessionId: AcpRegistrySessionId,
194});
195export type AcpRegistryDeleteSessionInput = typeof AcpRegistryDeleteSessionInput.Type;
196​
197export const AcpRegistryDeleteSessionResult = Schema.Struct({ deleted: Schema.Literal(true) });
198export type AcpRegistryDeleteSessionResult = typeof AcpRegistryDeleteSessionResult.Type;
199​
200const AcpRegistryProviderId = TrimmedNonEmptyString.check(Schema.isMaxLength(256));
201const AcpRegistryProviderProtocol = TrimmedNonEmptyString.check(Schema.isMaxLength(64));
202​
203export const AcpRegistryConfigurableProvider = Schema.Struct({
204 providerId: AcpRegistryProviderId,
205 supported: Schema.Array(AcpRegistryProviderProtocol).check(Schema.isMaxLength(16)),
206 required: Schema.Boolean,
207 current: Schema.NullOr(
208 Schema.Struct({
209 apiType: AcpRegistryProviderProtocol,
210 baseUrl: AcpRegistryUrl,
211 }),
212 ),
213});
214export type AcpRegistryConfigurableProvider = typeof AcpRegistryConfigurableProvider.Type;
215​
216export const AcpRegistryListProvidersInput = Schema.Struct({
217 instanceId: ProviderInstanceId,
218 projectId: ProjectId,
219});
220export type AcpRegistryListProvidersInput = typeof AcpRegistryListProvidersInput.Type;
221​
222export const AcpRegistryListProvidersResult = Schema.Struct({
223 providers: Schema.Array(AcpRegistryConfigurableProvider).check(Schema.isMaxLength(64)),
224});
225export type AcpRegistryListProvidersResult = typeof AcpRegistryListProvidersResult.Type;
226​
227export const AcpRegistrySetProviderInput = Schema.Struct({
228 instanceId: ProviderInstanceId,
229 projectId: ProjectId,
230 providerId: AcpRegistryProviderId,
231 apiType: AcpRegistryProviderProtocol,
232 baseUrl: AcpRegistryUrl,
233 // Write-only secrets. Provider list responses intentionally cannot carry headers.
234 headers: Schema.optionalKey(
235 Schema.Record(
236 TrimmedNonEmptyString.check(Schema.isMaxLength(128)),
237 Schema.String.check(Schema.isMaxLength(8_192)),
238 ),
239 ),
240});
241export type AcpRegistrySetProviderInput = typeof AcpRegistrySetProviderInput.Type;
242​
243export const AcpRegistrySetProviderResult = Schema.Struct({ configured: Schema.Literal(true) });
244export type AcpRegistrySetProviderResult = typeof AcpRegistrySetProviderResult.Type;
245​
246export const AcpRegistryDisableProviderInput = Schema.Struct({
247 instanceId: ProviderInstanceId,
248 projectId: ProjectId,
249 providerId: AcpRegistryProviderId,
250});
251export type AcpRegistryDisableProviderInput = typeof AcpRegistryDisableProviderInput.Type;
252​
253export const AcpRegistryDisableProviderResult = Schema.Struct({ disabled: Schema.Literal(true) });
254export type AcpRegistryDisableProviderResult = typeof AcpRegistryDisableProviderResult.Type;
255​
256export const AcpRegistryLogoutInput = Schema.Struct({
257 instanceId: ProviderInstanceId,
258});
259export type AcpRegistryLogoutInput = typeof AcpRegistryLogoutInput.Type;
260​
261export const AcpRegistryLogoutResult = Schema.Struct({
262 loggedOut: Schema.Literal(true),
263});
264export type AcpRegistryLogoutResult = typeof AcpRegistryLogoutResult.Type;
265​
266export const AcpRegistryProbeModel = Schema.Struct({
267 id: AcpRegistryProbeText,
268 name: AcpRegistryProbeText,
269 description: AcpRegistryProbeDescription,
270});
271export type AcpRegistryProbeModel = typeof AcpRegistryProbeModel.Type;
272​
273export const AcpRegistryProbeResult = Schema.Struct({
274 instanceId: ProviderInstanceId,
275 // `ready` requires a disposable ACP session/new probe, not passive auth detection.
276 ready: Schema.Literal(true),
277 icon: Schema.NullOr(AcpRegistryUrl),
278 authMethods: Schema.Array(AcpRegistryProbeAuthMethod).check(Schema.isMaxLength(32)),
279 models: Schema.Array(AcpRegistryProbeModel).check(Schema.isMaxLength(256)),
280 currentModelId: Schema.NullOr(AcpRegistryProbeText),
281 configOptions: Schema.Array(ProviderOptionDescriptor).check(Schema.isMaxLength(16)),
282 // configOptions ids that describe only the current model (ACP thought_level / model_config).
283 modelOptionIds: Schema.optionalKey(
284 Schema.Array(AcpRegistryProbeText).check(Schema.isMaxLength(16)),
285 ),
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) {}