fix(core): handle missing read paths (#33255)

This commit is contained in:
Kit Langton 2026-06-21 19:50:14 +02:00 committed by GitHub
parent 82d9cab48d
commit 823d327401
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 39 additions and 4 deletions

View File

@ -57,8 +57,8 @@ export const layer = Layer.effectDiscard(
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)
const real = yield* fs.realPath(absolute)
const root = yield* fs.realPath(selected)
if (!FSUtil.contains(root, real))
return yield* Effect.die(new Error("Path escapes the allowed read root"))
const resource = path.relative(root, real).replaceAll("\\", "/") || "."

View File

@ -1,5 +1,5 @@
import { beforeEach, describe, expect } from "bun:test"
import { Effect, Exit, Layer } from "effect"
import { Effect, Exit, Layer, PlatformError } from "effect"
import { Config } from "@opencode-ai/core/config"
import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
import { FileSystem } from "@opencode-ai/core/filesystem"
@ -18,6 +18,8 @@ import { testEffect } from "./lib/effect"
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
const assertions: PermissionV2.AssertInput[] = []
const missingPath = "__missing_read_target__.txt"
const missingAbsolutePath = `${process.cwd()}/${missingPath}`
const readCalls: {
input: AbsolutePath
page: ReadToolFileSystem.PageInput
@ -70,7 +72,24 @@ const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () =>
const image = Image.layer.pipe(Layer.provide(config))
const testFileSystem = Layer.effect(
FSUtil.Service,
FSUtil.Service.use((fs) => Effect.succeed(FSUtil.Service.of({ ...fs, realPath: (path) => Effect.succeed(path) }))),
FSUtil.Service.use((fs) =>
Effect.succeed(
FSUtil.Service.of({
...fs,
realPath: (path) =>
path === missingAbsolutePath
? Effect.fail(
PlatformError.systemError({
_tag: "NotFound",
module: "FileSystem",
method: "realPath",
pathOrDescriptor: path,
}),
)
: Effect.succeed(path),
}),
),
),
).pipe(Layer.provide(FSUtil.defaultLayer))
const infrastructure = Layer.mergeAll(
testFileSystem,
@ -453,6 +472,22 @@ describe("ReadTool", () => {
}),
)
it.effect("returns missing paths as model-visible tool failures", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
expect(
yield* executeTool(registry, {
sessionID,
...toolIdentity,
call: { type: "tool-call", id: "call-missing-path", name: "read", input: { path: missingPath } },
}),
).toEqual({ type: "error", value: `Unable to read ${missingPath}` })
expect(assertions).toEqual([])
expect(readCalls).toEqual([])
}),
)
it.effect("lists a bounded directory page through read", () =>
Effect.gen(function* () {
resolvedType = "directory"