diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 4e89198dff..01aa6a0b72 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -54,19 +54,20 @@ export const GrepTool = Tool.define( }) const ins = yield* InstanceState.context - const search = AppFileSystem.resolve( - path.isAbsolute(params.path ?? ins.directory) - ? (params.path ?? ins.directory) - : path.join(ins.directory, params.path ?? "."), - ) - yield* reference.ensure(search) + const requested = path.isAbsolute(params.path ?? ins.directory) + ? (params.path ?? ins.directory) + : path.join(ins.directory, params.path ?? ".") + yield* reference.ensure(requested) + const requestedInfo = yield* fs.stat(requested).pipe(Effect.catch(() => Effect.succeed(undefined))) + yield* assertExternalDirectoryEffect(ctx, requested, { + bypass: yield* reference.contains(requested), + kind: requestedInfo?.type === "Directory" ? "directory" : "file", + }) + + const search = AppFileSystem.resolve(requested) const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined))) const cwd = info?.type === "Directory" ? search : path.dirname(search) const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)] - yield* assertExternalDirectoryEffect(ctx, search, { - bypass: yield* reference.contains(search), - kind: info?.type === "Directory" ? "directory" : "file", - }) const result = yield* rg.search({ cwd, diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index 53f5d9a19c..29b5a60db2 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -1,4 +1,6 @@ import { describe, expect } from "bun:test" +import fs from "fs/promises" +import os from "os" import path from "path" import { Effect, Layer } from "effect" import { GrepTool } from "../../src/tool/grep" @@ -11,6 +13,8 @@ import { Ripgrep } from "../../src/file/ripgrep" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { testEffect } from "../lib/effect" import { Reference } from "@/reference/reference" +import { Permission } from "../../src/permission" +import type * as Tool from "../../src/tool/tool" const it = testEffect( Layer.mergeAll( @@ -110,4 +114,53 @@ describe("tool.grep", () => { expect(result.output).toContain("Line 2: line2") }), ) + + it.instance("does not ask for external_directory when alias path is allowed", () => + Effect.gen(function* () { + if (process.platform === "win32") return + + yield* TestInstance + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "opencode-grep-alias-"))), + (dir) => Effect.promise(() => fs.rm(dir, { recursive: true, force: true })), + ) + const real = path.join(tmp, "real") + const alias = path.join(tmp, "alias") + yield* Effect.promise(() => fs.mkdir(real)) + yield* Effect.promise(() => fs.symlink(real, alias, "dir")) + yield* Effect.promise(() => Bun.write(path.join(real, "test.txt"), "needle")) + + const ruleset = Permission.fromConfig({ + grep: "allow", + external_directory: { + [path.join(alias, "*")]: "allow", + }, + }) + const requests: Array> = [] + const next: Tool.Context = { + ...ctx, + ask: (req) => + Effect.sync(() => { + const needsAsk = req.patterns.some( + (pattern) => Permission.evaluate(req.permission, pattern, ruleset).action !== "allow", + ) + if (needsAsk) requests.push(req) + }), + } + + const info = yield* GrepTool + const grep = yield* info.init() + const result = yield* grep.execute( + { + pattern: "needle", + path: alias, + include: "*.txt", + }, + next, + ) + + expect(result.metadata.matches).toBe(1) + expect(requests.find((req) => req.permission === "external_directory")).toBeUndefined() + }), + ) })