| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| export const ARTIFACT_KINDS = ['file', 'diff', 'html', 'image', 'pdf'] as const; |
| |
| export type ArtifactKind = (typeof ARTIFACT_KINDS)[number]; |
| |
| /** Maximum encoded image payload admitted to a renderer preview. */ |
| export const ARTIFACT_IMAGE_PREVIEW_MAX_BYTES = 2 * 1024 * 1024; |
| |
| const ARTIFACT_IMAGE_PREVIEW_MIME_BY_EXTENSION: Readonly<Record<string, string>> = { |
| '.png': 'image/png', |
| '.jpg': 'image/jpeg', |
| '.jpeg': 'image/jpeg', |
| '.gif': 'image/gif', |
| '.webp': 'image/webp', |
| '.avif': 'image/avif', |
| }; |
| |
| const ARTIFACT_IMAGE_PREVIEW_MIMES = new Set( |
| Object.values(ARTIFACT_IMAGE_PREVIEW_MIME_BY_EXTENSION), |
| ); |
| |
| export interface ArtifactImagePreviewInput { |
| name: string; |
| kind: ArtifactKind; |
| mimeType?: string; |
| sizeBytes?: number; |
| } |
| |
| export type ArtifactImagePreviewResolution = |
| | { kind: 'image'; reason: 'mime_match' | 'ext_fallback' } |
| | { |
| kind: 'unsupported'; |
| reason: 'kind_disallowed' | 'mime_disallowed' | 'no_mime_no_ext' | 'oversize'; |
| }; |
| |
| /** Normalize the raster MIME admitted to renderer image previews. */ |
| export function normalizeArtifactImagePreviewMime( |
| mimeType: string | undefined, |
| name?: string, |
| ): string | null { |
| if (typeof mimeType === 'string' && mimeType.trim() !== '') { |
| const normalized = mimeType.trim().toLowerCase(); |
| return ARTIFACT_IMAGE_PREVIEW_MIMES.has(normalized) ? normalized : null; |
| } |
| if (!name) return null; |
| const dot = name.lastIndexOf('.'); |
| if (dot <= 0 || dot === name.length - 1) return null; |
| return ARTIFACT_IMAGE_PREVIEW_MIME_BY_EXTENSION[name.slice(dot).toLowerCase()] ?? null; |
| } |
| |
| /** One metadata policy shared by preview admission and renderer presentation. */ |
| export function resolveArtifactImagePreview( |
| input: ArtifactImagePreviewInput, |
| ): ArtifactImagePreviewResolution { |
| if (input.kind !== 'image') { |
| return { kind: 'unsupported', reason: 'kind_disallowed' }; |
| } |
| if (input.sizeBytes !== undefined && input.sizeBytes > ARTIFACT_IMAGE_PREVIEW_MAX_BYTES) { |
| return { kind: 'unsupported', reason: 'oversize' }; |
| } |
| if (input.mimeType) { |
| return normalizeArtifactImagePreviewMime(input.mimeType) |
| ? { kind: 'image', reason: 'mime_match' } |
| : { kind: 'unsupported', reason: 'mime_disallowed' }; |
| } |
| return normalizeArtifactImagePreviewMime(undefined, input.name) |
| ? { kind: 'image', reason: 'ext_fallback' } |
| : { kind: 'unsupported', reason: 'no_mime_no_ext' }; |
| } |
| |
| export const ARTIFACT_SOURCES = [ |
| 'tool_result', |
| 'tool_result_projection', |
| 'tool_result_archive', |
| 'subagent_writeback', |
| 'deep_research', |
| 'user_upload', |
| 'session_effect', |
| ] as const; |
| |
| export type ArtifactSource = (typeof ARTIFACT_SOURCES)[number]; |
| |
| export const ARTIFACT_ENTITY_ID_MAX_CHARS = 128; |
| export const ARTIFACT_TURN_KEY_MAX_CHARS = 512; |
| |
| const ARTIFACT_ENTITY_ID_PATTERN = new RegExp(`^[A-Za-z0-9_-]{1,${ARTIFACT_ENTITY_ID_MAX_CHARS}}$`); |
| const ARTIFACT_TURN_KEY_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/; |
| |
| export function isCanonicalArtifactEntityId(value: unknown): value is string { |
| return typeof value === 'string' && ARTIFACT_ENTITY_ID_PATTERN.test(value); |
| } |
| |
| export function isArtifactTurnKey(value: unknown): value is string { |
| return ( |
| typeof value === 'string' && |
| value.length > 0 && |
| value.length <= ARTIFACT_TURN_KEY_MAX_CHARS && |
| !ARTIFACT_TURN_KEY_CONTROL_CHARACTERS.test(value) |
| ); |
| } |
| |
| export interface ArtifactDescriptor { |
| id: string; |
| sessionId: string; |
| /** Opaque, bounded Runtime turn key. It is a reference, not a filesystem path component. */ |
| turnId: string; |
| createdAt: number; |
| name: string; |
| kind: ArtifactKind; |
| sizeBytes: number; |
| mimeType?: string; |
| source: ArtifactSource; |
| summary?: string; |
| } |
| |
| export interface ArtifactRecord extends ArtifactDescriptor { |
| /** |
| * Artifact-root-relative path. Never absolute and never exposed as a |
| * filesystem path to renderer code. |
| */ |
| relativePath: string; |
| /** Durable role for artifacts owned by a Deep Research workspace. */ |
| deepResearchRole?: import('./deep-research-run.js').DeepResearchArtifactRole; |
| } |
| |
| interface ArtifactSourcePolicy { |
| readonly userDeletable: boolean; |
| readonly userVisible: boolean; |
| readonly sharedReadable: boolean; |
| } |
| |
| const ARTIFACT_SOURCE_POLICIES = { |
| tool_result: { userDeletable: true, userVisible: false, sharedReadable: true }, |
| tool_result_projection: { userDeletable: false, userVisible: false, sharedReadable: true }, |
| tool_result_archive: { userDeletable: false, userVisible: false, sharedReadable: false }, |
| subagent_writeback: { userDeletable: false, userVisible: true, sharedReadable: false }, |
| deep_research: { userDeletable: false, userVisible: true, sharedReadable: false }, |
| user_upload: { userDeletable: true, userVisible: false, sharedReadable: true }, |
| session_effect: { userDeletable: false, userVisible: false, sharedReadable: false }, |
| } as const satisfies Record<ArtifactSource, ArtifactSourcePolicy>; |
| |
| const CHILD_RESULT_OUTPUT_SOURCES = new Set<ArtifactSource>([ |
| 'tool_result', |
| 'tool_result_projection', |
| 'subagent_writeback', |
| 'deep_research', |
| ]); |
| |
| export function isArtifactUserVisible( |
| record: Pick<ArtifactRecord, 'source'> & Partial<Pick<ArtifactRecord, 'kind'>>, |
| ): boolean { |
| // A directly written HTML file is an intentional user-facing deliverable: |
| // the Artifact Pane must be able to preview and open it without requiring a |
| // child-workspace writeback. Other tool results remain internal to avoid |
| // flooding the Generated Files tab with command output and diffs. |
| if (record.source === 'tool_result' && record.kind === 'html') return true; |
| return ARTIFACT_SOURCE_POLICIES[record.source].userVisible; |
| } |
| |
| /** Visibility does not grant permission to destroy evidence owned by a runtime workflow. */ |
| export function canUserDeleteArtifact(record: Pick<ArtifactRecord, 'source'>): boolean { |
| return ARTIFACT_SOURCE_POLICIES[record.source].userDeletable; |
| } |
| |
| export function isArtifactSharedSessionReadable(record: Pick<ArtifactRecord, 'source'>): boolean { |
| return ARTIFACT_SOURCE_POLICIES[record.source].sharedReadable; |
| } |
| |
| export function isArtifactChildResultOutput(record: Pick<ArtifactRecord, 'source'>): boolean { |
| return CHILD_RESULT_OUTPUT_SOURCES.has(record.source); |
| } |
| |
| export type ArtifactReadFailureReason = 'not_found' | 'too_large' | 'read_failed' | 'not_allowed'; |
| |
| export type ArtifactBinaryReadFailureReason = ArtifactReadFailureReason | 'unsupported_mime'; |
| |
| export type ArtifactTextReadResult = |
| | { ok: true; text: string } |
| | { ok: false; reason: ArtifactReadFailureReason }; |
| |
| export type ArtifactBinaryReadResult = |
| | { ok: true; base64: string; mimeType: string } |
| | { ok: false; reason: ArtifactBinaryReadFailureReason }; |
| |
| export type ArtifactSaveFailureReason = |
| | 'canceled' |
| | 'not_found' |
| | 'not_allowed' |
| | 'write_failed' |
| | 'deleted' |
| | 'source_failed' |
| | 'size_mismatch' |
| | 'target_write_failed' |
| | 'replace_failed'; |
| |
| export type ArtifactSaveResult = |
| | { ok: true; saved: string } |
| | { ok: false; reason: ArtifactSaveFailureReason }; |