| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | |
| 4 | import { ProjectId, ThreadId, TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; |
| 5 | import { ProviderOptionDescriptor } from "./model.ts"; |
| 6 | import { ProviderInstanceId } from "./providerInstance.ts"; |
| 7 | |
| 8 | const AcpRegistryAgentId = TrimmedNonEmptyString.check( |
| 9 | Schema.isMaxLength(128), |
| 10 | Schema.isPattern(/^[a-z0-9][a-z0-9._-]*$/), |
| 11 | ); |
| 12 | |
| 13 | const ACP_REGISTRY_CDN_HOSTNAME = "cdn.agentclientprotocol.com"; |
| 14 | const ACP_REGISTRY_AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/; |
| 15 | |
| 16 | /** Derives the official Registry icon URL without trusting stored URL metadata. */ |
| 17 | export 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. */ |
| 27 | export 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 | |
| 46 | export const AcpRegistryDistribution = Schema.Literals(["binary", "npx", "uvx"]); |
| 47 | export type AcpRegistryDistribution = typeof AcpRegistryDistribution.Type; |
| 48 | |
| 49 | export const AcpRegistryIntegrity = Schema.Literals(["sha256", "registry"]); |
| 50 | export type AcpRegistryIntegrity = typeof AcpRegistryIntegrity.Type; |
| 51 | |
| 52 | export const AcpRegistrySearchInput = Schema.Struct({ |
| 53 | query: TrimmedString.check(Schema.isMaxLength(120)), |
| 54 | }); |
| 55 | export type AcpRegistrySearchInput = typeof AcpRegistrySearchInput.Type; |
| 56 | |
| 57 | const AcpRegistryUrl = Schema.String.check(Schema.isMaxLength(2_048)); |
| 58 | |
| 59 | export 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 | }); |
| 72 | export type AcpRegistrySearchAgent = typeof AcpRegistrySearchAgent.Type; |
| 73 | |
| 74 | export const AcpRegistrySearchResult = Schema.Struct({ |
| 75 | agents: Schema.Array(AcpRegistrySearchAgent).check(Schema.isMaxLength(20)), |
| 76 | }); |
| 77 | export type AcpRegistrySearchResult = typeof AcpRegistrySearchResult.Type; |
| 78 | |
| 79 | export const AcpRegistryPrepareInput = Schema.Struct({ |
| 80 | agentId: AcpRegistryAgentId, |
| 81 | }); |
| 82 | export type AcpRegistryPrepareInput = typeof AcpRegistryPrepareInput.Type; |
| 83 | |
| 84 | export const AcpRegistryPrepareResult = Schema.Struct({ |
| 85 | agentId: AcpRegistryAgentId, |
| 86 | version: TrimmedNonEmptyString.check(Schema.isMaxLength(128)), |
| 87 | distribution: AcpRegistryDistribution, |
| 88 | prepared: Schema.Literal(true), |
| 89 | }); |
| 90 | export type AcpRegistryPrepareResult = typeof AcpRegistryPrepareResult.Type; |
| 91 | |
| 92 | export const AcpRegistryManagedBinaryUninstallInput = Schema.Struct({ |
| 93 | agentId: AcpRegistryAgentId, |
| 94 | }); |
| 95 | export type AcpRegistryManagedBinaryUninstallInput = |
| 96 | typeof AcpRegistryManagedBinaryUninstallInput.Type; |
| 97 | |
| 98 | export const AcpRegistryManagedBinaryUninstallResult = Schema.Struct({ |
| 99 | agentId: AcpRegistryAgentId, |
| 100 | removed: Schema.Boolean, |
| 101 | }); |
| 102 | export type AcpRegistryManagedBinaryUninstallResult = |
| 103 | typeof AcpRegistryManagedBinaryUninstallResult.Type; |
| 104 | |
| 105 | const AcpRegistryProbeText = TrimmedNonEmptyString.check(Schema.isMaxLength(256)); |
| 106 | const AcpRegistryProbeDescription = Schema.NullOr(Schema.String.check(Schema.isMaxLength(1_024))); |
| 107 | |
| 108 | export 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 | }); |
| 121 | export type AcpRegistryProbeAuthMethod = typeof AcpRegistryProbeAuthMethod.Type; |
| 122 | |
| 123 | export 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 | }); |
| 132 | export type AcpRegistryUrlAuthAction = typeof AcpRegistryUrlAuthAction.Type; |
| 133 | |
| 134 | export const AcpRegistryAcceptUrlAuthInput = Schema.Struct({ |
| 135 | instanceId: ProviderInstanceId, |
| 136 | elicitationId: AcpRegistryProbeText, |
| 137 | }); |
| 138 | export type AcpRegistryAcceptUrlAuthInput = typeof AcpRegistryAcceptUrlAuthInput.Type; |
| 139 | |
| 140 | export const AcpRegistryAcceptUrlAuthResult = Schema.Struct({ |
| 141 | accepted: Schema.Boolean, |
| 142 | }); |
| 143 | export type AcpRegistryAcceptUrlAuthResult = typeof AcpRegistryAcceptUrlAuthResult.Type; |
| 144 | |
| 145 | const AcpRegistrySessionId = TrimmedNonEmptyString.check(Schema.isMaxLength(1_024)); |
| 146 | const AcpRegistrySessionPath = TrimmedNonEmptyString.check(Schema.isMaxLength(4_096)); |
| 147 | |
| 148 | export 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 | }); |
| 156 | export type AcpRegistrySession = typeof AcpRegistrySession.Type; |
| 157 | |
| 158 | export const AcpRegistryListSessionsInput = Schema.Struct({ |
| 159 | instanceId: ProviderInstanceId, |
| 160 | projectId: ProjectId, |
| 161 | cursor: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2_048))), |
| 162 | }); |
| 163 | export type AcpRegistryListSessionsInput = typeof AcpRegistryListSessionsInput.Type; |
| 164 | |
| 165 | export 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 | }); |
| 172 | export type AcpRegistryListSessionsResult = typeof AcpRegistryListSessionsResult.Type; |
| 173 | |
| 174 | export 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 | }); |
| 183 | export type AcpRegistryImportSessionInput = typeof AcpRegistryImportSessionInput.Type; |
| 184 | |
| 185 | export const AcpRegistryImportSessionResult = Schema.Struct({ |
| 186 | threadId: ThreadId, |
| 187 | imported: Schema.Boolean, |
| 188 | }); |
| 189 | export type AcpRegistryImportSessionResult = typeof AcpRegistryImportSessionResult.Type; |
| 190 | |
| 191 | export const AcpRegistryDeleteSessionInput = Schema.Struct({ |
| 192 | instanceId: ProviderInstanceId, |
| 193 | projectId: ProjectId, |
| 194 | sessionId: AcpRegistrySessionId, |
| 195 | }); |
| 196 | export type AcpRegistryDeleteSessionInput = typeof AcpRegistryDeleteSessionInput.Type; |
| 197 | |
| 198 | export const AcpRegistryDeleteSessionResult = Schema.Struct({ deleted: Schema.Literal(true) }); |
| 199 | export type AcpRegistryDeleteSessionResult = typeof AcpRegistryDeleteSessionResult.Type; |
| 200 | |
| 201 | const AcpRegistryProviderId = TrimmedNonEmptyString.check(Schema.isMaxLength(256)); |
| 202 | const AcpRegistryProviderProtocol = TrimmedNonEmptyString.check(Schema.isMaxLength(64)); |
| 203 | |
| 204 | export 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 | }); |
| 215 | export type AcpRegistryConfigurableProvider = typeof AcpRegistryConfigurableProvider.Type; |
| 216 | |
| 217 | export const AcpRegistryListProvidersInput = Schema.Struct({ |
| 218 | instanceId: ProviderInstanceId, |
| 219 | projectId: ProjectId, |
| 220 | }); |
| 221 | export type AcpRegistryListProvidersInput = typeof AcpRegistryListProvidersInput.Type; |
| 222 | |
| 223 | export const AcpRegistryListProvidersResult = Schema.Struct({ |
| 224 | providers: Schema.Array(AcpRegistryConfigurableProvider).check(Schema.isMaxLength(64)), |
| 225 | }); |
| 226 | export type AcpRegistryListProvidersResult = typeof AcpRegistryListProvidersResult.Type; |
| 227 | |
| 228 | export 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 | }); |
| 242 | export type AcpRegistrySetProviderInput = typeof AcpRegistrySetProviderInput.Type; |
| 243 | |
| 244 | export const AcpRegistrySetProviderResult = Schema.Struct({ configured: Schema.Literal(true) }); |
| 245 | export type AcpRegistrySetProviderResult = typeof AcpRegistrySetProviderResult.Type; |
| 246 | |
| 247 | export const AcpRegistryDisableProviderInput = Schema.Struct({ |
| 248 | instanceId: ProviderInstanceId, |
| 249 | projectId: ProjectId, |
| 250 | providerId: AcpRegistryProviderId, |
| 251 | }); |
| 252 | export type AcpRegistryDisableProviderInput = typeof AcpRegistryDisableProviderInput.Type; |
| 253 | |
| 254 | export const AcpRegistryDisableProviderResult = Schema.Struct({ disabled: Schema.Literal(true) }); |
| 255 | export type AcpRegistryDisableProviderResult = typeof AcpRegistryDisableProviderResult.Type; |
| 256 | |
| 257 | export const AcpRegistryLogoutInput = Schema.Struct({ |
| 258 | instanceId: ProviderInstanceId, |
| 259 | }); |
| 260 | export type AcpRegistryLogoutInput = typeof AcpRegistryLogoutInput.Type; |
| 261 | |
| 262 | export const AcpRegistryLogoutResult = Schema.Struct({ |
| 263 | loggedOut: Schema.Literal(true), |
| 264 | }); |
| 265 | export type AcpRegistryLogoutResult = typeof AcpRegistryLogoutResult.Type; |
| 266 | |
| 267 | export const AcpRegistryProbeModel = Schema.Struct({ |
| 268 | id: AcpRegistryProbeText, |
| 269 | name: AcpRegistryProbeText, |
| 270 | description: AcpRegistryProbeDescription, |
| 271 | }); |
| 272 | export type AcpRegistryProbeModel = typeof AcpRegistryProbeModel.Type; |
| 273 | |
| 274 | export 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 | }); |
| 306 | export type AcpRegistryProbeResult = typeof AcpRegistryProbeResult.Type; |
| 307 | |
| 308 | export 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 | ]); |
| 334 | export type AcpRegistryOperationErrorReason = typeof AcpRegistryOperationErrorReason.Type; |
| 335 | |
| 336 | export 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 | ) {} |