Skip to content

Commit 5eadf70

Browse files
author
李冠辰
committed
fix(opencode): publish permission cleanup replies
1 parent a78605f commit 5eadf70

2 files changed

Lines changed: 103 additions & 30 deletions

File tree

packages/opencode/src/permission/index.ts

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -157,17 +157,34 @@ export const layer = Layer.effect(
157157

158158
yield* Effect.addFinalizer(() =>
159159
Effect.gen(function* () {
160-
for (const item of state.pending.values()) {
160+
for (const id of Array.from(state.pending.keys())) {
161+
const item = yield* removePending(state.pending, id, "reject")
162+
if (!item) continue
161163
yield* Deferred.fail(item.deferred, new RejectedError())
162164
}
163-
state.pending.clear()
164165
}),
165166
)
166167

167168
return state
168169
}),
169170
)
170171

172+
const removePending = Effect.fn("Permission.removePending")(function* (
173+
pending: Map<PermissionID, PendingEntry>,
174+
id: PermissionID,
175+
reply: Reply,
176+
) {
177+
const item = pending.get(id)
178+
if (!item) return undefined
179+
pending.delete(id)
180+
yield* bus.publish(Event.Replied, {
181+
sessionID: item.info.sessionID,
182+
requestID: item.info.id,
183+
reply,
184+
})
185+
return item
186+
})
187+
171188
const ask = Effect.fn("Permission.ask")(function* (input: AskInput) {
172189
const { approved, pending } = yield* InstanceState.get(state)
173190
const { ruleset, ...request } = input
@@ -202,26 +219,14 @@ export const layer = Layer.effect(
202219
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
203220
pending.set(id, { info, deferred })
204221
yield* bus.publish(Event.Asked, info)
205-
return yield* Effect.ensuring(
206-
Deferred.await(deferred),
207-
Effect.sync(() => {
208-
pending.delete(id)
209-
}),
210-
)
222+
return yield* Effect.ensuring(Deferred.await(deferred), removePending(pending, id, "reject").pipe(Effect.ignore))
211223
})
212224

213225
const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) {
214226
const { approved, pending } = yield* InstanceState.get(state)
215-
const existing = pending.get(input.requestID)
227+
const existing = yield* removePending(pending, input.requestID, input.reply)
216228
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
217229

218-
pending.delete(input.requestID)
219-
yield* bus.publish(Event.Replied, {
220-
sessionID: existing.info.sessionID,
221-
requestID: existing.info.id,
222-
reply: input.reply,
223-
})
224-
225230
if (input.reply === "reject") {
226231
yield* Deferred.fail(
227232
existing.deferred,
@@ -230,13 +235,8 @@ export const layer = Layer.effect(
230235

231236
for (const [id, item] of pending.entries()) {
232237
if (item.info.sessionID !== existing.info.sessionID) continue
233-
pending.delete(id)
234-
yield* bus.publish(Event.Replied, {
235-
sessionID: item.info.sessionID,
236-
requestID: item.info.id,
237-
reply: "reject",
238-
})
239-
yield* Deferred.fail(item.deferred, new RejectedError())
238+
const removed = yield* removePending(pending, id, "reject")
239+
if (removed) yield* Deferred.fail(removed.deferred, new RejectedError())
240240
}
241241
return
242242
}
@@ -258,13 +258,8 @@ export const layer = Layer.effect(
258258
(pattern) => evaluate(item.info.permission, pattern, approved).action === "allow",
259259
)
260260
if (!ok) continue
261-
pending.delete(id)
262-
yield* bus.publish(Event.Replied, {
263-
sessionID: item.info.sessionID,
264-
requestID: item.info.id,
265-
reply: "always",
266-
})
267-
yield* Deferred.succeed(item.deferred, undefined)
261+
const removed = yield* removePending(pending, id, "always")
262+
if (removed) yield* Deferred.succeed(removed.deferred, undefined)
268263
}
269264
})
270265

packages/opencode/test/permission/next.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { InstanceBootstrap } from "../../src/project/bootstrap-service"
99
import { InstanceStore } from "../../src/project/instance-store"
1010
import { TestInstance, tmpdirScoped } from "../fixture/fixture"
1111
import { testEffect } from "../lib/effect"
12+
import { waitGlobalBusEvent } from "../server/global-bus"
1213
import { MessageID, SessionID } from "../../src/session/schema"
1314

