Skip to content

Commit e707e41

Browse files
authored
fix(acp): include external directory permission context (anomalyco#30567)
1 parent fc62b3d commit e707e41

5 files changed

Lines changed: 79 additions & 8 deletions

File tree

packages/opencode/src/acp/tool.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ export function toLocations(toolName: string, input: ToolInput): ToolCallLocatio
7878
case "write":
7979
return locationFrom(input.filePath ?? input.filepath)
8080

81+
case "external_directory":
82+
return locationFrom(input.filePath ?? input.filepath, input.parentDir, input.directories)
83+
8184
case "grep":
8285
case "glob":
8386
case "context":
@@ -255,9 +258,19 @@ export const buildDuplicateRunningToolUpdate = duplicateRunningToolUpdate
255258
export const buildCompletedToolUpdate = completedToolUpdate
256259
export const buildErrorToolUpdate = errorToolUpdate
257260

258-
function locationFrom(value: unknown): ToolCallLocation[] {
259-
const path = stringValue(value)
260-
return path ? [{ path }] : []
261+
function locationFrom(...values: unknown[]): ToolCallLocation[] {
262+
return Array.from(
263+
new Set(
264+
values.flatMap((value): string[] => {
265+
if (Array.isArray(value)) {
266+
return value.filter((item): item is string => typeof item === "string" && item.length > 0)
267+
}
268+
const path = stringValue(value)
269+
return path ? [path] : []
270+
}),
271+
),
272+
(path) => ({ path }),
273+
)
261274
}
262275

263276
function diffContent(input: ToolInput): ToolCallContent[] {

packages/opencode/src/tool/shell.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,17 +263,27 @@ const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boole
263263
return tree
264264
})
265265

266-
const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan) {
266+
const ask = Effect.fn("ShellTool.ask")(function* (
267+
ctx: Tool.Context,
268+
scan: Scan,
269+
input: { command: string; description: string },
270+
) {
267271
if (scan.dirs.size > 0) {
268-
const globs = Array.from(scan.dirs).map((dir) => {
272+
const directories = Array.from(scan.dirs)
273+
const globs = directories.map((dir) => {
269274
if (process.platform === "win32") return FSUtil.normalizePathPattern(path.join(dir, "*"))
270275
return path.join(dir, "*")
271276
})
272277
yield* ctx.ask({
273278
permission: "external_directory",
274279
patterns: globs,
275280
always: globs,
276-
metadata: {},
281+
metadata: {
282+
command: input.command,
283+
description: input.description,
284+
directories,
285+
patterns: globs,
286+
},
277287
})
278288
}
279289

@@ -282,7 +292,10 @@ const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan)
282292
permission: ShellID.ToolID,
283293
patterns: Array.from(scan.patterns),
284294
always: Array.from(scan.always),
285-
metadata: {},
295+
metadata: {
296+
command: input.command,
297+
description: input.description,
298+
},
286299
})
287300
})
288301

@@ -625,7 +638,7 @@ export const ShellTool = Tool.define(
625638
)
626639
const scan = yield* collect(tree.rootNode, cwd, ps, shell, instanceCtx)
627640
if (!containsPath(cwd, instanceCtx)) scan.dirs.add(cwd)
628-
yield* ask(ctx, scan)
641+
yield* ask(ctx, scan, params)
629642
}),
630643
)
631644

packages/opencode/test/acp/permission.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,42 @@ describe("acp permissions", () => {
165165
expect(harness.replies).toEqual([{ requestID: "perm_1", reply: "once", directory: "/workspace" }])
166166
})
167167

168+
it("forwards external_directory metadata and locations to requestPermission", async () => {
169+
const harness = createHarness()
170+
await createSession(harness.session, "ses_a")
171+
172+
harness.subscription.handle(
173+
permissionAsked("ses_a", "perm_external", {
174+
permission: "external_directory",
175+
metadata: {
176+
command: "mkdir -p /tmp/outside",
177+
description: "Create external directory",
178+
directories: ["/tmp/outside"],
179+
patterns: ["/tmp/outside/*"],
180+
},
181+
tool: { messageID: "msg_1", callID: "call_1" },
182+
}),
183+
)
184+
185+
await pollUntil(() => harness.replies.length === 1, "external_directory permission was never replied")
186+
187+
expect(harness.requests[0]).toMatchObject({
188+
sessionId: "ses_a",
189+
toolCall: {
190+
toolCallId: "call_1",
191+
status: "pending",
192+
title: "external_directory",
193+
rawInput: {
194+
command: "mkdir -p /tmp/outside",
195+
description: "Create external directory",
196+
directories: ["/tmp/outside"],
197+
patterns: ["/tmp/outside/*"],
198+
},
199+
locations: [{ path: "/tmp/outside" }],
200+
},
201+
})
202+
})
203+
168204
it("rejects non-selected outcomes", async () => {
169205
const harness = createHarness(() => Promise.resolve({ outcome: { outcome: "cancelled" } }))
170206
await createSession(harness.session, "ses_a")

packages/opencode/test/acp/tool.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ describe("acp tool conversion", () => {
3434
expect(toLocations("grep", { path: "/repo/src" })).toEqual([{ path: "/repo/src" }])
3535
expect(toLocations("glob", { path: "/repo/test" })).toEqual([{ path: "/repo/test" }])
3636
expect(toLocations("context7_get_library_docs", { path: "/docs" })).toEqual([{ path: "/docs" }])
37+
expect(toLocations("external_directory", { directories: ["/tmp/outside"], patterns: ["/tmp/outside/*"] })).toEqual([
38+
{ path: "/tmp/outside" },
39+
])
3740
expect(toLocations("bash", { filePath: "/tmp/nope.ts", path: "/tmp" })).toEqual([])
3841
expect(toLocations("read", { path: "/tmp/missing-file-path.ts" })).toEqual([])
3942
})

packages/opencode/test/tool/shell.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,12 @@ describe("tool.shell permissions", () => {
920920
expect(extDirReq).toBeDefined()
921921
expect(extDirReq!.patterns).toContain(expected)
922922
expect(extDirReq!.always).toContain(expected)
923+
expect(extDirReq!.metadata).toMatchObject({
924+
command: `cat ${filepath}`,
925+
description: "Read external file",
926+
directories: [outerTmp],
927+
patterns: [expected],
928+
})
923929
}),
924930
)
925931
}),

0 commit comments

Comments
 (0)