fix(core): handle missing read paths (#33255)
This commit is contained in:
@@ -57,8 +57,8 @@ export const layer = Layer.effectDiscard(
|
|||||||
const selected = path.isAbsolute(input.path) ? path.dirname(absolute) : location.directory
|
const selected = path.isAbsolute(input.path) ? path.dirname(absolute) : location.directory
|
||||||
if (!path.isAbsolute(input.path) && !FSUtil.contains(location.directory, absolute))
|
if (!path.isAbsolute(input.path) && !FSUtil.contains(location.directory, absolute))
|
||||||
return yield* Effect.die(new Error("Path escapes the allowed read root"))
|
return yield* Effect.die(new Error("Path escapes the allowed read root"))
|
||||||
const real = yield* fs.realPath(absolute).pipe(Effect.orDie)
|
const real = yield* fs.realPath(absolute)
|
||||||
const root = yield* fs.realPath(selected).pipe(Effect.orDie)
|
const root = yield* fs.realPath(selected)
|
||||||
if (!FSUtil.contains(root, real))
|
if (!FSUtil.contains(root, real))
|
||||||
return yield* Effect.die(new Error("Path escapes the allowed read root"))
|
return yield* Effect.die(new Error("Path escapes the allowed read root"))
|
||||||
const resource = path.relative(root, real).replaceAll("\\", "/") || "."
|
const resource = path.relative(root, real).replaceAll("\\", "/") || "."
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect } from "bun:test"
|
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 { Config } from "@opencode-ai/core/config"
|
||||||
import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
|
import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
|
||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||||
@@ -18,6 +18,8 @@ import { testEffect } from "./lib/effect"
|
|||||||
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
||||||
|
|
||||||
const assertions: PermissionV2.AssertInput[] = []
|
const assertions: PermissionV2.AssertInput[] = []
|
||||||
|
const missingPath = "__missing_read_target__.txt"
|
||||||
|
const missingAbsolutePath = `${process.cwd()}/${missingPath}`
|
||||||
const readCalls: {
|
const readCalls: {
|
||||||
input: AbsolutePath
|
input: AbsolutePath
|
||||||
page: ReadToolFileSystem.PageInput
|
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 image = Image.layer.pipe(Layer.provide(config))
|
||||||
const testFileSystem = Layer.effect(
|
const testFileSystem = Layer.effect(
|
||||||
FSUtil.Service,
|
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))
|
).pipe(Layer.provide(FSUtil.defaultLayer))
|
||||||
const infrastructure = Layer.mergeAll(
|
const infrastructure = Layer.mergeAll(
|
||||||
testFileSystem,
|
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", () =>
|
it.effect("lists a bounded directory page through read", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
resolvedType = "directory"
|
resolvedType = "directory"
|
||||||
|
|||||||
Reference in New Issue
Block a user