mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-30 06:32:02 +00:00
fix(core): treat project paths as internal (#45799)
This commit is contained in:
parent
0ce3214844
commit
7111e71528
4 changed files with 53 additions and 16 deletions
|
|
@ -16,7 +16,8 @@ export type Kind = typeof Kind.Type
|
|||
/**
|
||||
* Mutation paths do not accept project references. A leading `~` expands to
|
||||
* the home directory; other relative paths resolve from the active Location.
|
||||
* Paths outside it require separate `external_directory` approval.
|
||||
* Paths outside it and its non-root project worktree require separate
|
||||
* `external_directory` approval.
|
||||
*/
|
||||
export const ResolveInput = Schema.Struct({
|
||||
path: Schema.String,
|
||||
|
|
@ -52,8 +53,8 @@ export interface Interface {
|
|||
/**
|
||||
* Resolve a path and derive its permission resources. A leading `~` expands
|
||||
* to the home directory; other relative paths resolve from the Location.
|
||||
* Paths outside it require separate `external_directory` approval. This does
|
||||
* not approve the mutation.
|
||||
* Paths outside it and its non-root project worktree require separate
|
||||
* `external_directory` approval. This does not approve the mutation.
|
||||
*/
|
||||
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, FSUtil.Error>
|
||||
}
|
||||
|
|
@ -82,7 +83,11 @@ const layer = Layer.effect(
|
|||
|
||||
const resolve = Effect.fnUntraced(function* (input: ResolveInput) {
|
||||
const absolute = resolvePath(location.directory, input.path)
|
||||
if (FSUtil.contains(location.directory, absolute)) {
|
||||
const worktree = path.resolve(location.project.directory)
|
||||
const internal =
|
||||
FSUtil.contains(location.directory, absolute) ||
|
||||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
|
||||
if (internal) {
|
||||
return {
|
||||
absolute,
|
||||
resource: slash(path.relative(location.directory, absolute) || "."),
|
||||
|
|
|
|||
|
|
@ -11,12 +11,20 @@ import { tmpdir } from "./fixture/tmpdir"
|
|||
import { location } from "./fixture/location"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
function provide(directory: string) {
|
||||
function provide(directory: string, projectDirectory = directory) {
|
||||
return Effect.provide(
|
||||
LayerNode.compile(LocationMutation.node, [
|
||||
[
|
||||
Location.node,
|
||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
|
||||
Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
location(
|
||||
{ directory: AbsolutePath.make(directory) },
|
||||
{ projectDirectory: AbsolutePath.make(projectDirectory) },
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
|
@ -76,6 +84,30 @@ describe("LocationMutation", () => {
|
|||
),
|
||||
)
|
||||
|
||||
it.live("allows a relative path outside the Location but inside the project worktree", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const active = path.join(directory, "packages", "opencode")
|
||||
yield* Effect.promise(() => fs.mkdir(active, { recursive: true }))
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: "../../README.md" })
|
||||
expect(target).toMatchObject({
|
||||
absolute: path.join(directory, "README.md"),
|
||||
resource: "../../README.md",
|
||||
})
|
||||
expect(target.externalDirectory).toBeUndefined()
|
||||
}).pipe(provide(path.join(directory, "packages", "opencode"), directory)),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("does not treat a filesystem-root project sentinel as an internal boundary", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: "../outside.txt" })
|
||||
expect(target.externalDirectory).toBeDefined()
|
||||
}).pipe(provide(directory, path.parse(directory).root)),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("resolves a prospective target below an external symlink lexically", () =>
|
||||
withTmp((directory) => {
|
||||
const outside = `${directory}-outside`
|
||||
|
|
|
|||
|
|
@ -920,7 +920,7 @@ describe("PatchTool", () => {
|
|||
),
|
||||
)
|
||||
|
||||
it.live("treats a sibling path inside the project worktree as external to the Location", () =>
|
||||
it.live("treats a sibling path inside the project worktree as internal", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
|
|
@ -939,9 +939,8 @@ describe("PatchTool", () => {
|
|||
call("*** Begin Patch\n*** Update File: ../sibling.txt\n@@\n-before\n+after\n*** End Patch"),
|
||||
),
|
||||
).toMatchObject({ status: "completed" })
|
||||
expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])
|
||||
expect(assertions[0]?.resources).toEqual([path.join(tmp.path, "*").replaceAll("\\", "/")])
|
||||
expect(assertions[1]?.resources).toEqual([target.replaceAll("\\", "/")])
|
||||
expect(assertions.map((input) => input.action)).toEqual(["edit"])
|
||||
expect(assertions[0]?.resources).toEqual(["../sibling.txt"])
|
||||
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\n")
|
||||
}),
|
||||
tmp.path,
|
||||
|
|
|
|||
|
|
@ -83,10 +83,10 @@ current built-in actions use these resources:
|
|||
|
||||
## External directories
|
||||
|
||||
An external path requires a separate `external_directory` decision before the
|
||||
tool's own `read` or `edit` decision. This applies to external paths used by
|
||||
`read`, `edit`, `write`, and `patch`, and to an external `shell` working
|
||||
directory.
|
||||
A path outside both the active Location and its non-root project worktree
|
||||
requires a separate `external_directory` decision before the tool's own `read`
|
||||
or `edit` decision. This applies to external paths used by `read`, `edit`,
|
||||
`write`, and `patch`, and to an external `shell` working directory.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
|
|
@ -122,8 +122,9 @@ raw command text and are **not** home-expanded.
|
|||
identify every dangerous command.
|
||||
</Callout>
|
||||
|
||||
Relative mutation paths cannot escape the active Location, and symlink escapes
|
||||
from inside it are rejected. Explicit external paths are canonicalized before
|
||||
Relative mutation paths may traverse outside the active Location while
|
||||
remaining inside its project worktree. Paths outside both boundaries require
|
||||
`external_directory` approval. Explicit external paths are canonicalized before
|
||||
matching, so authorize only trusted directory boundaries.
|
||||
|
||||
## Experimental shell scanner
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue