Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
011c8d9
fix(opencode): Make use of standard relative function in tools to pro…
OpeOginni Mar 23, 2026
98d40bf
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 2, 2026
20a0d4d
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 10, 2026
ecd8ba8
refactor(read): use relative paths for permission requests in new Rea…
OpeOginni Apr 10, 2026
d18cd2c
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 11, 2026
5a32ac2
refactor: update relative path handling in tools after effect update
OpeOginni Apr 11, 2026
8e1dcd3
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 12, 2026
ab672f6
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 17, 2026
95b4f47
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 17, 2026
970cded
refactor(write): remove redundant filetime assertion before editing
OpeOginni Apr 17, 2026
6ca7127
refactor: update type annotations for pattern arrays in tests
OpeOginni Apr 17, 2026
9131741
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 17, 2026
837a102
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 21, 2026
5196a18
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 23, 2026
40d2902
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 25, 2026
3626d69
refactor(lsp): replace path.relative with custom relative function fo…
OpeOginni Apr 25, 2026
6397664
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 26, 2026
2781058
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 27, 2026
f0c3a8a
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 29, 2026
daee7c9
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 29, 2026
1103cf5
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni Apr 30, 2026
ea95131
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni May 1, 2026
1fcfc04
refactor(tool): yield InstanceState.context instead of reading ALS fo…
OpeOginni May 1, 2026
cd45577
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni May 1, 2026
8da1664
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni May 2, 2026
0156fa1
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni May 3, 2026
d7b659a
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni May 5, 2026
78b58e5
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni May 5, 2026
d999088
Merge branch 'dev' into fix/perms-system-file-path
OpeOginni May 5, 2026
037f00c
Merge remote-tracking branch 'anomalyco/dev' into fix/perms-system-fi…
OpeOginni Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions packages/opencode/src/tool/apply_patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LSP } from "@/lsp/lsp"
import { FSUtil } from "@opencode-ai/core/fs-util"
import DESCRIPTION from "./apply_patch.txt"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { relative } from "./relative"
import { Format } from "../format"
import * as Bom from "@/util/bom"

