Compare commits

...
1 Commits
Author SHA1 Message Date
Aiden Cline f81d771e47 fix(core): treat project paths as internal 2026-08-15 12:00:30 +00:00
20 changed files with 137 additions and 93 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ import { ConfigAgentV1 } from "../../v1/config/agent.js"
import { ConfigMigrateV1 } from "../../v1/config/migrate.js"
import { Global } from "@opencode-ai/util/global"
import { Permission } from "../../permission.js"
import type { LocationMutation } from "../../location-mutation.js"
import type { LocationPath } from "../../location-path.js"
import type { ReadTool } from "../../tool/plugin/read.js"
import type { EditTool } from "../../tool/plugin/edit.js"
import { AbsolutePath } from "../../schema.js"
@@ -28,7 +28,7 @@ const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info)
const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info)
const decodeConfig = Schema.decodeUnknownOption(Info)
type PathAction =
| LocationMutation.ExternalDirectoryAuthorization["action"]
| LocationPath.ExternalDirectoryAuthorization["action"]
| typeof ReadTool.name
| typeof EditTool.name
const pathActions = ["external_directory", "read", "edit"] as const satisfies readonly PathAction[]
@@ -1,4 +1,4 @@
export * as LocationMutation from "./location-mutation.js"
export * as LocationPath from "./location-path.js"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import path from "path"
@@ -12,8 +12,8 @@ export const Kind = Schema.Literals(["file", "directory"])
export type Kind = typeof Kind.Type
/**
* Mutation paths do not accept project references. Relative paths resolve
* from the active Location. Paths outside it require separate
* Tool paths do not accept project references. Relative paths resolve from
* the active Location. Paths outside its project require separate
* `external_directory` approval.
*/
export const ResolveInput = Schema.Struct({
@@ -49,13 +49,13 @@ export interface Target {
export interface Interface {
/**
* Resolve a path and derive its permission resources. Relative paths resolve
* from the Location. Paths outside it require separate `external_directory`
* approval. This does not approve the mutation.
* from the Location. Paths outside its project require separate
* `external_directory` approval. This does not approve access.
*/
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, FSUtil.Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationMutation") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationPath") {}
const slash = (value: string) => value.replaceAll("\\", "/")
@@ -65,9 +65,13 @@ const layer = Layer.effect(
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) {
const resolve = Effect.fn("LocationPath.resolve")(function* (input: ResolveInput) {
const absolute = path.resolve(location.directory, input.path)
if (FSUtil.contains(location.directory, absolute)) {
const projectRoot = path.parse(location.project.directory).root
if (
FSUtil.contains(location.directory, absolute) ||
(location.project.directory !== projectRoot && FSUtil.contains(location.project.directory, absolute))
) {
return {
absolute,
resource: slash(path.relative(location.directory, absolute) || "."),
+2 -2
View File
@@ -19,7 +19,7 @@ import { Image } from "./image.js"
import { LocationWatcher } from "./filesystem/location-watcher.js"
import { Integration } from "./integration.js"
import { Location } from "./location.js"
import { LocationMutation } from "./location-mutation.js"
import { LocationPath } from "./location-path.js"
import { LocationServiceMap } from "./location-service-map.js"
import { ModelResolver } from "./model-resolver.js"
import { MCP } from "./mcp/index.js"
@@ -76,7 +76,7 @@ const locationServiceNodes = [
Skill.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
LocationMutation.node,
LocationPath.node,
FileMutation.node,
Formatter.node,
MCP.node,
+4 -4
View File
@@ -32,7 +32,7 @@ import { InstructionDiscovery } from "../instruction-discovery.js"
import { Integration } from "../integration.js"
import { KV } from "../kv.js"
import { Location } from "../location.js"
import { LocationMutation } from "../location-mutation.js"
import { LocationPath } from "../location-path.js"
import { ModelsDev } from "../models-dev.js"
import { Npm } from "@opencode-ai/util/npm"
import { Permission } from "../permission.js"
@@ -92,7 +92,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const integration = yield* Integration.Service
const kv = yield* KV.Service
const location = yield* Location.Service
const locationMutation = yield* LocationMutation.Service
const locationMutation = yield* LocationPath.Service
const models = yield* ModelsDev.Service
const npm = yield* Npm.Service
const permission = yield* Permission.Service
@@ -129,7 +129,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(Integration.Service, integration),
Context.make(KV.Service, kv),
Context.make(Location.Service, location),
Context.make(LocationMutation.Service, locationMutation),
Context.make(LocationPath.Service, locationMutation),
Context.make(ModelsDev.Service, models),
Context.make(Npm.Service, npm),
Context.make(Permission.Service, permission),
@@ -173,7 +173,7 @@ export const requirements = LayerNode.group([
Integration.node,
KV.node,
Location.node,
LocationMutation.node,
LocationPath.node,
ModelsDev.node,
Npm.node,
Permission.node,
+3 -3
View File
@@ -16,7 +16,7 @@ import { Environment } from "../../environment/index.js"
import { FileMutation } from "../../file-mutation.js"
import { Formatter } from "../../formatter.js"
import { Location } from "../../location.js"
import { LocationMutation } from "../../location-mutation.js"
import { LocationPath } from "../../location-path.js"
import { Permission } from "../../permission.js"
import { fileDiff } from "./file-diff.js"
@@ -110,7 +110,7 @@ const findLineOccurrences = (content: string, search: string) => {
export const Plugin = {
id: "opencode.tool.edit",
effect: Effect.fn("EditTool.Plugin")(function* (ctx: PluginContext) {
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const fileMutation = yield* FileMutation.Service
const environment = yield* Environment.Service
const formatter = yield* Formatter.Service
@@ -148,7 +148,7 @@ export const Plugin = {
const external = target.externalDirectory
if (external) {
yield* permission.assert({
...LocationMutation.externalDirectoryPermission(external),
...LocationPath.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source: permissionSource,
+3 -3
View File
@@ -7,7 +7,7 @@ import path from "path"
import { Environment } from "../../environment/index.js"
import { FileSystem } from "../../filesystem.js"
import { Location } from "../../location.js"
import { LocationMutation } from "../../location-mutation.js"
import { LocationPath } from "../../location-path.js"
import { Ripgrep } from "../../ripgrep.js"
import { RelativePath } from "../../schema.js"
import { Permission } from "../../permission.js"
@@ -45,7 +45,7 @@ export const Plugin = {
const environment = yield* Environment.Service
const ripgrep = yield* Ripgrep.Service
const location = yield* Location.Service
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const permission = yield* Permission.Service
yield* ctx.tool
@@ -64,7 +64,7 @@ export const Plugin = {
const external = target.externalDirectory
if (external)
yield* permission.assert({
...LocationMutation.externalDirectoryPermission(external),
...LocationPath.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source,
+3 -3
View File
@@ -7,7 +7,7 @@ import path from "path"
import { Environment } from "../../environment/index.js"
import { FileSystem } from "../../filesystem.js"
import { Location } from "../../location.js"
import { LocationMutation } from "../../location-mutation.js"
import { LocationPath } from "../../location-path.js"
import { Permission } from "../../permission.js"
import { Ripgrep } from "../../ripgrep.js"
import { RelativePath } from "../../schema.js"
@@ -61,7 +61,7 @@ export const Plugin = {
const environment = yield* Environment.Service
const ripgrep = yield* Ripgrep.Service
const location = yield* Location.Service
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const permission = yield* Permission.Service
yield* ctx.tool
@@ -79,7 +79,7 @@ export const Plugin = {
const target = yield* mutation.resolve({ path: input.path ?? "." })
if (target.externalDirectory)
yield* permission.assert({
...LocationMutation.externalDirectoryPermission(target.externalDirectory),
...LocationPath.externalDirectoryPermission(target.externalDirectory),
sessionID: context.sessionID,
agent: context.agent,
source,
+7 -7
View File
@@ -10,7 +10,7 @@ import { Environment } from "../../environment/index.js"
import { Formatter } from "../../formatter.js"
import { FileMutation } from "../../file-mutation.js"
import { Location } from "../../location.js"
import { LocationMutation } from "../../location-mutation.js"
import { LocationPath } from "../../location-path.js"
import { Patch } from "@opencode-ai/util/patch"
import { Permission } from "../../permission.js"
import DESCRIPTION from "../patch.txt"
@@ -46,29 +46,29 @@ export const toModelOutput = (output: Output) =>
type Prepared =
| (Extract<Patch.Hunk, { readonly type: "add" }> & {
readonly target: LocationMutation.Target
readonly target: LocationPath.Target
readonly content: string
readonly before: string
readonly after: string
})
| (Extract<Patch.Hunk, { readonly type: "delete" }> & {
readonly target: LocationMutation.Target
readonly target: LocationPath.Target
readonly before: string
readonly after: string
})
| (Extract<Patch.Hunk, { readonly type: "update" }> & {
readonly target: LocationMutation.Target
readonly target: LocationPath.Target
readonly content: string
readonly before: string
readonly after: string
readonly moveTarget?: LocationMutation.Target
readonly moveTarget?: LocationPath.Target
})
export const Plugin = {
id: "opencode.tool.patch",
effect: Effect.fn("PatchTool.Plugin")(function* (ctx: PluginContext) {
const environment = yield* Environment.Service
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const fileMutation = yield* FileMutation.Service
const formatter = yield* Formatter.Service
const location = yield* Location.Service
@@ -116,7 +116,7 @@ export const Plugin = {
const target = yield* mutation.resolve({ path: value, kind: "file" })
if (!target.externalDirectory) return target
yield* permission.assert({
...LocationMutation.externalDirectoryPermission(target.externalDirectory),
...LocationPath.externalDirectoryPermission(target.externalDirectory),
metadata: {
filepath: target.absolute,
parentDir: target.externalDirectory.directory,
+3 -3
View File
@@ -6,7 +6,7 @@ import { ToolFailure } from "@opencode-ai/ai"
import { Effect, Schema } from "effect"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Location } from "../../location.js"
import { LocationMutation } from "../../location-mutation.js"
import { LocationPath } from "../../location-path.js"
import { Permission } from "../../permission.js"
import { SessionInstructions } from "../../session/instructions.js"
import { AbsolutePath } from "../../schema.js"
@@ -32,7 +32,7 @@ export const Plugin = {
id: "opencode.tool.read",
effect: Effect.fn("ReadTool.Plugin")(function* (ctx: PluginContext) {
const reader = yield* ReadToolFileSystem.Service
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const permission = yield* Permission.Service
const sessionInstructions = yield* SessionInstructions.Service
const fs = yield* FSUtil.Service
@@ -58,7 +58,7 @@ export const Plugin = {
const external = target.externalDirectory
if (external)
yield* permission.assert({
...LocationMutation.externalDirectoryPermission(external),
...LocationPath.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source,
+2 -2
View File
@@ -7,7 +7,7 @@ import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin
import { Deferred, Effect, Schema, Scope } from "effect"
import { Config } from "../../config.js"
import { Environment } from "../../environment/index.js"
import { LocationMutation } from "../../location-mutation.js"
import { LocationPath } from "../../location-path.js"
import { Permission } from "../../permission.js"
import { PluginRuntime } from "../../plugin/runtime.js"
import { NonNegativeInt } from "../../schema.js"
@@ -84,7 +84,7 @@ export const Plugin = {
const runtime = yield* PluginRuntime.Service
const scope = yield* Scope.Scope
const environment = yield* Environment.Service
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const shell = yield* Shell.Service
const permission = yield* Permission.Service
const config = yield* Config.Service
+3 -3
View File
@@ -13,7 +13,7 @@ import { Bom } from "@opencode-ai/util/bom"
import { Environment } from "../../environment/index.js"
import { FileMutation } from "../../file-mutation.js"
import { Formatter } from "../../formatter.js"
import { LocationMutation } from "../../location-mutation.js"
import { LocationPath } from "../../location-path.js"
import { Permission } from "../../permission.js"
import { fileDiff } from "./file-diff.js"
@@ -46,7 +46,7 @@ export const toModelOutput = (output: Output) =>
export const Plugin = {
id: "opencode.tool.write",
effect: Effect.fn("WriteTool.Plugin")(function* (ctx: PluginContext) {
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const fileMutation = yield* FileMutation.Service
const environment = yield* Environment.Service
const formatter = yield* Formatter.Service
@@ -72,7 +72,7 @@ export const Plugin = {
const external = target.externalDirectory
if (external)
yield* permission.assert({
...LocationMutation.externalDirectoryPermission(external),
...LocationPath.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source,
+9 -9
View File
@@ -7,7 +7,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { FileMutation } from "@opencode-ai/core/file-mutation"
import { Environment } from "@opencode-ai/core/environment/index"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { type EnvironmentFilesTransform, transformEnvironmentFiles } from "./fixture/environment"
import { location } from "./fixture/location"
@@ -20,7 +20,7 @@ function provide(directory: string, transformFiles: EnvironmentFilesTransform =
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
)
return Effect.provide(
AppNodeBuilder.build(LayerNode.group([LocationMutation.node, FileMutation.node]), [
AppNodeBuilder.build(LayerNode.group([LocationPath.node, FileMutation.node]), [
[Location.node, activeLocation],
[Environment.node, transformEnvironmentFiles(activeLocation, transformFiles)],
]),
@@ -40,7 +40,7 @@ describe("FileMutation", () => {
Effect.gen(function* () {
const targetPath = path.join(directory, "hello.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "before"))
const target = yield* (yield* LocationMutation.Service).resolve({ path: "hello.txt" })
const target = yield* (yield* LocationPath.Service).resolve({ path: "hello.txt" })
expect(yield* (yield* FileMutation.Service).write({ target, content: "after" })).toEqual({
operation: "write",
@@ -56,7 +56,7 @@ describe("FileMutation", () => {
it.live("writes a prospective internal file and creates parent directories", () =>
withTmp((directory) =>
Effect.gen(function* () {
const target = yield* (yield* LocationMutation.Service).resolve({
const target = yield* (yield* LocationPath.Service).resolve({
path: path.join("src", "nested", "hello.txt"),
})
const result = yield* (yield* FileMutation.Service).write({ target, content: "hello" })
@@ -77,8 +77,8 @@ describe("FileMutation", () => {
Effect.gen(function* () {
const preservedPath = path.join(directory, "preserved.txt")
yield* Effect.promise(() => fs.writeFile(preservedPath, "\uFEFFbefore"))
const preserved = yield* (yield* LocationMutation.Service).resolve({ path: "preserved.txt" })
const created = yield* (yield* LocationMutation.Service).resolve({ path: "created.txt" })
const preserved = yield* (yield* LocationPath.Service).resolve({ path: "preserved.txt" })
const created = yield* (yield* LocationPath.Service).resolve({ path: "created.txt" })
const files = yield* FileMutation.Service
yield* files.writeTextPreservingBom({ target: preserved, content: "\uFEFFafter" })
@@ -95,7 +95,7 @@ describe("FileMutation", () => {
withTmp((outside) =>
Effect.gen(function* () {
const targetPath = path.join(outside, "external.txt")
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
const result = yield* (yield* FileMutation.Service).write({ target, content: "external" })
expect(result).toEqual({
@@ -133,7 +133,7 @@ describe("FileMutation", () => {
)
yield* Effect.gen(function* () {
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const files = yield* FileMutation.Service
const firstPlan = yield* mutation.resolve({ path: "shared.txt" })
const secondPlan = yield* mutation.resolve({ path: "shared.txt" })
@@ -222,7 +222,7 @@ describe("FileMutation", () => {
)
yield* Effect.gen(function* () {
const mutation = yield* LocationMutation.Service
const mutation = yield* LocationPath.Service
const files = yield* FileMutation.Service
const firstPlan = yield* mutation.resolve({ path: "first.txt" })
const secondPlan = yield* mutation.resolve({ path: "second.txt" })
@@ -4,18 +4,26 @@ import { describe, expect, test } from "bun:test"
import { Effect, Layer, Schema } from "effect"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { AbsolutePath } from "@opencode-ai/core/schema"
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, [
LayerNode.compile(LocationPath.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) },
),
),
),
],
]),
)
@@ -28,13 +36,13 @@ function withTmp<A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) {
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
}
describe("LocationMutation", () => {
describe("LocationPath", () => {
it.live("resolves an active relative existing file target", () =>
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "hello.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "hello"))
const target = yield* (yield* LocationMutation.Service).resolve({ path: "hello.txt" })
const target = yield* (yield* LocationPath.Service).resolve({ path: "hello.txt" })
expect(target).toMatchObject({
absolute: targetPath,
@@ -49,7 +57,7 @@ describe("LocationMutation", () => {
withTmp((directory) =>
Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(path.join(directory, "src")))
const target = yield* (yield* LocationMutation.Service).resolve({ path: path.join("src", "new.txt") })
const target = yield* (yield* LocationPath.Service).resolve({ path: path.join("src", "new.txt") })
expect(target).toMatchObject({
absolute: path.join(directory, "src", "new.txt"),
resource: "src/new.txt",
@@ -58,10 +66,43 @@ describe("LocationMutation", () => {
),
)
it.live("does not require external authorization inside the project but outside the active directory", () =>
withTmp((project) =>
Effect.gen(function* () {
const directory = path.join(project, "packages", "app")
const targetPath = path.join(project, "README.md")
yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
yield* Effect.promise(() => fs.writeFile(targetPath, "hello"))
const locationPath = yield* LocationPath.Service
const target = yield* locationPath.resolve({ path: targetPath })
expect(target).toMatchObject({
absolute: targetPath,
resource: "../../README.md",
})
expect(target.externalDirectory).toBeUndefined()
}).pipe(provide(path.join(project, "packages", "app"), project)),
),
)
it.live("does not treat a filesystem-root project fallback as internal", () =>
withTmp((directory) =>
withTmp((outside) =>
Effect.gen(function* () {
const locationPath = yield* LocationPath.Service
const target = yield* locationPath.resolve({ path: path.join(outside, "target.txt") })
expect(target.externalDirectory).toBeDefined()
}).pipe(provide(directory, path.parse(directory).root)),
),
),
)
it.live("requires external-directory authorization for a relative lexical escape", () =>
withTmp((directory) =>
Effect.gen(function* () {
const target = yield* (yield* LocationMutation.Service).resolve({ path: "../outside.txt" })
const target = yield* (yield* LocationPath.Service).resolve({ path: "../outside.txt" })
const root = path.dirname(directory)
expect(target).toMatchObject({
absolute: path.join(root, "outside.txt"),
@@ -84,7 +125,7 @@ describe("LocationMutation", () => {
await fs.mkdir(outside)
await fs.symlink(outside, path.join(directory, "escape"))
})
const target = yield* (yield* LocationMutation.Service).resolve({ path: path.join("escape", "new.txt") })
const target = yield* (yield* LocationPath.Service).resolve({ path: path.join("escape", "new.txt") })
expect(target).toMatchObject({
absolute: path.join(directory, "escape", "new.txt"),
resource: "escape/new.txt",
@@ -104,7 +145,7 @@ describe("LocationMutation", () => {
await fs.symlink(path.join(directory, "actual"), path.join(directory, "linked"))
})
expect(yield* (yield* LocationMutation.Service).resolve({ path: "linked/new.txt" })).toMatchObject({
expect(yield* (yield* LocationPath.Service).resolve({ path: "linked/new.txt" })).toMatchObject({
absolute: path.join(directory, "linked", "new.txt"),
resource: "linked/new.txt",
})
@@ -116,7 +157,7 @@ describe("LocationMutation", () => {
withTmp((directory) =>
Effect.gen(function* () {
const targetPath = path.join(directory, "new.txt")
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
expect(target).toMatchObject({
absolute: targetPath,
resource: "new.txt",
@@ -131,7 +172,7 @@ describe("LocationMutation", () => {
withTmp((outside) =>
Effect.gen(function* () {
const targetPath = path.join(outside, "new.txt")
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
const root = outside
expect(target).toMatchObject({
absolute: path.join(root, "new.txt"),
@@ -152,7 +193,7 @@ describe("LocationMutation", () => {
Effect.gen(function* () {
const targetPath = path.join(outside, "existing.txt")
yield* Effect.promise(() => fs.writeFile(targetPath, "existing"))
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
expect(target).toMatchObject({ absolute: targetPath })
expect(target.externalDirectory?.directory).toBe(outside)
}).pipe(provide(directory)),
@@ -164,7 +205,7 @@ describe("LocationMutation", () => {
withTmp((directory) =>
withTmp((outside) =>
Effect.gen(function* () {
const target = yield* (yield* LocationMutation.Service).resolve({ path: outside, kind: "file" })
const target = yield* (yield* LocationPath.Service).resolve({ path: outside, kind: "file" })
expect(target.externalDirectory).toMatchObject({
directory: path.dirname(outside),
resource: path.join(path.dirname(outside), "*").replaceAll("\\", "/"),
@@ -179,7 +220,7 @@ describe("LocationMutation", () => {
withTmp((outside) =>
Effect.gen(function* () {
const targetPath = path.join(outside, "new", "nested", "file.txt")
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
const parent = path.dirname(targetPath)
expect(target.externalDirectory).toMatchObject({
directory: parent,
@@ -190,9 +231,9 @@ describe("LocationMutation", () => {
),
)
test("ignores unknown mutation input fields", () => {
expect(Object.keys(LocationMutation.ResolveInput.fields)).toEqual(["path", "kind"])
expect(Schema.decodeUnknownSync(LocationMutation.ResolveInput)({ path: "README.md", reference: "docs" })).toEqual({
test("ignores unknown path input fields", () => {
expect(Object.keys(LocationPath.ResolveInput.fields)).toEqual(["path", "kind"])
expect(Schema.decodeUnknownSync(LocationPath.ResolveInput)({ path: "README.md", reference: "docs" })).toEqual({
path: "README.md",
})
})
@@ -13,7 +13,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
import { Global } from "@opencode-ai/util/global"
import { Image } from "@opencode-ai/core/image"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { Model } from "@opencode-ai/core/model"
import { Permission } from "@opencode-ai/core/permission"
import { Project } from "@opencode-ai/core/project"
@@ -43,7 +43,7 @@ const readToolNode = makeLocationNode({
deps: [
Tool.node,
ReadToolFileSystem.node,
LocationMutation.node,
LocationPath.node,
Image.node,
Permission.node,
SessionInstructions.node,
@@ -65,7 +65,7 @@ const testLayer = AppNodeBuilder.build(
Session.node,
Location.node,
FSUtil.node,
LocationMutation.node,
LocationPath.node,
ReadToolFileSystem.node,
readToolNode,
Tool.node,
+3 -3
View File
@@ -8,7 +8,7 @@ import { Environment } from "@opencode-ai/core/environment/index"
import { FileMutation } from "@opencode-ai/core/file-mutation"
import { Formatter } from "@opencode-ai/core/formatter"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { Permission } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Session } from "@opencode-ai/core/session"
@@ -27,7 +27,7 @@ const editToolNode = makeLocationNode({
layer: Layer.effectDiscard(registerToolPlugin(EditTool.Plugin)),
deps: [
Tool.node,
LocationMutation.node,
LocationPath.node,
FileMutation.node,
Environment.node,
Formatter.node,
@@ -84,7 +84,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: Tool.Interface) =
}).pipe(
Effect.provide(
AppNodeBuilder.build(
LayerNode.group([Tool.node, Tool.node, LocationMutation.node, FileMutation.node, editToolNode]),
LayerNode.group([Tool.node, Tool.node, LocationPath.node, FileMutation.node, editToolNode]),
[
[
Environment.node,
+6 -7
View File
@@ -8,7 +8,7 @@ import { Environment } from "@opencode-ai/core/environment/index"
import { Formatter } from "@opencode-ai/core/formatter"
import { FileMutation } from "@opencode-ai/core/file-mutation"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { Permission } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Session } from "@opencode-ai/core/session"
@@ -27,7 +27,7 @@ const patchToolNode = makeLocationNode({
layer: Layer.effectDiscard(registerToolPlugin(PatchTool.Plugin)),
deps: [
Tool.node,
LocationMutation.node,
LocationPath.node,
FileMutation.node,
Environment.node,
Formatter.node,
@@ -99,7 +99,7 @@ const withTool = <A, E, R>(
return yield* body(yield* Tool.Service)
}).pipe(
Effect.provide(
AppNodeBuilder.build(LayerNode.group([Tool.node, LocationMutation.node, FileMutation.node, patchToolNode]), [
AppNodeBuilder.build(LayerNode.group([Tool.node, LocationPath.node, FileMutation.node, patchToolNode]), [
[
Environment.node,
transformEnvironmentFiles(activeLocation, (files) => ({
@@ -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,
+5 -5
View File
@@ -14,7 +14,7 @@ import { Permission } from "@opencode-ai/core/permission"
import { Session } from "@opencode-ai/core/session"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/util/global"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { location } from "./fixture/location"
import { Tool } from "@opencode-ai/core/tool"
import { ReadTool } from "@opencode-ai/core/tool/plugin/read"
@@ -32,7 +32,7 @@ const readToolNode = makeLocationNode({
deps: [
Tool.node,
ReadToolFileSystem.node,
LocationMutation.node,
LocationPath.node,
Image.node,
Permission.node,
SessionInstructions.node,
@@ -107,8 +107,8 @@ const locationLayer = Layer.succeed(
Location.Service.of(location({ directory: AbsolutePath.make(process.cwd()) })),
)
const mutation = Layer.succeed(
LocationMutation.Service,
LocationMutation.Service.of({
LocationPath.Service,
LocationPath.Service.of({
resolve: (input) => {
const absolute = path.resolve(process.cwd(), input.path)
const external = path.isAbsolute(input.path) && !FSUtil.contains(process.cwd(), absolute)
@@ -141,7 +141,7 @@ const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
[Permission.node, permission],
[Config.node, config],
[Image.node, imageLayer],
[LocationMutation.node, mutation],
[LocationPath.node, mutation],
[FSUtil.node, testFileSystem],
[Location.node, locationLayer],
[Global.node, Global.layerWith({ data: Global.Path.data })],
+3 -3
View File
@@ -8,7 +8,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Environment } from "@opencode-ai/core/environment/index"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { Permission } from "@opencode-ai/core/permission"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -25,12 +25,12 @@ import { executeTool, registerToolPlugin, toolIdentity } from "./lib/tool"
const globToolNode = makeLocationNode({
name: "test/glob-tool-plugin",
layer: Layer.effectDiscard(registerToolPlugin(GlobTool.Plugin)),
deps: [Tool.node, Environment.node, Ripgrep.node, Location.node, LocationMutation.node, Permission.node],
deps: [Tool.node, Environment.node, Ripgrep.node, Location.node, LocationPath.node, Permission.node],
})
const grepToolNode = makeLocationNode({
name: "test/grep-tool-plugin",
layer: Layer.effectDiscard(registerToolPlugin(GrepTool.Plugin)),
deps: [Tool.node, Environment.node, Ripgrep.node, Location.node, LocationMutation.node, Permission.node],
deps: [Tool.node, Environment.node, Ripgrep.node, Location.node, LocationPath.node, Permission.node],
})
const sessionID = Session.ID.make("ses_search_tool_test")
+2 -2
View File
@@ -16,7 +16,7 @@ import { Environment } from "@opencode-ai/core/environment/index"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Global } from "@opencode-ai/util/global"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
import { Model } from "@opencode-ai/core/model"
import { Provider } from "@opencode-ai/core/provider"
@@ -132,7 +132,7 @@ const shellPluginSupervisor = makeLocationNode({
deps: [
Config.node,
Environment.node,
LocationMutation.node,
LocationPath.node,
Permission.node,
PluginRuntime.node,
Shell.node,
+3 -3
View File
@@ -8,7 +8,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Environment } from "@opencode-ai/core/environment/index"
import { Location } from "@opencode-ai/core/location"
import { LocationMutation } from "@opencode-ai/core/location-mutation"
import { LocationPath } from "@opencode-ai/core/location-path"
import { Permission } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Session } from "@opencode-ai/core/session"
@@ -25,7 +25,7 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "
const writeToolNode = makeLocationNode({
name: "test/write-tool-plugin",
layer: Layer.effectDiscard(registerToolPlugin(WriteTool.Plugin)),
deps: [Tool.node, LocationMutation.node, FileMutation.node, Environment.node, Formatter.node, Permission.node],
deps: [Tool.node, LocationPath.node, FileMutation.node, Environment.node, Formatter.node, Permission.node],
})
const sessionID = Session.ID.make("ses_write_tool_test")
@@ -72,7 +72,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: Tool.Interface) =
}).pipe(
Effect.provide(
AppNodeBuilder.build(
LayerNode.group([Tool.node, Tool.node, LocationMutation.node, FileMutation.node, writeToolNode]),
LayerNode.group([Tool.node, Tool.node, LocationPath.node, FileMutation.node, writeToolNode]),
[
[
Environment.node,