SDK v1.17.12: session.permission — programmatic create/fetch, more reliable auto-accept (#1982)

* docs: add SDK v1.17.12 migration plan — phase 4 (session.permission)

* feat(permissions): verify pending permission before auto-accept via SDK v1.17.12

Adds createPermission() and fetchPermission() wrappers on OpencodeService
for the new v2.session.permission endpoints (OpenCode SDK 1.17.12).

fetchPermission() is used by the auto-accept sweep in
resyncBlockingRequestsForDirectory to verify a permission is still
pending before replying. The auto-accept flow now skips permissions
that are already resolved, returning a null from fetchPermission()
rather than blindly calling respondToPermission on a stale entry.

createPermission() is exposed for future programmatic permission
creation; the V1 list/reply path used by the UI is unchanged.

The plan doc at plans/opencode-v1.17.12-sdk/ was rebased onto
origin/main in the prior commit to keep the PR diff focused on
this change.

Closes #1972

* fix(permissions): drop confirmed-resolved permissions from auto-accept resync

fetchPermission() now returns a tagged FetchPermissionResult so the
auto-accept loop can distinguish a server-confirmed 404 (the
permission is no longer pending) from a fetch failure (network error
or pre-v1.17.12 server). Previously both cases collapsed to null, so
a permission the server had already answered would still appear in
the resync output and trigger a spurious 'Permission needed' toast.

The auto-accept loop in resyncBlockingRequestsForDirectory now tracks
both accepted and resolved permissions, then drops both from the
'grouped' map before it falls through to the toast path. On a
pre-v1.17.12 server (no V2 endpoint) the call still returns
'unknown' and the permission stays in the resync output so the user
can answer manually — fail-closed, no false-resolved signals.

Adds a focused unit test for fetchPermission (4 cases: 200 ok, 404
resolved, 500 unknown, network throw) mocking the V2 SDK client
shape.

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
This commit is contained in:
Leonid
2026-07-11 16:11:13 +03:00
committed by GitHub
co-authored by bashrusakh
parent 6d7ea82d86
commit 01a52eccab
8 changed files with 585 additions and 6 deletions
+37 -6
View File
@@ -1139,13 +1139,41 @@ export async function resyncBlockingRequestsForDirectory(
if (autoAcceptingSessionIds.length > 0) {
const acceptedIdsBySession = new Map<string, Set<string>>()
// Track server-confirmed resolved permissions separately so we can
// remove them from `grouped` below — the V1 listPendingPermissions
// snapshot can still contain entries the server has already answered,
// and leaving them in place produces a spurious "Permission needed"
// toast for a permission the user has already resolved.
const resolvedIdsBySession = new Map<string, Set<string>>()
await Promise.all(autoAcceptingSessionIds.flatMap((sessionId) =>
(grouped[sessionId] ?? []).map(async (permission) => {
try {
await sessionActions.respondToPermission(permission.sessionID, permission.id, "once")
const accepted = acceptedIdsBySession.get(sessionId) ?? new Set<string>()
accepted.add(permission.id)
acceptedIdsBySession.set(sessionId, accepted)
// Verify the permission is still pending before auto-accepting.
// - state: "ok" → still pending, safe to auto-accept
// - state: "resolved" → server returned 404, drop from grouped
// - state: "unknown" → network error / pre-1.17.12 server,
// keep in grouped for the user to act on
//
// On a pre-v1.17.12 server without the V2 endpoint, every call
// returns "unknown". This permanently disables auto-accept
// (acknowledged scope tradeoff — project requires SDK 1.17.12)
// but does not falsely report permissions as resolved.
const outcome = await opencodeClient.fetchPermission(
permission.sessionID,
permission.id,
)
if (outcome.state === "ok") {
await sessionActions.respondToPermission(permission.sessionID, permission.id, "once")
const accepted = acceptedIdsBySession.get(sessionId) ?? new Set<string>()
accepted.add(permission.id)
acceptedIdsBySession.set(sessionId, accepted)
} else if (outcome.state === "resolved") {
const resolved = resolvedIdsBySession.get(sessionId) ?? new Set<string>()
resolved.add(permission.id)
resolvedIdsBySession.set(sessionId, resolved)
}
// state: "unknown" → keep the permission in grouped; user can
// answer manually.
} catch {
// Keep failed auto-accept permissions in UI state so the user can act.
}
@@ -1154,8 +1182,11 @@ export async function resyncBlockingRequestsForDirectory(
for (const sessionId of autoAcceptingSessionIds) {
const acceptedIds = acceptedIdsBySession.get(sessionId)
if (!acceptedIds) continue
const remaining = (grouped[sessionId] ?? []).filter((permission) => !acceptedIds.has(permission.id))
const resolvedIds = resolvedIdsBySession.get(sessionId)
if (!acceptedIds && !resolvedIds) continue
const drop = (id: string) =>
acceptedIds?.has(id) || resolvedIds?.has(id) || false
const remaining = (grouped[sessionId] ?? []).filter((permission) => !drop(permission.id))
if (remaining.length > 0) grouped[sessionId] = remaining
else delete grouped[sessionId]
}