Expand Down Expand Up @@ -193,7 +194,7 @@ export const ApplyPatchTool = Tool.define(
// Build per-file metadata for UI rendering (used for both permission and result)
const files = fileChanges.map((change) => ({
filePath: change.filePath,
relativePath: path.relative(instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"),
relativePath: relative(instance, change.movePath ?? change.filePath).replaceAll("\\", "/"),
type: change.type,
patch: change.diff,
additions: change.additions,
Expand All @@ -202,7 +203,7 @@ export const ApplyPatchTool = Tool.define(
}))

// Check permissions if needed
const relativePaths = fileChanges.map((c) => path.relative(instance.worktree, c.filePath).replaceAll("\\", "/"))
const relativePaths = fileChanges.map((c) => relative(instance, c.filePath).replaceAll("\\", "/"))
yield* ctx.ask({
permission: "edit",
patterns: relativePaths,
Expand Down Expand Up @@ -273,13 +274,13 @@ export const ApplyPatchTool = Tool.define(
// Generate output summary
const summaryLines = fileChanges.map((change) => {
if (change.type === "add") {
return `A ${path.relative(instance.worktree, change.filePath).replaceAll("\\", "/")}`
return `A ${relative(instance, change.filePath).replaceAll("\\", "/")}`
}
if (change.type === "delete") {
return `D ${path.relative(instance.worktree, change.filePath).replaceAll("\\", "/")}`
return `D ${relative(instance, change.filePath).replaceAll("\\", "/")}`
}
const target = change.movePath ?? change.filePath
return `M ${path.relative(instance.worktree, target).replaceAll("\\", "/")}`
return `M ${relative(instance, target).replaceAll("\\", "/")}`
})
let output = `Success. Updated the following files:\n${summaryLines.join("\n")}`

Expand All @@ -288,7 +289,7 @@ export const ApplyPatchTool = Tool.define(
const target = change.movePath ?? change.filePath
const block = LSP.Diagnostic.report(target, diagnostics[FSUtil.normalizePath(target)] ?? [])
if (!block) continue
const rel = path.relative(instance.worktree, target).replaceAll("\\", "/")
const rel = relative(instance, target).replaceAll("\\", "/")
output += `\n\nLSP errors detected in ${rel}, please fix:\n${block}`
}

Expand Down
8 changes: 5 additions & 3 deletions packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { InstanceState } from "@/effect/instance-state"
import { Snapshot } from "@/snapshot"
import { assertExternalDirectoryEffect } from "./external-directory"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { relative } from "./relative"
import * as Bom from "@/util/bom"

function normalizeLineEndings(text: string): string {
Expand Down Expand Up @@ -81,6 +82,7 @@ export const EditTool = Tool.define(
? params.filePath
: path.join(instance.directory, params.filePath)
yield* assertExternalDirectoryEffect(ctx, filePath)
const rel = relative(instance, filePath)

let diff = ""
let contentOld = ""
Expand All @@ -101,7 +103,7 @@ export const EditTool = Tool.define(
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(instance.worktree, filePath)],
patterns: [rel],
always: ["*"],
metadata: {
filepath: filePath,
Expand Down Expand Up @@ -144,7 +146,7 @@ export const EditTool = Tool.define(
)
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(instance.worktree, filePath)],
patterns: [rel],
always: ["*"],
metadata: {
filepath: filePath,
Expand Down Expand Up @@ -206,7 +208,7 @@ export const EditTool = Tool.define(
diff,
filediff,
},
title: `${path.relative(instance.worktree, filePath)}`,
title: rel,
output,
}
}),
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/tool/lsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InstanceState } from "@/effect/instance-state"
import { pathToFileURL } from "url"
import { assertExternalDirectoryEffect } from "./external-directory"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { relative } from "./relative"

const operations = [
"goToDefinition",
Expand Down Expand Up @@ -62,7 +63,7 @@ export const LspTool = Tool.define(

const uri = pathToFileURL(file).href
const position = { file, line: args.line - 1, character: args.character - 1 }
const relPath = path.relative(instance.worktree, file)
const relPath = relative(instance, file)
const detail =
args.operation === "workspaceSymbol"
? ""
Expand Down
11 changes: 6 additions & 5 deletions packages/opencode/src/tool/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DESCRIPTION from "./read.txt"
import { InstanceState } from "@/effect/instance-state"
import { assertExternalDirectoryEffect } from "./external-directory"
import { Instruction } from "../session/instruction"
import { relative } from "./relative"
import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"

const DEFAULT_READ_LIMIT = 2000
Expand Down Expand Up @@ -238,7 +239,7 @@ export const ReadTool = Tool.define<
if (process.platform === "win32") {
filepath = FSUtil.normalizePath(filepath)
}
const title = path.relative(instance.worktree, filepath)
const rel = relative(instance, filepath)

const stat = yield* fs.stat(filepath).pipe(
Effect.catchIf(
Expand All @@ -254,7 +255,7 @@ export const ReadTool = Tool.define<

yield* ctx.ask({
permission: "read",
patterns: [path.relative(instance.worktree, filepath)],
patterns: [rel],
always: ["*"],
metadata: {},
})
Expand All @@ -270,7 +271,7 @@ export const ReadTool = Tool.define<
const truncated = start + sliced.length < items.length

return {
title,
title: rel,
output: [
`<path>${filepath}</path>`,
`<type>directory</type>`,
Expand Down Expand Up @@ -307,7 +308,7 @@ export const ReadTool = Tool.define<
const bytes = yield* fs.readFile(filepath)
const msg = isPdfAttachment(mime) ? "PDF read successfully" : "Image read successfully"
return {
title,
title: rel,
output: msg,
metadata: {
preview: msg,
Expand Down Expand Up @@ -357,7 +358,7 @@ export const ReadTool = Tool.define<
}

return {
title,
title: rel,
output,
metadata: {
preview: file.raw.slice(0, 20).join("\n"),
Expand Down
8 changes: 8 additions & 0 deletions packages/opencode/src/tool/relative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import path from "path"
import type { InstanceContext } from "@/project/instance-context"

export function relative(instance: InstanceContext, file: string) {

const root = instance.worktree === "/" ? instance.directory : instance.worktree
return path.relative(root, file)
}
7 changes: 5 additions & 2 deletions packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { InstanceState } from "@/effect/instance-state"
import { trimDiff } from "./edit"
import { assertExternalDirectoryEffect } from "./external-directory"
import { relative } from "./relative"
import * as Bom from "@/util/bom"

const MAX_PROJECT_DIAGNOSTICS_FILES = 5
Expand Down Expand Up @@ -49,11 +50,13 @@ export const WriteTool = Tool.define(
const desiredBom = source.bom || next.bom
const contentOld = source.text
const contentNew = next.text

const rel = relative(instance, filepath)

const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(instance.worktree, filepath)],
patterns: [rel],
always: ["*"],
metadata: {
filepath,
Expand Down Expand Up @@ -90,7 +93,7 @@ export const WriteTool = Tool.define(
}

return {
title: path.relative(instance.worktree, filepath),
title: rel,
metadata: {
diagnostics,
filepath,
Expand Down
14 changes: 14 additions & 0 deletions packages/opencode/test/tool/apply_patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,20 @@ describe("tool.apply_patch freeform", () => {
{ git: true },
)

it.instance("uses directory-relative permission paths in non-git projects", () =>
Effect.gen(function* () {
const { ctx, calls } = makeCtx()

const patchText = "*** Begin Patch\n*** Add File: .agents/new.txt\n+created\n*** End Patch"

yield* execute({ patchText }, ctx)

expect(calls).toHaveLength(1)
expect(calls[0]?.patterns).toEqual([".agents/new.txt"])
expect(calls[0]?.metadata.files[0]?.relativePath).toBe(".agents/new.txt")
}),
)

it.instance("applies multiple hunks to one file", () =>
Effect.gen(function* () {
const test = yield* TestInstance
Expand Down
22 changes: 22 additions & 0 deletions packages/opencode/test/tool/edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,28 @@ describe("tool.edit", () => {
yield* Deferred.await(updated)
}),
)

it.instance("uses directory-relative permission paths in non-git projects", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, ".agents", "file.txt")
const calls: Array<{ patterns: readonly string[] }> = []

yield* run(
{ filePath: filepath, oldString: "", newString: "new content" },
{
...ctx,
ask: (input) =>
Effect.sync(() => {
calls.push({ patterns: input.patterns })
}),
},
)

expect(calls).toHaveLength(1)
expect(calls[0]?.patterns).toEqual([path.join(".agents", "file.txt")])
}),
)
})

describe("editing existing files", () => {
Expand Down
15 changes: 15 additions & 0 deletions packages/opencode/test/tool/read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,21 @@ const asks = () => {
}

describe("tool.read external_directory permission", () => {
it.live("uses directory-relative read permission paths in non-git projects", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
yield* put(path.join(dir, ".agents", "test.txt"), "hello world")

const { items, next } = asks()

const result = yield* exec(dir, { filePath: path.join(dir, ".agents", "test.txt") }, next)
expect(result.output).toContain("hello world")
const read = items.find((item) => item.permission === "read")
expect(read).toBeDefined()
expect(read!.patterns).toEqual([path.join(".agents", "test.txt")])
}),
)

it.live("allows reading absolute path inside project directory", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
Expand Down
25 changes: 25 additions & 0 deletions packages/opencode/test/tool/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@ describe("tool.write", () => {
expect(content).toBe("relative content")
}),
)

it.instance("uses directory-relative permission paths in non-git projects", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, ".agents", "file.txt")
const calls: Array<{ patterns: readonly string[] }> = []

yield* run(
{
filePath: filepath,
content: "content",
},
{
...ctx,
ask: (input) =>
Effect.sync(() => {
calls.push({ patterns: input.patterns })
}),
},
)

expect(calls).toHaveLength(1)
expect(calls[0]?.patterns).toEqual([path.join(".agents", "file.txt")])
}),
)
})

describe("existing file overwrite", () => {
Expand Down
Loading