feat(agent): 完善权限与结果引用安全

This commit is contained in:
2026-08-06 18:41:52 +08:00
parent a5e91ac2b8
commit b19af8846a
22 changed files with 621 additions and 47 deletions
+7 -1
View File
@@ -65,7 +65,13 @@ const payloadSchema = z.object({
model: z.string().refine(isSupportedModel, {
message: "unsupported model",
}).optional(),
approval_mode: z.enum(["request", "always"]).optional().default("request"),
approval_mode: z
.enum(["request", "auto", "always"])
.optional()
.default("request")
.describe(
"request forwards approval prompts; auto approves only low-risk allowlisted tools; always approves every prompt not explicitly denied by OpenCode",
),
});
const createSessionPayloadSchema = z.object({
+57
View File
@@ -0,0 +1,57 @@
export type ApprovalMode = "request" | "auto" | "always";
const lowRiskToolPermissions = new Set([
"apply_layer_style",
"geocode",
"locate_features",
"render_junctions",
"show_chart",
"tjwater_server_query",
"view_history",
"view_scada",
"web_search",
"zoom_to_map",
]);
const normalizePermission = (permission: string) => permission.trim().toLowerCase();
export const canAutoApprovePermission = (permission: string): boolean => {
const normalized = normalizePermission(permission);
if (lowRiskToolPermissions.has(normalized)) {
return true;
}
if (normalized.startsWith("tjwater_")) {
return lowRiskToolPermissions.has(normalized.slice("tjwater_".length));
}
return false;
};
export const resolvePermissionApproval = (
approvalMode: ApprovalMode,
permission: string,
) => {
if (approvalMode === "always") {
return {
autoApprove: true,
title: "已按始终允许模式放行",
detail:
"当前会话处于始终允许模式,已放行本次权限请求;明确禁止的权限仍由 OpenCode 拒绝。",
} as const;
}
if (approvalMode === "auto" && canAutoApprovePermission(permission)) {
return {
autoApprove: true,
title: "已自动批准低风险权限",
detail: "当前批准模式允许自动执行低风险工具,已放行本次请求。",
} as const;
}
return {
autoApprove: false,
title: "等待权限确认",
detail: undefined,
} as const;
};
+25 -19
View File
@@ -46,6 +46,10 @@ import {
type TodoItemPayload,
type TodoUpdatePayload,
} from "./chatStreamEvents.js";
import {
resolvePermissionApproval,
type ApprovalMode,
} from "./chatPermissionPolicy.js";
export {
collectTextContent,
@@ -55,7 +59,7 @@ export {
type TodoUpdatePayload,
} from "./chatStreamEvents.js";
export type ApprovalMode = "request" | "always";
export type { ApprovalMode } from "./chatPermissionPolicy.js";
type StreamPromptOptions = {
runtime: OpencodeRuntimeAdapter;
@@ -372,6 +376,10 @@ export const streamPromptResponse = async ({
if (isPermissionAskedEvent(event)) {
sawResponseActivity = true;
const permissionApproval = resolvePermissionApproval(
approvalMode,
event.properties.permission,
);
logDevelopmentDebug("permission request received", {
...debugContext,
requestId: event.properties.id,
@@ -382,23 +390,20 @@ export const streamPromptResponse = async ({
emitProgress({
id: `permission-${event.properties.id}`,
phase: "permission",
status: approvalMode === "always" ? "completed" : "running",
title: approvalMode === "always" ? "已自动允许权限请求" : "等待权限确认",
detail:
approvalMode === "always"
? "当前批准模式为始终允许,已自动允许本次权限请求。"
: buildPermissionDetail(event),
status: permissionApproval.autoApprove ? "completed" : "running",
title: permissionApproval.title,
detail: permissionApproval.detail ?? buildPermissionDetail(event),
});
if (approvalMode === "always") {
if (permissionApproval.autoApprove) {
await runtime.replyPermission({
requestId: event.properties.id,
sessionId,
reply: "always",
reply: "once",
});
write("permission_response", {
session_id: clientSessionId,
request_id: event.properties.id,
reply: "always" satisfies PermissionReply,
reply: "once" satisfies PermissionReply,
});
continue;
}
@@ -417,6 +422,10 @@ export const streamPromptResponse = async ({
if (isPermissionV2AskedEvent(event)) {
sawResponseActivity = true;
const permissionApproval = resolvePermissionApproval(
approvalMode,
event.properties.action,
);
logDevelopmentDebug("permission v2 request received", {
...debugContext,
requestId: event.properties.id,
@@ -427,23 +436,20 @@ export const streamPromptResponse = async ({
emitProgress({
id: `permission-${event.properties.id}`,
phase: "permission",
status: approvalMode === "always" ? "completed" : "running",
title: approvalMode === "always" ? "已自动允许权限请求" : "等待权限确认",
detail:
approvalMode === "always"
? "当前批准模式为始终允许,已自动允许本次权限请求。"
: buildPermissionV2Detail(event),
status: permissionApproval.autoApprove ? "completed" : "running",
title: permissionApproval.title,
detail: permissionApproval.detail ?? buildPermissionV2Detail(event),
});
if (approvalMode === "always") {
if (permissionApproval.autoApprove) {
await runtime.replyPermission({
requestId: event.properties.id,
sessionId,
reply: "always",
reply: "once",
});
write("permission_response", {
session_id: clientSessionId,
request_id: event.properties.id,
reply: "always" satisfies PermissionReply,
reply: "once" satisfies PermissionReply,
});
continue;
}