Skip to content
Source
Sign inSign up

assets.ts

338 lines11.6 KB

1import * as Schema from "effect/Schema";
2​
3import { NonNegativeInt, ThreadId, TrimmedNonEmptyString, TurnItemId } from "./baseSchemas.ts";
4import {
5 PROVIDER_SEND_TURN_MAX_FILE_BYTES,
6 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
7 PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES,
8} from "./chatAttachment.ts";
9import { ProjectFaviconPath } from "./project.ts";
10import { ToolActivityNativeAppReference } from "./providerRuntime.ts";
11​
12const ASSET_PATH_MAX_LENGTH = 1024;
13​
14/**
15 * The wire tag of a source control host's credentialed media. It names GitHub because GitHub had
16 * it first, and servers and clients of different releases must keep understanding each other.
17 */
18export const SOURCE_CONTROL_MEDIA_ASSET = "github-media" as const;
19​
20export const NativeAppIconReference = Schema.Union([
21 ToolActivityNativeAppReference,
22 Schema.TaggedStruct("app-path", {
23 path: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
24 }),
25]);
26export type NativeAppIconReference = typeof NativeAppIconReference.Type;
27​
28export const AssetResource = Schema.Union([
29 Schema.TaggedStruct("workspace-file", {
30 threadId: ThreadId,
31 path: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
32 }),
33 Schema.TaggedStruct("media-file", {
34 threadId: ThreadId,
35 path: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
36 disposition: Schema.optionalKey(Schema.Literal("attachment")),
37 // Written relative to the thread's workspace, so only that host's file is meant.
38 hostOnly: Schema.optionalKey(Schema.Literal(true)),
39 }),
40 Schema.TaggedStruct("draft-workspace-file", {
41 cwd: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
42 path: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
43 }),
44 Schema.TaggedStruct("attachment", {
45 attachmentId: TrimmedNonEmptyString.check(Schema.isMaxLength(256)),
46 fileName: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(255))),
47 mimeType: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(100))),
48 /** Defaults to download; viewers may request inline after checking the type is safe. */
49 disposition: Schema.optionalKey(Schema.Literals(["inline", "attachment"])),
50 renderIntent: Schema.optionalKey(Schema.Literal("mcp-app")),
51 }),
52 Schema.TaggedStruct("tool-output-image", {
53 threadId: ThreadId,
54 itemId: TurnItemId,
55 index: NonNegativeInt,
56 }),
57 Schema.TaggedStruct("project-favicon", {
58 cwd: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
59 // Cache-key hint only; the project projection supplies the authoritative path.
60 path: Schema.optional(ProjectFaviconPath),
61 }),
62 Schema.TaggedStruct("native-app-icon", {
63 app: NativeAppIconReference,
64 }),
65 // Media a change request body points at on its host. A private repository serves these only
66 // to a request that carries a credential, which the client has none of, so the server fetches
67 // them with the credential the host's driver holds for the repository at `cwd`. `repository`
68 // names the change request's repository, which can differ from the checkout at `cwd`.
69 Schema.TaggedStruct(SOURCE_CONTROL_MEDIA_ASSET, {
70 cwd: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
71 url: TrimmedNonEmptyString.check(Schema.isMaxLength(2048)),
72 repository: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(512))),
73 }),
74]);
75export type AssetResource = typeof AssetResource.Type;
76​
77export const AssetCreateUrlInput = Schema.Struct({
78 resource: AssetResource,
79});
80export type AssetCreateUrlInput = typeof AssetCreateUrlInput.Type;
81​
82export const AssetImageDimensions = Schema.Struct({
83 width: NonNegativeInt.check(Schema.isGreaterThanOrEqualTo(1)),
84 height: NonNegativeInt.check(Schema.isGreaterThanOrEqualTo(1)),
85});
86export type AssetImageDimensions = typeof AssetImageDimensions.Type;
87​
88export const AssetCreateUrlResult = Schema.Struct({
89 relativeUrl: TrimmedNonEmptyString.check(Schema.isMaxLength(4096)),
90 expiresAt: Schema.Number,
91 sourcePath: Schema.optional(
92 TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
93 ),
94 imageDimensions: Schema.optional(AssetImageDimensions),
95 /** Changes only when the file's contents or identity change, unlike `relativeUrl`, which is re-signed on every mint. */
96 fileRevision: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(256))),
97});
98export type AssetCreateUrlResult = typeof AssetCreateUrlResult.Type;
99​
100export const ATTACHMENT_UPLOAD_URL_TTL_MS = 10 * 60_000;
101​
102const ImageAttachmentCreateUploadUrlInput = Schema.Struct({
103 type: Schema.optionalKey(Schema.Literal("image")),
104 name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)),
105 mimeType: Schema.Literals(PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES),
106 sizeBytes: NonNegativeInt.check(
107 Schema.isGreaterThanOrEqualTo(1),
108 Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES),
109 ),
110});
111​
112const FileAttachmentCreateUploadUrlInput = Schema.Struct({
113 type: Schema.Literal("file"),
114 name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)),
115 mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100)),
116 sizeBytes: NonNegativeInt.check(
117 Schema.isGreaterThanOrEqualTo(1),
118 Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_FILE_BYTES),
119 ),
120});
121​
122export const AttachmentCreateUploadUrlInput = Schema.Union([
123 ImageAttachmentCreateUploadUrlInput,
124 FileAttachmentCreateUploadUrlInput,
125]);
126export type AttachmentCreateUploadUrlInput = typeof AttachmentCreateUploadUrlInput.Type;
127​
128export const AttachmentCreateUploadUrlResult = Schema.Struct({
129 attachmentId: TrimmedNonEmptyString.check(Schema.isMaxLength(256)),
130 relativeUrl: TrimmedNonEmptyString.check(Schema.isMaxLength(4096)),
131 expiresAt: Schema.Number,
132});
133export type AttachmentCreateUploadUrlResult = typeof AttachmentCreateUploadUrlResult.Type;
134​
135export const AttachmentDeleteInput = Schema.Struct({
136 attachmentId: TrimmedNonEmptyString.check(Schema.isMaxLength(256)),
137});
138export type AttachmentDeleteInput = typeof AttachmentDeleteInput.Type;
139​
140export class AttachmentUploadSigningKeyError extends Schema.TaggedError<AttachmentUploadSigningKeyError>()(
141 "AttachmentUploadSigningKeyError",
142 {
143 cause: Schema.Defect(),
144 },
145) {
146 override get message(): string {
147 return "Failed to load the attachment upload signing key.";
148 }
150​
151export class AssetWorkspaceContextNotFoundError extends Schema.TaggedError<AssetWorkspaceContextNotFoundError>()(
152 "AssetWorkspaceContextNotFoundError",
153 {
154 resource: AssetResource,
155 },
156) {
157 override get message(): string {
158 return "Workspace context was not found.";
159 }
161​
162export class AssetWorkspaceContextResolutionError extends Schema.TaggedError<AssetWorkspaceContextResolutionError>()(
163 "AssetWorkspaceContextResolutionError",
164 {
165 resource: AssetResource,
166 cause: Schema.Defect(),
167 },
168) {
169 override get message(): string {
170 return "Failed to resolve workspace context.";
171 }
173​
174export class AssetWorkspaceRootNormalizationError extends Schema.TaggedError<AssetWorkspaceRootNormalizationError>()(
175 "AssetWorkspaceRootNormalizationError",
176 {
177 resource: AssetResource,
178 cause: Schema.Defect(),
179 },
180) {
181 override get message(): string {
182 return "Failed to normalize the workspace root.";
183 }
185​
186export class AssetWorkspacePathValidationError extends Schema.TaggedError<AssetWorkspacePathValidationError>()(
187 "AssetWorkspacePathValidationError",
188 {
189 resource: AssetResource,
190 cause: Schema.Defect(),
191 },
192) {
193 override get message(): string {
194 return "Workspace file path must be relative to the project root.";
195 }
197​
198export class AssetPreviewTypeValidationError extends Schema.TaggedError<AssetPreviewTypeValidationError>()(
199 "AssetPreviewTypeValidationError",
200 {
201 resource: AssetResource,
202 },
203) {
204 override get message(): string {
205 return this.resource._tag === "media-file" || this.resource._tag === "draft-workspace-file"
206 ? "Only images, videos, audio, HTML, and PDF files can be previewed."
207 : "Only browser documents and images can be previewed.";
208 }
210​
211export class AssetWorkspaceAssetInspectionError extends Schema.TaggedError<AssetWorkspaceAssetInspectionError>()(
212 "AssetWorkspaceAssetInspectionError",
213 {
214 resource: AssetResource,
215 cause: Schema.Defect(),
216 },
217) {
218 override get message(): string {
219 return this.resource._tag === "media-file"
220 ? "Failed to inspect the media file."
221 : "Failed to inspect the workspace asset.";
222 }
224​
225export class AssetWorkspaceAssetNotFoundError extends Schema.TaggedError<AssetWorkspaceAssetNotFoundError>()(
226 "AssetWorkspaceAssetNotFoundError",
227 {
228 resource: AssetResource,
229 // Absolute path after home/workspace resolution; absent on older servers.
230 checkedPath: Schema.optionalKey(Schema.String),
231 },
232) {
233 override get message(): string {
234 return this.resource._tag === "media-file"
235 ? "Media file was not found."
236 : this.resource._tag === "tool-output-image"
237 ? "Tool output image was not found."
238 : "Workspace asset was not found.";
239 }
241​
242export class AssetWorkspaceResolutionError extends Schema.TaggedError<AssetWorkspaceResolutionError>()(
243 "AssetWorkspaceResolutionError",
244 {
245 resource: AssetResource,
246 cause: Schema.Defect(),
247 },
248) {
249 override get message(): string {
250 return "Failed to resolve workspace.";
251 }
253​
254export class AssetAttachmentNotFoundError extends Schema.TaggedError<AssetAttachmentNotFoundError>()(
255 "AssetAttachmentNotFoundError",
256 {
257 resource: AssetResource,
258 },
259) {
260 override get message(): string {
261 return "Attachment was not found.";
262 }
264​
265export class AssetProjectFaviconResolutionError extends Schema.TaggedError<AssetProjectFaviconResolutionError>()(
266 "AssetProjectFaviconResolutionError",
267 {
268 resource: AssetResource,
269 cause: Schema.Defect(),
270 },
271) {
272 override get message(): string {
273 return "Failed to resolve project favicon.";
274 }
276​
277export class AssetProjectFaviconInspectionError extends Schema.TaggedError<AssetProjectFaviconInspectionError>()(
278 "AssetProjectFaviconInspectionError",
279 {
280 resource: AssetResource,
281 cause: Schema.Defect(),
282 },
283) {
284 override get message(): string {
285 return "Failed to inspect the project favicon.";
286 }
288​
289export class AssetProjectFaviconNotFoundError extends Schema.TaggedError<AssetProjectFaviconNotFoundError>()(
290 "AssetProjectFaviconNotFoundError",
291 {
292 resource: AssetResource,
293 },
294) {
295 override get message(): string {
296 return "Project favicon was not found.";
297 }
299​
300export class AssetSigningKeyLoadError extends Schema.TaggedError<AssetSigningKeyLoadError>()(
301 "AssetSigningKeyLoadError",
302 {
303 resource: AssetResource,
304 cause: Schema.Defect(),
305 },
306) {
307 override get message(): string {
308 return "Failed to load the asset signing key.";
309 }
311​
312// The tag predates the rename and stays: clients on either side of an update decode it.
313export class AssetSourceControlMediaUrlValidationError extends Schema.TaggedError<AssetSourceControlMediaUrlValidationError>()(
314 "AssetGitHubMediaUrlValidationError",
315 {},
316) {
317 override get message(): string {
318 return "Only media a source control host keeps behind a credential can be fetched with one.";
319 }
321​
322export const AssetAccessError = Schema.Union([
323 AssetWorkspaceContextNotFoundError,
324 AssetWorkspaceContextResolutionError,
325 AssetWorkspaceRootNormalizationError,
326 AssetWorkspacePathValidationError,
327 AssetPreviewTypeValidationError,
328 AssetWorkspaceAssetInspectionError,
329 AssetWorkspaceAssetNotFoundError,
330 AssetWorkspaceResolutionError,
331 AssetAttachmentNotFoundError,
332 AssetProjectFaviconResolutionError,
333 AssetProjectFaviconInspectionError,
334 AssetProjectFaviconNotFoundError,
335 AssetSourceControlMediaUrlValidationError,
336 AssetSigningKeyLoadError,
337]);
338export type AssetAccessError = typeof AssetAccessError.Type;