Compare commits

...
Author SHA1 Message Date
rekram1-node 62f73d1eb2 fix(core): allow plan writes from home location 2026-08-31 13:02:47 +00:00
3 changed files with 39 additions and 8 deletions
+14 -6
View File
@@ -76,6 +76,17 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Lo
const slash = (value: string) => value.replaceAll("\\", "/")
export const permissionTarget = (location: Location.Info, absolute: string) => {
const worktree = path.resolve(location.project.directory)
const internal =
FSUtil.contains(location.directory, absolute) ||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
return {
internal,
resource: slash(internal ? path.relative(location.directory, absolute) || "." : absolute),
}
}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
@@ -85,14 +96,11 @@ const layer = Layer.effect(
const resolve = Effect.fnUntraced(function* (input: ResolveInput) {
const absolute = resolvePath(location.directory, input.path)
const worktree = path.resolve(location.project.directory)
const internal =
FSUtil.contains(location.directory, absolute) ||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
if (internal) {
const target = permissionTarget(location, absolute)
if (target.internal) {
return {
absolute,
resource: slash(path.relative(location.directory, absolute) || "."),
resource: target.resource,
} satisfies Target
}
const type =
+3 -1
View File
@@ -7,6 +7,7 @@ import type { SessionEvent } from "@opencode-ai/schema/session-event"
import { Global } from "@opencode-ai/util/global"
import { Effect, Stream } from "effect"
import path from "path"
import { LocationMutation } from "../location-mutation.js"
import { Permission } from "../permission.js"
const plan = Agent.ID.make("plan")
@@ -29,6 +30,7 @@ export const Plugin = define({
effect: Effect.fn(function* (ctx) {
const global = yield* Global.Service
const directory = path.join(global.home, ".opencode", "plan")
const resource = LocationMutation.permissionTarget(ctx.location, directory).resource
const enterReminder = enter(directory)
yield* ctx.agent.transform((draft) => {
draft.update(plan, (item) => {
@@ -37,7 +39,7 @@ export const Plugin = define({
item.mode = "primary"
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
item.permissions.push({ action: "edit", resource: "*", effect: "deny" })
item.permissions.push({ action: "edit", resource: path.join(directory, "*"), effect: "allow" })
item.permissions.push({ action: "edit", resource: path.join(resource, "*"), effect: "allow" })
item.permissions.push({ action: "external_directory", resource: path.join(directory, "*"), effect: "allow" })
})
})
+22 -1
View File
@@ -9,7 +9,9 @@ import { Event } from "@opencode-ai/schema/event"
import { Model } from "@opencode-ai/core/model"
import { PlanPlugin } from "@opencode-ai/core/plugin/plan"
import { Permission } from "@opencode-ai/core/permission"
import { Project } from "@opencode-ai/core/project"
import { Provider } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Session } from "@opencode-ai/core/session"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionInbox } from "@opencode-ai/core/session/inbox"
@@ -35,7 +37,10 @@ const agentSelected = (agent: Agent.ID, previous: Agent.ID): SessionEvent.AgentS
})
/** Runs the plan plugin against stubbed domains, capturing persisted reminders and the context hook. */
const run = Effect.fnUntraced(function* (events: ReadonlyArray<SessionEvent.AgentSelected> = []) {
const run = Effect.fnUntraced(function* (
events: ReadonlyArray<SessionEvent.AgentSelected> = [],
locationDirectory = "/workspace",
) {
const persisted = new Array<string>()
let contextHook: ((input: SessionContext) => Effect.Effect<void>) | undefined
let toolHook: ((input: ToolHooks["execute.after"]) => Effect.Effect<void>) | undefined
@@ -53,6 +58,14 @@ const run = Effect.fnUntraced(function* (events: ReadonlyArray<SessionEvent.Agen
const driver = Environment.makeMemoryDriver()
yield* PlanPlugin.Plugin.effect(
host({
location: {
directory: AbsolutePath.make(locationDirectory),
project: {
id: Project.ID.global,
directory: AbsolutePath.make(locationDirectory),
canonical: AbsolutePath.make(locationDirectory),
},
},
agent: {
get: () => Effect.die("unused agent.get"),
list: () => Effect.die("unused agent.list"),
@@ -263,6 +276,14 @@ describe("plan plugin mutations", () => {
}),
)
it.effect("allows the Plan directory when the Location is the home directory", () =>
Effect.gen(function* () {
const { planAgent } = yield* run([], home)
expect(Permission.evaluate("edit", ".opencode/plan/work.md", planAgent.permissions).effect).toBe("allow")
expect(Permission.evaluate("edit", "source.ts", planAgent.permissions).effect).toBe("deny")
}),
)
it.effect("allows the Plan directory external boundary", () =>
Effect.gen(function* () {
const { planAgent } = yield* run()