Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/opencode/src/permission/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ const layer = Layer.effect(

if (!needsAsk) return

// Deduplicate: if an identical permission request is already pending in
// the same session (same permission + patterns), wait on the existing
// deferred instead of creating a duplicate prompt. When the user
// responds, all waiters proceed together.
for (const entry of pending.values()) {
if (
entry.info.sessionID === request.sessionID &&
entry.info.permission === request.permission &&
entry.info.patterns.length === request.patterns.length &&
entry.info.patterns.every((p, i) => p === request.patterns[i])
) {
return yield* Deferred.await(entry.deferred)
}
}

const id = request.id ?? PermissionV1.ID.ascending()
const info: PermissionV1.Request = {
id,
Expand Down
82 changes: 80 additions & 2 deletions packages/opencode/test/permission/next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -850,15 +850,15 @@ it.instance(
permission: "bash",
patterns: ["ls"],
metadata: {},
always: ["ls"],
always: ["*"],
ruleset: [],
}).pipe(Effect.forkScoped)

const b = yield* ask({
id: PermissionV1.ID.make("per_test5b"),
sessionID: SessionID.make("session_same"),
permission: "bash",
patterns: ["ls"],
patterns: ["rm"],
metadata: {},
always: [],
ruleset: [],
Expand All @@ -874,6 +874,84 @@ it.instance(
{ git: true },
)

it.instance(
"ask - deduplicates identical permission requests in same session",
() =>
Effect.gen(function* () {
const a = yield* ask({
id: PermissionV1.ID.make("per_dedup_a"),
sessionID: SessionID.make("session_dedup"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)

const b = yield* ask({
id: PermissionV1.ID.make("per_dedup_b"),
sessionID: SessionID.make("session_dedup"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)

// Only one pending request should exist — b is deduplicated to a
yield* waitForPending(1)
expect((yield* list()).map((item) => item.id)).toEqual([PermissionV1.ID.make("per_dedup_a")])

yield* reply({ requestID: PermissionV1.ID.make("per_dedup_a"), reply: "once" })

// Both callers should succeed from a single reply
yield* Fiber.join(a)
yield* Fiber.join(b)
expect(yield* list()).toHaveLength(0)
}),
{ git: true },
)

it.instance(
"ask - does not deduplicate requests with different patterns",
() =>
Effect.gen(function* () {
const a = yield* ask({
id: PermissionV1.ID.make("per_nodedup_a"),
sessionID: SessionID.make("session_nodedup"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)

const b = yield* ask({
id: PermissionV1.ID.make("per_nodedup_b"),
sessionID: SessionID.make("session_nodedup"),
permission: "bash",
patterns: ["rm"],
metadata: {},
always: [],
ruleset: [],
}).pipe(Effect.forkScoped)

// Different patterns → both pending
yield* waitForPending(2)
expect((yield* list()).map((item) => item.id)).toEqual([
PermissionV1.ID.make("per_nodedup_a"),
PermissionV1.ID.make("per_nodedup_b"),
])

// Rejecting one cascades to all pending in the same session
yield* reply({ requestID: PermissionV1.ID.make("per_nodedup_a"), reply: "reject" })
const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
expect(Exit.isFailure(ea)).toBe(true)
expect(Exit.isFailure(eb)).toBe(true)
}),
{ git: true },
)

it.instance(
"reply - always keeps other session pending",
() =>
Expand Down
Loading