feat: handle opencode permission requests

This commit is contained in:
2026-06-08 13:32:50 +08:00
parent 4e31b141e7
commit 05d36aa8ca
5 changed files with 590 additions and 6 deletions
+86 -3
View File
@@ -3,6 +3,8 @@ import {
createOpencodeClient,
type OpencodeClient,
} from "@opencode-ai/sdk/v2";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { config } from "../config.js";
import { logger } from "../logger.js";
@@ -29,6 +31,8 @@ type RuntimeModelOverride = {
modelID: string;
};
export type PermissionReply = "once" | "always" | "reject";
export class OpencodeRuntimeAdapter {
private clientPromise: Promise<OpencodeClient> | null = null;
private closeServer: (() => void) | null = null;
@@ -129,6 +133,34 @@ export class OpencodeRuntimeAdapter {
return response.stream;
}
async replyPermission(options: {
requestId: string;
sessionId?: string;
reply: PermissionReply;
message?: string;
}) {
const client = await this.ensureClient();
if ("permission" in client && client.permission?.reply) {
const response = await client.permission.reply({
requestID: options.requestId,
reply: options.reply,
message: options.message,
});
return response.data;
}
if ("permission" in client && client.permission?.respond && options.sessionId) {
const response = await client.permission.respond({
sessionID: options.sessionId,
permissionID: options.requestId,
response: options.reply,
});
return response.data;
}
throw new Error("opencode permission reply API is unavailable");
}
async dispose(): Promise<void> {
this.closeServer?.();
this.closeServer = null;
@@ -174,9 +206,7 @@ export class OpencodeRuntimeAdapter {
hostname: config.OPENCODE_HOSTNAME,
port: config.OPENCODE_PORT,
timeout: config.OPENCODE_TIMEOUT_MS,
config: {
model: config.OPENCODE_MODEL,
},
config: buildOpencodeConfig(),
});
} catch (error) {
if (isMissingOpencodeCli(error)) {
@@ -207,6 +237,59 @@ export class OpencodeRuntimeAdapter {
export const opencodeRuntime = new OpencodeRuntimeAdapter();
function buildOpencodeConfig(): Record<string, unknown> {
return deepMerge(
deepMerge(readProjectOpencodeConfig(), readEnvOpencodeConfig()),
{
model: config.OPENCODE_MODEL,
},
);
}
function readProjectOpencodeConfig(): Record<string, unknown> {
const path = resolve(process.cwd(), "opencode.json");
if (!existsSync(path)) {
return {};
}
return parseConfigJson(readFileSync(path, "utf8"), path);
}
function readEnvOpencodeConfig(): Record<string, unknown> {
const content = process.env.OPENCODE_CONFIG_CONTENT;
if (!content?.trim()) {
return {};
}
return parseConfigJson(content, "OPENCODE_CONFIG_CONTENT");
}
function parseConfigJson(content: string, source: string): Record<string, unknown> {
const parsed = JSON.parse(content) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error(`${source} must contain a JSON object`);
}
return parsed as Record<string, unknown>;
}
function deepMerge(
left: Record<string, unknown>,
right: Record<string, unknown>,
): Record<string, unknown> {
const next = { ...left };
for (const [key, value] of Object.entries(right)) {
const existing = next[key];
if (isPlainObject(existing) && isPlainObject(value)) {
next[key] = deepMerge(existing, value);
} else {
next[key] = value;
}
}
return next;
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isMissingOpencodeCli(error: unknown): error is NodeJS.ErrnoException {
return (
typeof error === "object" &&