22// Deliberately no write/delete/upload surface: mutations need their own
33// reviewed contract; see the allowlisted agents.files.* API for edits.
44import path from "node:path" ;
5+ import { detectMime } from "@openclaw/media-core/mime" ;
56import {
67 ErrorCodes ,
78 errorShape ,
@@ -32,17 +33,27 @@ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
3233const DEFAULT_LIST_LIMIT = 250 ;
3334const MAX_LIST_LIMIT = 500 ;
3435
35- const IMAGE_MIME_BY_EXTENSION : Record < string , string > = {
36- ".avif" : "image/avif" ,
37- ".bmp" : "image/bmp" ,
38- ".gif" : "image/gif" ,
39- ".heic" : "image/heic" ,
40- ".heif" : "image/heif" ,
41- ".jpeg" : "image/jpeg" ,
42- ".jpg" : "image/jpeg" ,
43- ".png" : "image/png" ,
44- ".webp" : "image/webp" ,
45- } ;
36+ const IMAGE_EXTENSIONS = new Set ( [
37+ ".avif" ,
38+ ".bmp" ,
39+ ".gif" ,
40+ ".heic" ,
41+ ".heif" ,
42+ ".jpeg" ,
43+ ".jpg" ,
44+ ".png" ,
45+ ".webp" ,
46+ ] ) ;
47+ const SUPPORTED_IMAGE_MIME_TYPES = new Set ( [
48+ "image/avif" ,
49+ "image/bmp" ,
50+ "image/gif" ,
51+ "image/heic" ,
52+ "image/heif" ,
53+ "image/jpeg" ,
54+ "image/png" ,
55+ "image/webp" ,
56+ ] ) ;
4657
4758function workspaceError ( type : string , message : string , details ?: Record < string , unknown > ) {
4859 return errorShape ( ErrorCodes . INVALID_REQUEST , message , {
@@ -183,8 +194,8 @@ export const agentsWorkspaceHandlers: GatewayRequestHandlers = {
183194 respondNotFound ( ) ;
184195 return ;
185196 }
186- const imageMime = IMAGE_MIME_BY_EXTENSION [ path . extname ( browserPath ) . toLowerCase ( ) ] ;
187- const maxBytes = imageMime ? MAX_IMAGE_BYTES : WORKSPACE_PREVIEW_MAX_BYTES ;
197+ const expectsImage = IMAGE_EXTENSIONS . has ( path . extname ( browserPath ) . toLowerCase ( ) ) ;
198+ const maxBytes = expectsImage ? MAX_IMAGE_BYTES : WORKSPACE_PREVIEW_MAX_BYTES ;
188199 const read =
189200 stat . size > maxBytes
190201 ? "too-large"
@@ -205,8 +216,7 @@ export const agentsWorkspaceHandlers: GatewayRequestHandlers = {
205216 respondNotFound ( ) ;
206217 return ;
207218 }
208- const text = imageMime ? undefined : decodeUtf8Strict ( read . buffer ) ;
209- if ( ! imageMime && text === undefined ) {
219+ const respondUnsupported = ( ) => {
210220 respond (
211221 false ,
212222 undefined ,
@@ -216,6 +226,34 @@ export const agentsWorkspaceHandlers: GatewayRequestHandlers = {
216226 { path : browserPath } ,
217227 ) ,
218228 ) ;
229+ } ;
230+ // The extension only picks the byte cap; content decides what leaves the
231+ // gateway. Magic-byte sniffing (no filename hints) keeps renamed binaries
232+ // from riding the image path past the UTF-8 text gate.
233+ if ( expectsImage ) {
234+ const sniffedMime = await detectMime ( { buffer : read . buffer } ) ;
235+ if ( ! sniffedMime || ! SUPPORTED_IMAGE_MIME_TYPES . has ( sniffedMime ) ) {
236+ respondUnsupported ( ) ;
237+ return ;
238+ }
239+ respond ( true , {
240+ agentId,
241+ workspace : workspaceDir ,
242+ file : {
243+ path : browserPath ,
244+ name : path . basename ( browserPath ) ,
245+ size : read . stat . size ,
246+ updatedAtMs : toUpdatedAtMs ( read . stat . mtimeMs ) ,
247+ mimeType : sniffedMime ,
248+ encoding : "base64" as const ,
249+ content : read . buffer . toString ( "base64" ) ,
250+ } ,
251+ } ) ;
252+ return ;
253+ }
254+ const text = decodeUtf8Strict ( read . buffer ) ;
255+ if ( text === undefined ) {
256+ respondUnsupported ( ) ;
219257 return ;
220258 }
221259 respond ( true , {
@@ -226,9 +264,9 @@ export const agentsWorkspaceHandlers: GatewayRequestHandlers = {
226264 name : path . basename ( browserPath ) ,
227265 size : read . stat . size ,
228266 updatedAtMs : toUpdatedAtMs ( read . stat . mtimeMs ) ,
229- mimeType : imageMime ?? "text/plain" ,
230- encoding : imageMime ? ( "base64" as const ) : ( " utf8" as const ) ,
231- content : imageMime ? read . buffer . toString ( "base64" ) : ( text as string ) ,
267+ mimeType : "text/plain" ,
268+ encoding : " utf8" as const ,
269+ content : text ,
232270 } ,
233271 } ) ;
234272 } ,
0 commit comments