fix(core): restore external directory defaults

This commit is contained in:
Aiden Cline 2026-07-17 17:26:08 +00:00 committed by 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
commit edf0ce766d
10 changed files with 259 additions and 70 deletions

View file

@ -10,6 +10,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { SHELL_OUTPUT_GLOB } from "@opencode-ai/core/permission/defaults"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
import { tmpdir } from "../fixture/tmpdir"
@ -22,6 +23,11 @@ const defaultPermissions = [
{ action: "*", resource: "*", effect: "allow" },
{ action: "external_directory", resource: "*", effect: "ask" },
] satisfies PermissionV2.Ruleset
const shellOutputPermission = {
action: "external_directory",
resource: SHELL_OUTPUT_GLOB,
effect: "allow",
} satisfies PermissionV2.Rule
describe("ConfigAgentPlugin.Plugin", () => {
it.effect("matches POSIX paths against home-relative permissions", () =>
@ -114,6 +120,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
{ action: "bash", resource: "*", effect: "ask" },
{ action: "read", resource: "*", effect: "allow" },
{ action: "bash", resource: "git *", effect: "allow" },
shellOutputPermission,
])
expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).effect).toBe("allow")
expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("ask")
@ -132,6 +139,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
{ action: "read", resource: "*", effect: "allow" },
{ action: "edit", resource: "*", effect: "deny" },
{ action: "read", resource: "*", effect: "deny" },
shellOutputPermission,
])
expect(PermissionV2.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny")
expect((yield* agents.get(AgentV2.ID.make("late")))?.permissions).toEqual([
@ -139,11 +147,31 @@ describe("ConfigAgentPlugin.Plugin", () => {
{ action: "bash", resource: "*", effect: "ask" },
{ action: "read", resource: "*", effect: "allow" },
{ action: "edit", resource: "*", effect: "allow" },
shellOutputPermission,
])
expect(yield* agents.get(AgentV2.ID.make("removed"))).toBeUndefined()
}),
)
it.effect("keeps shell output readable through a broad external-directory deny", () =>
Effect.gen(function* () {
const permissions = yield* loadConfiguredPermissions([
{ action: "external_directory", resource: "*", effect: "deny" },
])
expect(PermissionV2.evaluate("external_directory", SHELL_OUTPUT_GLOB, permissions).effect).toBe("allow")
}),
)
it.effect("respects an exact shell output deny", () =>
Effect.gen(function* () {
const permissions = yield* loadConfiguredPermissions([
{ action: "external_directory", resource: "*", effect: "deny" },
{ action: "external_directory", resource: SHELL_OUTPUT_GLOB, effect: "deny" },
])
expect(PermissionV2.evaluate("external_directory", SHELL_OUTPUT_GLOB, permissions).effect).toBe("deny")
}),
)
it.effect("maps configured agent fields and preserves an unspecified model variant", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
@ -294,13 +322,21 @@ Use native v2 fields.`,
system: "Review carefully.",
description: "Markdown description",
request: { body: { temperature: 0.5 } },
permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }],
permissions: [
...defaultPermissions,
{ action: "edit", resource: "*", effect: "deny" },
shellOutputPermission,
],
})
expect(yield* agents.get(AgentV2.ID.make("team/helper"))).toMatchObject({ system: "Help the team." })
expect(yield* agents.get(AgentV2.ID.make("native"))).toMatchObject({
system: "Use native v2 fields.",
request: { headers: { "x-agent": "native" }, body: { effort: "high" } },
permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }],
permissions: [
...defaultPermissions,
{ action: "edit", resource: "*", effect: "deny" },
shellOutputPermission,
],
})
expect(yield* agents.get(AgentV2.ID.make("disabled"))).toBeUndefined()
expect(yield* agents.get(AgentV2.ID.make("plan"))).toMatchObject({ system: "Make a plan.", mode: "primary" })
@ -357,3 +393,26 @@ function loadHomePermissions(home: string) {
return agent.permissions
})
}
function loadConfiguredPermissions(permissions: PermissionV2.Ruleset) {
return Effect.gen(function* () {
const agents = yield* AgentV2.Service
const build = AgentV2.ID.make("build")
yield* agents.transform((draft) => draft.update(build, () => {}))
const config = Config.Service.of({
entries: () =>
Effect.succeed([
new Config.Document({
type: "document",
info: decode({ permissions }),
}),
]),
})
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config),
)
const agent = yield* agents.get(build)
if (!agent) throw new Error("expected configured build agent")
return agent.permissions
})
}

View file

@ -34,8 +34,10 @@ describe("ConfigSkillPlugin.Plugin", () => {
return { dispose }
})
const agent = host().agent
yield* ConfigSkillPlugin.Plugin.effect(
host({
agent: { ...agent, transform: () => Effect.succeed({ dispose: Effect.void }) },
skill: { list: () => Effect.die("unused skill.list"), transform, reload: () => Effect.void },
}),
).pipe(