refactor(core): simplify location filesystem (#31545)

This commit is contained in:
Dax
2026-06-09 14:28:45 -04:00
committed by GitHub
parent 0777cf1ccf
commit 132ef57272
73 changed files with 1054 additions and 1422 deletions
+35 -17
View File
@@ -1,31 +1,38 @@
export * as ReadTool from "./read"
import { ToolFailure } from "@opencode-ai/llm"
import path from "path"
import { Effect, Layer, Schema } from "effect"
import { FileSystem } from "../filesystem"
import { FSUtil } from "../fs-util"
import { Image } from "../image"
import { Location } from "../location"
import { PermissionV2 } from "../permission"
import { AbsolutePath } from "../schema"
import { ReadToolFileSystem } from "./read-filesystem"
import { Tool } from "./tool"
import { Tools } from "./tools"
export const name = "read"
const SUPPORTED_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"])
const LocationInput = Schema.Struct({
...FileSystem.ReadInput.fields,
offset: FileSystem.ListPageInput.fields.offset.annotate({
path: Schema.String,
offset: ReadToolFileSystem.PageInput.fields.offset.annotate({
description: "The 1-based directory entry or text line offset to start reading from",
}),
limit: FileSystem.ListPageInput.fields.limit.annotate({
limit: ReadToolFileSystem.PageInput.fields.limit.annotate({
description: "The maximum number of directory entries or text lines to read",
}),
})
const Input = LocationInput
const Output = Schema.Union([FileSystem.Content, FileSystem.TextPage, FileSystem.ListPage])
const Output = Schema.Union([FileSystem.Content, ReadToolFileSystem.TextPage, ReadToolFileSystem.ListPage])
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const filesystem = yield* FileSystem.Service
const fs = yield* FSUtil.Service
const reader = yield* ReadToolFileSystem.Service
const location = yield* Location.Service
const image = yield* Image.Service
const permission = yield* PermissionV2.Service
@@ -33,11 +40,12 @@ export const layer = Layer.effectDiscard(
.register({
[name]: Tool.make({
description:
"Read a text file or supported image, page through a large UTF-8 text file by line offset, or list a directory page relative to the current location. Absolute paths are accepted only for managed tool-output files.",
"Read a text file or supported image, page through a large UTF-8 text file by line offset, or list a directory page. Relative paths resolve from the current location; absolute paths are read directly.",
input: Input,
output: Output,
toModelOutput: ({ input, output }) => {
if (!("type" in output) || output.type !== "binary" || !SUPPORTED_IMAGE_MIMES.has(output.mime)) return []
if (!("encoding" in output) || output.encoding !== "base64" || !SUPPORTED_IMAGE_MIMES.has(output.mime))
return []
return [
{ type: "text", text: "Image read successfully" },
{ type: "file", data: output.content, mime: output.mime, name: input.path },
@@ -45,33 +53,43 @@ export const layer = Layer.effectDiscard(
},
execute: (input, context) => {
return Effect.gen(function* () {
const resolved = yield* filesystem.resolveReadPath(input)
const absolute = path.resolve(location.directory, input.path)
const selected = path.isAbsolute(input.path) ? path.dirname(absolute) : location.directory
if (!path.isAbsolute(input.path) && !FSUtil.contains(location.directory, absolute))
return yield* Effect.die(new Error("Path escapes the allowed read root"))
const real = yield* fs.realPath(absolute).pipe(Effect.orDie)
const root = yield* fs.realPath(selected).pipe(Effect.orDie)
if (!FSUtil.contains(root, real)) return yield* Effect.die(new Error("Path escapes the allowed read root"))
const resource = path.relative(root, real).replaceAll("\\", "/") || "."
const target = AbsolutePath.make(real)
const type = yield* reader.inspect(target)
yield* permission.assert({
action: name,
resources: [resolved.resource],
resources: [resource],
save: ["*"],
sessionID: context.sessionID,
agent: context.agent,
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
})
if (resolved.type === "directory") return yield* filesystem.listPage(input)
const content = yield* filesystem.readTool(input, {
if (type === "directory")
return yield* reader.list(target, { offset: input.offset, limit: input.limit })
const content = yield* reader.read(target, resource, {
offset: input.offset,
limit: input.limit,
})
if (content.type === "binary" && SUPPORTED_IMAGE_MIMES.has(content.mime)) {
if ("encoding" in content && content.encoding === "base64" && SUPPORTED_IMAGE_MIMES.has(content.mime)) {
return yield* image
.normalize(resolved.resource, content)
.normalize(resource, { ...content, encoding: "base64" })
.pipe(Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(content)))
}
if (content.type === "binary")
return yield* Effect.fail(new FileSystem.BinaryFileError(resolved.resource))
if ("encoding" in content && content.encoding === "base64")
return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError(resource))
return content
}).pipe(
Effect.mapError((error) => {
const message =
error instanceof FileSystem.BinaryFileError ||
error instanceof FileSystem.MediaIngestLimitError ||
error instanceof ReadToolFileSystem.BinaryFileError ||
error instanceof ReadToolFileSystem.MediaIngestLimitError ||
error instanceof Image.DecodeError ||
error instanceof Image.SizeError
? error.message