Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions packages/opencode/src/project/instance-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const context = LocalContext.create<InstanceContext>("instance")
*/
export function containsPath(filepath: string, ctx: InstanceContext): boolean {
if (FSUtil.contains(ctx.directory, filepath)) return true
// Non-git projects set worktree to "/" which would match ANY absolute path.
// Skip worktree check in this case to preserve external_directory permissions.
// Legacy non-git projects could have worktree "/", which would match any absolute path.
// Keep the guard so persisted rows cannot bypass external_directory permissions.
if (ctx.worktree === "/") return false
return FSUtil.contains(ctx.worktree, filepath)
}
5 changes: 3 additions & 2 deletions packages/opencode/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,9 @@ export const layer = Layer.effect(
const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) {
yield* Effect.logInfo("fromDirectory", { directory })

const data = yield* projectV2.resolve(AbsolutePath.make(directory))
const worktree = data.id === ProjectV2.ID.make("global") && !data.vcs ? "/" : data.directory
const opened = AbsolutePath.make(FSUtil.resolve(directory))
const data = yield* projectV2.resolve(opened)
const worktree = data.vcs ? data.directory : opened

// Phase 2: upsert
const projectID = ProjectV2.ID.make(data.id)
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/test/project/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ describe("InstanceStore", () => {
}),
)

it.live("uses the opened directory as worktree for non-git instances", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const store = yield* InstanceStore.Service
const ctx = yield* store.load({ directory: dir })

expect(ctx.directory).toBe(dir)
expect(ctx.worktree).toBe(dir)
expect(ctx.project.worktree).toBe(dir)
}),
)

it.live("runs bootstrap with InstanceRef provided", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/test/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ describe("Project.fromDirectory", () => {
const tmp = yield* tmpdirScoped()
const result = yield* project.fromDirectory(tmp)
expect(result.project.id).toBe(ProjectV2.ID.global)
expect(result.project.worktree).toBe(tmp)
expect(result.sandbox).toBe(tmp)
}),
)

Expand Down
13 changes: 13 additions & 0 deletions packages/opencode/test/tool/read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,19 @@ describe("tool.read external_directory permission", () => {
}),
)

it.live("uses opened directory-relative read permission in non-git projects", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
yield* put(path.join(dir, "src", "secret.ts"), "shh")

const { items, next } = asks()
yield* exec(dir, { filePath: path.join(dir, "src", "secret.ts") }, next)
const read = items.find((item) => item.permission === "read")
expect(read).toBeDefined()
expect(read!.patterns).toEqual([path.join("src", "secret.ts")])
}),
)

it.live("asks for directory-scoped external_directory permission when reading external directory", () =>
Effect.gen(function* () {
const outer = yield* tmpdirScoped()
Expand Down
Loading