1415
const bus = Bus.layer
@@ -50,6 +51,22 @@ const waitForPending = (count: number) =>
5051
)
5152
})
5253

54+
const waitForReplied = (requestID: PermissionID) =>
55+
Effect.gen(function* () {
56+
const bus = yield* Bus.Service
57+
const seen = yield* Deferred.make<{ sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }>()
58+
const unsub = yield* bus.subscribeCallback(Permission.Event.Replied, (event) => {
59+
if (event.properties.requestID === requestID) Deferred.doneUnsafe(seen, Effect.succeed(event.properties))
60+
})
61+
yield* Effect.addFinalizer(() => Effect.sync(unsub))
62+
return yield* Deferred.await(seen).pipe(
63+
Effect.timeoutOrElse({
64+
duration: "1 second",
65+
orElse: () => Effect.fail(new Error(`timed out waiting for permission replied event: ${requestID}`)),
66+
}),
67+
)
68+
})
69+
5370
const fail = <A, E, R>(self: Effect.Effect<A, E, R>) =>
5471
Effect.gen(function* () {
5572
const exit = yield* self.pipe(Effect.exit)
@@ -1003,6 +1020,31 @@ it.live("permission requests stay isolated by directory", () =>
10031020
}),
10041021
)
10051022

1023+
it.instance(
1024+
"ask - publishes replied event when pending request is interrupted",
1025+
() =>
1026+
Effect.gen(function* () {
1027+
const requestID = PermissionID.make("per_interrupt_event")
1028+
const sessionID = SessionID.make("session_interrupt_event")
1029+
const replied = yield* waitForReplied(requestID).pipe(Effect.forkScoped)
1030+
const fiber = yield* ask({
1031+
id: requestID,
1032+
sessionID,
1033+
permission: "bash",
1034+
patterns: ["ls"],
1035+
metadata: {},
1036+
always: [],
1037+
ruleset: [],
1038+
}).pipe(Effect.forkScoped)
1039+
1040+
expect(yield* waitForPending(1)).toHaveLength(1)
1041+
yield* Fiber.interrupt(fiber)
1042+
expect(yield* Fiber.join(replied)).toEqual({ sessionID, requestID, reply: "reject" })
1043+
expect(yield* list()).toHaveLength(0)
1044+
}),
1045+
{ git: true },
1046+
)
1047+
10061048
it.instance(
10071049
"pending permission rejects on instance dispose",
10081050
() =>
@@ -1030,6 +1072,42 @@ it.instance(
10301072
{ git: true },
10311073
)
10321074

1075+
it.instance(
1076+
"pending permission publishes global replied event on instance dispose",
1077+
() =>
1078+
Effect.gen(function* () {
1079+
const requestID = PermissionID.make("per_dispose_event")
1080+
const sessionID = SessionID.make("session_dispose_event")
1081+
const test = yield* TestInstance
1082+
const store = yield* InstanceStore.Service
1083+
const replied = yield* waitGlobalBusEvent({
1084+
message: "timed out waiting for global permission replied event",
1085+
predicate: (event) =>
1086+
event.directory === test.directory &&
1087+
event.payload.type === "permission.replied" &&
1088+
event.payload.properties.requestID === requestID,
1089+
}).pipe(Effect.forkScoped)
1090+
const fiber = yield* ask({
1091+
id: requestID,
1092+
sessionID,
1093+
permission: "bash",
1094+
patterns: ["ls"],
1095+
metadata: {},
1096+
always: [],
1097+
ruleset: [],
1098+
}).pipe(Effect.forkScoped)
1099+
1100+
expect(yield* waitForPending(1)).toHaveLength(1)
1101+
const ctx = yield* store.load({ directory: test.directory })
1102+
yield* store.dispose(ctx)
1103+
1104+
const exit = yield* Fiber.await(fiber)
1105+
expect(Exit.isFailure(exit)).toBe(true)
1106+
expect((yield* Fiber.join(replied)).payload.properties).toEqual({ sessionID, requestID, reply: "reject" })
1107+
}),
1108+
{ git: true },
1109+
)
1110+
10331111
it.instance(
10341112
"pending permission rejects on instance reload",
10351113
() =>

0 commit comments

Comments
 (0)