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:
committed by
GitHub
co-authored by
bashrusakh
parent
6d7ea82d86
commit
01a52eccab
@@ -1,4 +1,5 @@
|
||||
import { createOpencodeClient, OpencodeClient } from "@opencode-ai/sdk/v2";
|
||||
import type { PermissionV2Request, PermissionV2Effect, PermissionV2Source } from "@opencode-ai/sdk/v2/client";
|
||||
import type { FilesAPI } from "../api/types";
|
||||
import { getDesktopHomeDirectory } from "../desktop";
|
||||
import type {
|
||||
@@ -13,6 +14,17 @@ import type {
|
||||
} from "@opencode-ai/sdk/v2";
|
||||
import type { PermissionRequest } from "@/types/permission";
|
||||
import type { QuestionRequest } from "@/types/question";
|
||||
|
||||
/**
|
||||
* Tagged result of `OpencodeService.fetchPermission()`. The caller can
|
||||
* distinguish a server-confirmed "no longer pending" permission (HTTP
|
||||
* 404) from a fetch failure (network error, malformed response, or a
|
||||
* pre-v1.17.12 server without the V2 endpoint).
|
||||
*/
|
||||
export type FetchPermissionResult =
|
||||
| { state: "ok"; permission: PermissionV2Request }
|
||||
| { state: "resolved" }
|
||||
| { state: "unknown" };
|
||||
import { getRuntimeUrlResolver } from "@/lib/runtime-url";
|
||||
import { runtimeFetch } from "@/lib/runtime-fetch";
|
||||
import { getRuntimeKey } from "@/lib/runtime-switch";
|
||||
@@ -1108,6 +1120,106 @@ class OpencodeService {
|
||||
return unwrapSdkOptional(response, 'permission.reply') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically evaluate and (when approval is required) create a
|
||||
* permission request for a session via the V2 endpoint introduced in
|
||||
* OpenCode SDK v1.17.12. Wraps `session.permission.create`.
|
||||
*
|
||||
* Returns `{ id, effect }` on success, or `null` on any failure
|
||||
* (network error, 4xx/5xx response, malformed payload, or pre-v1.17.12
|
||||
* server without the V2 endpoint). Callers driving authoritative state
|
||||
* must treat `null` as "unknown — do not act" rather than "permission
|
||||
* allowed."
|
||||
*
|
||||
* Thin wrapper for future programmatic permission creation. The V1
|
||||
* `permission.list` / `permission.reply` flow used by the auto-accept
|
||||
* path is unchanged.
|
||||
*/
|
||||
async createPermission(
|
||||
sessionID: string,
|
||||
action: string,
|
||||
resources: string[],
|
||||
options?: {
|
||||
id?: string;
|
||||
save?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
source?: PermissionV2Source;
|
||||
agent?: string;
|
||||
}
|
||||
): Promise<{ id: string; effect: PermissionV2Effect } | null> {
|
||||
try {
|
||||
const response = await this.client.v2.session.permission.create({
|
||||
sessionID,
|
||||
action,
|
||||
resources,
|
||||
...(options?.id ? { id: options.id } : {}),
|
||||
...(options?.save ? { save: options.save } : {}),
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
...(options?.source ? { source: options.source } : {}),
|
||||
...(options?.agent ? { agent: options.agent } : {}),
|
||||
});
|
||||
// Discriminated union narrowing on `error` (see fetchPermission).
|
||||
if (response.error !== undefined) return null;
|
||||
const payload = response.data?.data;
|
||||
if (payload === undefined) return null;
|
||||
return { id: payload.id, effect: payload.effect };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a pending permission request owned by a session via the V2
|
||||
* endpoint introduced in OpenCode SDK v1.17.12. Wraps
|
||||
* `session.permission.get`.
|
||||
*
|
||||
* Returns a tagged `FetchPermissionResult` so the caller can distinguish
|
||||
* a confirmed-resolved permission (HTTP 404) from a fetch failure
|
||||
* (network error, malformed response, or pre-v1.17.12 server without
|
||||
* the V2 endpoint). The auto-accept flow uses this distinction to drop
|
||||
* resolved permissions from the resync output, preventing stale
|
||||
* `permission.list` entries from sticking around in the UI.
|
||||
*/
|
||||
async fetchPermission(
|
||||
sessionID: string,
|
||||
requestID: string,
|
||||
): Promise<FetchPermissionResult> {
|
||||
try {
|
||||
// The V2 path is session-scoped and does not require a `directory`
|
||||
// parameter. The client-scoped directory (set via setDirectory) is
|
||||
// honored by the underlying SDK client when the call is routed.
|
||||
const response = await this.client.v2.session.permission.get({
|
||||
sessionID,
|
||||
requestID,
|
||||
});
|
||||
// The SDK returns a discriminated union on `error`/`data` (HeyApi
|
||||
// `RequestResult` with `ThrowOnError = false`). The error branch
|
||||
// collapses `data` to `undefined`; the data branch returns the
|
||||
// 200-response payload as `{ data: PermissionV2Request }`. Narrow
|
||||
// via `error` first, then unwrap the inner `data` field.
|
||||
if (response.error === undefined) {
|
||||
const payload = response.data?.data;
|
||||
if (payload !== undefined) {
|
||||
return { state: "ok", permission: payload };
|
||||
}
|
||||
}
|
||||
// On the error branch the server has answered but the request was
|
||||
// not found. V2SessionPermissionGetErrors maps 404 to
|
||||
// `PermissionNotFoundError`, so the only server-confirmed
|
||||
// "no longer pending" signal we have is HTTP 404.
|
||||
if (response.response?.status === 404) {
|
||||
return { state: "resolved" };
|
||||
}
|
||||
return { state: "unknown" };
|
||||
} catch {
|
||||
// Network failure, pre-v1.17.12 server, or runtimeFetch throwing.
|
||||
// Treat as "unknown" — caller must decide what to do (auto-accept
|
||||
// fails closed, but the permission stays in the resync output so
|
||||
// the user can still act on it).
|
||||
return { state: "unknown" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws on fetch/SDK failure. Callers that drive authoritative state from
|
||||
* the result (e.g. reconnect resync) must let the throw propagate so they
|
||||
|
||||
Reference in New Issue
Block a user