fix(vscode): stop gating list-derived permission auto-accept on the V2 preflight (#3318)

Thanks for fixing both permission reconciliation paths. The fetchPermission documentation follow-up is maintainer-owned; #3259 remains open.
This commit is contained in:
Leonid
2026-09-05 02:14:17 +03:00
committed by GitHub
parent 578b1c2f86
commit ec86d738e2
3 changed files with 32 additions and 6 deletions
+1 -1
View File
@@ -161,7 +161,7 @@ The active-session watchdog in `sync-context.tsx` (per-directory status polls an
Imperative cross-directory session lookups use the cached ID index from `getAllSyncSessionMap()`. The index is rebuilt only when a child store's `state.session` reference changes; permission lineage checks must reuse it instead of rebuilding a full session map per call.
VS Code does not run the server permission-auto-accept runtime. The extension host persists and broadcasts authoritative policy, while its foreground UI runtime resolves missing child-session lineage through the OpenCode API before deciding whether to suppress and answer a `permission.asked` event. Once policy is enabled, a live `permission.asked` event sends the directory-scoped `permission.reply` immediately and does not block on a permission-state preflight request. Enabling the policy treats permission cards already present in the directory store the same way and replies immediately, then reconciles the server's pending list with a state preflight so stale already-resolved requests are not replied to or resurrected. Reconnect/bootstrap also uses the preflight while reconciling pending requests in the session directory, including requests inherited by child sessions. Unknown lineage and exhausted reply retries fail closed and leave the request available for manual action. A later `permission.replied` event invalidates any older deferred ask so the async policy check cannot resurrect a resolved request. With every OpenChamber webview closed or suspended no responder runs; this is an intentional VS Code limitation. Other runtimes remain fully server-owned.
VS Code does not run the server permission-auto-accept runtime. The extension host persists and broadcasts authoritative policy, while its foreground UI runtime resolves missing child-session lineage through the OpenCode API before deciding whether to suppress and answer a `permission.asked` event. Once policy is enabled, a live `permission.asked` event sends the directory-scoped `permission.reply` immediately and does not block on a permission-state preflight request. Enabling the policy treats permission cards already present in the directory store the same way and replies immediately, then reconciles the server's pending list by replying to listed requests directly without a permission-state preflight: `permission.list` is served by the V1 pending map while the state check reads the separate V2 map, so a preflight "resolved" verdict cannot prove a listed request settled. Reconnect/bootstrap reconciles pending requests in the session directory the same way, including requests inherited by child sessions. Unknown lineage and exhausted reply retries fail closed and leave the request available for manual action. A later `permission.replied` event invalidates any older deferred ask so the async policy check cannot resurrect a resolved request. With every OpenChamber webview closed or suspended no responder runs; this is an intentional VS Code limitation. Other runtimes remain fully server-owned.
### Mutation responsibility
@@ -199,19 +199,28 @@ describe("VS Code permission auto-accept runtime", () => {
expect(replyStarted).toBe(true)
})
test("keeps the permission-state preflight for refresh reconciliation", async () => {
test("reconciles list-derived permissions without the stale V2 preflight", async () => {
let replyCalls = 0
let stateChecks = 0
const runtime = createVSCodePermissionAutoAcceptRuntime({
getPolicy: () => ({ child: true }),
getSessions: () => new Map(),
getSession: async () => session("child"),
listPendingPermissions: async () => [{ ...permission, id: "resolved" }],
getPermissionState: async () => "resolved",
getPermissionState: async () => {
stateChecks += 1
return "resolved"
},
reply: async () => { replyCalls += 1 },
wait: async () => undefined,
})
// permission.list (V1 authority) and session.permission.get (V2
// authority) are different maps on the Stable runtime, so a "resolved"
// preflight verdict cannot prove a list-derived request was settled and
// must not suppress the reply.
await runtime.reconcilePending("/repo")
expect(replyCalls).toBe(0)
expect(stateChecks).toBe(0)
expect(replyCalls).toBe(1)
})
})
@@ -109,7 +109,15 @@ export function createVSCodePermissionAutoAcceptRuntime(dependencies: Dependenci
// those visible cards before the network reconciliation so enabling the
// toggle works even when permission.list is unavailable or stale.
await processAll(dependencies.getKnownPendingPermissions?.(directory) ?? [], false)
await processAll(await dependencies.listPendingPermissions(directory), true)
// The list arm must not pre-check against getPermissionState:
// permission.list is served by the V1 pending map while
// getPermissionState reads the V2 map, and on the Stable runtime those
// authorities are separate. A V1-only pending request therefore answers
// 404 ("resolved") from the V2 check and the reply would be skipped
// while the request stays pending. Re-enable the pre-check only when
// runtime detection lets it consult the same authority that produced
// the list.
await processAll(await dependencies.listPendingPermissions(directory), false)
})()
.finally(() => reconcileInFlight.delete(key))
@@ -135,5 +143,14 @@ export const processVSCodePermissionAutoAccept = (
permission: PermissionRequest,
directory?: string,
) => runtime.processPermission(permission, directory, { verifyPending: false })
export const processVSCodeReconciledPermissionAutoAccept = runtime.processPermission
// List-derived permissions (permission.list, the V1 authority on the stable
// runtime) must not be gated on the V2 session.permission.get preflight: on
// Stable/V1 the V1 producer and the V2 checker keep separate pending maps, so
// a 404 from the checker means "different authority", not "resolved". The
// preflight returns only after the selected runtime's own authority backs the
// check (runtime detection, dual-runtime foundation). See #3259.
export const processVSCodeReconciledPermissionAutoAccept = (
permission: PermissionRequest,
directory?: string,
) => runtime.processPermission(permission, directory, { verifyPending: false })
export const reconcileVSCodePendingPermissions = runtime.reconcilePending