Skip to content

Commit c5e6f47

Browse files
suryaiyer95claude
andcommitted
fix(permission): route HTTP reply to PermissionNext to unblock tool ask deadlock
The v1.4.0 upstream merge introduced a new Effect-based `Permission` namespace (`packages/opencode/src/permission/index.ts`) and rewired the HTTP permission routes to it, but left the tool ask path on the existing `PermissionNext` namespace (`packages/opencode/src/permission/next.ts`). Result: ask and reply talk to two separate in-memory pending Maps, and every permission round-trip silently no-ops. Concretely: - `tool/bash.ts:155` and `altimate/tools/sql-execute.ts:32` call `ctx.ask(...)` → `PermissionNext.ask` (via `session/processor.ts` and `session/prompt.ts`), storing `{resolve, reject}` in `PermissionNext`'s pending Map and publishing `permission.asked`. - TUI `--yolo` auto-reply and manual `Allow once` / `Allow always` / `Reject` buttons all reply via `POST /permission/{id}/reply`, which post-merge invokes `Permission.reply` (`permission/index.ts:204`). - `Permission.reply` looks up the id in `Permission`'s pending Map, finds nothing, hits `if (!existing) return` (line 207), no-ops. The deferred in `PermissionNext.pending` never resolves; tool stays in `state.status = "running"` forever. - Because the early return is *before* `Bus.publish(Event.Replied, ...)`, the TUI's `permission.replied` handler in `cli/cmd/tui/context/sync.tsx:162` never clears the dialog — that's why "Allow always + Confirm" loops back to the same prompt. This change reverts the routing layer to `PermissionNext`, matching `main`. Three files: 1. `server/routes/permission.ts` — runtime fix; this is the route the TUI / yolo / SDK actually hit. 2. `server/routes/session.ts` — same bug pattern in the legacy `/session/:sessionID/permission/:permissionID` route. No callers in the current repo (TUI / ACP / SDK), but closes the dormant footgun for any external SDK consumer. 3. `session/session.sql.ts` — type-only annotation. `Permission.Ruleset` and `PermissionNext.Ruleset` are structurally identical zod schemas, so cosmetic / consistency-with-main. The new Effect-based `Permission` namespace from upstream is left untouched but currently has no callers. Follow-up: either delete it or migrate every `ctx.ask` site to it; out of scope for this fix. Verified via session DB inspection on the upstream/merge-v1.4.0 branch: `bash` on a non-trivial command (e.g. `which duckdb`) and `sql_execute` on a write query (e.g. `CREATE TABLE …`) both stuck with `state.status = "running"` indefinitely; tools that bypass `ctx.ask` (warehouse_*, `sql_execute` on read queries) were unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 04dbf62 commit c5e6f47

3 files changed

Lines changed: 11 additions & 11 deletions

File tree

packages/opencode/src/server/routes/permission.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Hono } from "hono"
22
import { describeRoute, validator, resolver } from "hono-openapi"
33
import z from "zod"
4-
import { Permission } from "@/permission"
4+
import { PermissionNext } from "@/permission/next"
55
import { PermissionID } from "@/permission/schema"
66
import { errors } from "../error"
77
import { lazy } from "../../util/lazy"
@@ -32,11 +32,11 @@ export const PermissionRoutes = lazy(() =>
3232
requestID: PermissionID.zod,
3333
}),
3434
),
35-
validator("json", z.object({ reply: Permission.Reply, message: z.string().optional() })),
35+
validator("json", z.object({ reply: PermissionNext.Reply, message: z.string().optional() })),
3636
async (c) => {
3737
const params = c.req.valid("param")
3838
const json = c.req.valid("json")
39-
await Permission.reply({
39+
await PermissionNext.reply({
4040
requestID: params.requestID,
4141
reply: json.reply,
4242
message: json.message,
@@ -55,14 +55,14 @@ export const PermissionRoutes = lazy(() =>
5555
description: "List of pending permissions",
5656
content: {
5757
"application/json": {
58-
schema: resolver(Permission.Request.array()),
58+
schema: resolver(PermissionNext.Request.array()),
5959
},
6060
},
6161
},
6262
},
6363
}),
6464
async (c) => {
65-
const permissions = await Permission.list()
65+
const permissions = await PermissionNext.list()
6666
return c.json(permissions)
6767
},
6868
),

packages/opencode/src/server/routes/session.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { Todo } from "../../session/todo"
1414
import { Agent } from "../../agent/agent"
1515
import { Snapshot } from "@/snapshot"
1616
import { Log } from "../../util/log"
17-
import { Permission } from "@/permission"
17+
import { PermissionNext } from "@/permission/next"
1818
import { PermissionID } from "@/permission/schema"
1919
import { ModelID, ProviderID } from "@/provider/schema"
2020
import { errors } from "../error"
@@ -1021,10 +1021,10 @@ export const SessionRoutes = lazy(() =>
10211021
permissionID: PermissionID.zod,
10221022
}),
10231023
),
1024-
validator("json", z.object({ response: Permission.Reply })),
1024+
validator("json", z.object({ response: PermissionNext.Reply })),
10251025
async (c) => {
10261026
const params = c.req.valid("param")
1027-
Permission.reply({
1027+
PermissionNext.reply({
10281028
requestID: params.permissionID,
10291029
reply: c.req.valid("json").response,
10301030
})

packages/opencode/src/session/session.sql.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlit
22
import { ProjectTable } from "../project/project.sql"
33
import type { MessageV2 } from "./message-v2"
44
import type { Snapshot } from "../snapshot"
5-
import type { Permission } from "../permission"
5+
import type { PermissionNext } from "../permission/next"
66
import type { ProjectID } from "../project/schema"
77
import type { SessionID, MessageID, PartID } from "./schema"
88
import type { WorkspaceID } from "../control-plane/schema"
@@ -31,7 +31,7 @@ export const SessionTable = sqliteTable(
3131
summary_files: integer(),
3232
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
3333
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(),
34-
permission: text({ mode: "json" }).$type<Permission.Ruleset>(),
34+
permission: text({ mode: "json" }).$type<PermissionNext.Ruleset>(),
3535
...Timestamps,
3636
time_compacting: integer(),
3737
time_archived: integer(),
@@ -99,5 +99,5 @@ export const PermissionTable = sqliteTable("permission", {
9999
.primaryKey()
100100
.references(() => ProjectTable.id, { onDelete: "cascade" }),
101101
...Timestamps,
102-
data: text({ mode: "json" }).notNull().$type<Permission.Ruleset>(),
102+
data: text({ mode: "json" }).notNull().$type<PermissionNext.Ruleset>(),
103103
})

0 commit comments

Comments
 (0)