diff --git a/packages/opencode/src/project/instance-context.ts b/packages/opencode/src/project/instance-context.ts index 18ea39e16e91..a7b8b5bfc023 100644 --- a/packages/opencode/src/project/instance-context.ts +++ b/packages/opencode/src/project/instance-context.ts @@ -17,8 +17,8 @@ export const context = LocalContext.create("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) } diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 5f1b64743a74..a2c68801a035 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -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) diff --git a/packages/opencode/test/project/instance.test.ts b/packages/opencode/test/project/instance.test.ts index 491cfe93d70e..6c6b761faf9c 100644 --- a/packages/opencode/test/project/instance.test.ts +++ b/packages/opencode/test/project/instance.test.ts @@ -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 }) diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 05e205cd8704..2673752209e8 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -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) }), ) diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 67205f56e384..421fcbf85525 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -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()