


























@@ -0,0 +1,233 @@
1+import * as readline from "node:readline";
2+import type { RequestPermissionRequest, RequestPermissionResponse } from "@agentclientprotocol/sdk";
3+import {
4+materializeWindowsSpawnProgram,
5+resolveWindowsSpawnProgram,
6+} from "../plugin-sdk/windows-spawn.js";
7+import {
8+listKnownProviderAuthEnvVarNames,
9+omitEnvKeysCaseInsensitive,
10+} from "../secrets/provider-env-vars.js";
11+import {
12+normalizeLowercaseStringOrEmpty,
13+normalizeOptionalString,
14+} from "../shared/string-coerce.js";
15+import { sanitizeTerminalText } from "../terminal/safe-text.js";
16+import { classifyAcpToolApproval, type AcpApprovalClass } from "./approval-classifier.js";
17+18+type PermissionOption = RequestPermissionRequest["options"][number];
19+20+type PermissionResolverDeps = {
21+prompt?: (toolName: string | undefined, toolTitle?: string) => Promise<boolean>;
22+log?: (line: string) => void;
23+cwd?: string;
24+};
25+26+function resolveToolKindForPermission(
27+toolName: string | undefined,
28+approvalClass: AcpApprovalClass,
29+): string | undefined {
30+if (!toolName && approvalClass === "unknown") {
31+return undefined;
32+}
33+if (approvalClass === "readonly_scoped") {
34+return "readonly_scoped";
35+}
36+if (approvalClass === "readonly_search") {
37+return "readonly_search";
38+}
39+return approvalClass;
40+}
41+42+function pickOption(
43+options: PermissionOption[],
44+kinds: PermissionOption["kind"][],
45+): PermissionOption | undefined {
46+for (const kind of kinds) {
47+const match = options.find((option) => option.kind === kind);
48+if (match) {
49+return match;
50+}
51+}
52+return undefined;
53+}
54+55+function selectedPermission(optionId: string): RequestPermissionResponse {
56+return { outcome: { outcome: "selected", optionId } };
57+}
58+59+function cancelledPermission(): RequestPermissionResponse {
60+return { outcome: { outcome: "cancelled" } };
61+}
62+63+function promptUserPermission(toolName: string | undefined, toolTitle?: string): Promise<boolean> {
64+if (!process.stdin.isTTY || !process.stderr.isTTY) {
65+console.error(`[permission denied] ${toolName ?? "unknown"}: non-interactive terminal`);
66+return Promise.resolve(false);
67+}
68+return new Promise((resolve) => {
69+let settled = false;
70+const rl = readline.createInterface({
71+input: process.stdin,
72+output: process.stderr,
73+});
74+75+const finish = (approved: boolean) => {
76+if (settled) {
77+return;
78+}
79+settled = true;
80+clearTimeout(timeout);
81+rl.close();
82+resolve(approved);
83+};
84+85+const timeout = setTimeout(() => {
86+console.error(`\n[permission timeout] denied: ${toolName ?? "unknown"}`);
87+finish(false);
88+}, 30_000);
89+90+const label = toolTitle
91+ ? toolName
92+ ? `${toolTitle} (${toolName})`
93+ : toolTitle
94+ : (toolName ?? "unknown tool");
95+rl.question(`\n[permission] Allow "${label}"? (y/N) `, (answer) => {
96+const approved = normalizeLowercaseStringOrEmpty(answer) === "y";
97+console.error(`[permission ${approved ? "approved" : "denied"}] ${toolName ?? "unknown"}`);
98+finish(approved);
99+});
100+});
101+}
102+103+export async function resolvePermissionRequest(
104+params: RequestPermissionRequest,
105+deps: PermissionResolverDeps = {},
106+): Promise<RequestPermissionResponse> {
107+const log = deps.log ?? ((line: string) => console.error(line));
108+const prompt = deps.prompt ?? promptUserPermission;
109+const cwd = deps.cwd ?? process.cwd();
110+const options = params.options ?? [];
111+const toolTitle = sanitizeTerminalText(params.toolCall?.title ?? "tool");
112+const classification = classifyAcpToolApproval({ toolCall: params.toolCall, cwd });
113+const toolName = classification.toolName;
114+const toolKind = resolveToolKindForPermission(toolName, classification.approvalClass);
115+116+if (options.length === 0) {
117+log(`[permission cancelled] ${toolName ?? "unknown"}: no options available`);
118+return cancelledPermission();
119+}
120+121+const allowOption = pickOption(options, ["allow_once", "allow_always"]);
122+const rejectOption = pickOption(options, ["reject_once", "reject_always"]);
123+const promptRequired = !classification.autoApprove;
124+125+if (!promptRequired) {
126+const option = allowOption ?? options[0];
127+if (!option) {
128+log(`[permission cancelled] ${toolName}: no selectable options`);
129+return cancelledPermission();
130+}
131+log(`[permission auto-approved] ${toolName} (${toolKind ?? "unknown"})`);
132+return selectedPermission(option.optionId);
133+}
134+135+log(
136+`\n[permission requested] ${toolTitle}${toolName ? ` (${toolName})` : ""}${toolKind ? ` [${toolKind}]` : ""}`,
137+);
138+const approved = await prompt(toolName, toolTitle);
139+140+if (approved && allowOption) {
141+return selectedPermission(allowOption.optionId);
142+}
143+if (!approved && rejectOption) {
144+return selectedPermission(rejectOption.optionId);
145+}
146+147+log(
148+`[permission cancelled] ${toolName ?? "unknown"}: missing ${approved ? "allow" : "reject"} option`,
149+);
150+return cancelledPermission();
151+}
152+153+type AcpClientSpawnEnvOptions = {
154+stripKeys?: Iterable<string>;
155+};
156+157+export function resolveAcpClientSpawnEnv(
158+baseEnv: NodeJS.ProcessEnv = process.env,
159+options: AcpClientSpawnEnvOptions = {},
160+): NodeJS.ProcessEnv {
161+const env = omitEnvKeysCaseInsensitive(baseEnv, options.stripKeys ?? []);
162+env.OPENCLAW_SHELL = "acp-client";
163+return env;
164+}
165+166+export function shouldStripProviderAuthEnvVarsForAcpServer(
167+params: {
168+serverCommand?: string;
169+serverArgs?: string[];
170+defaultServerCommand?: string;
171+defaultServerArgs?: string[];
172+} = {},
173+): boolean {
174+const serverCommand = normalizeOptionalString(params.serverCommand);
175+if (!serverCommand) {
176+return true;
177+}
178+const defaultServerCommand = normalizeOptionalString(params.defaultServerCommand);
179+if (!defaultServerCommand || serverCommand !== defaultServerCommand) {
180+return false;
181+}
182+const serverArgs = params.serverArgs ?? [];
183+const defaultServerArgs = params.defaultServerArgs ?? [];
184+return (
185+serverArgs.length === defaultServerArgs.length &&
186+serverArgs.every((arg, index) => arg === defaultServerArgs[index])
187+);
188+}
189+190+export function buildAcpClientStripKeys(params: {
191+stripProviderAuthEnvVars?: boolean;
192+activeSkillEnvKeys?: Iterable<string>;
193+}): Set<string> {
194+const stripKeys = new Set<string>(params.activeSkillEnvKeys ?? []);
195+if (params.stripProviderAuthEnvVars) {
196+for (const key of listKnownProviderAuthEnvVarNames()) {
197+stripKeys.add(key);
198+}
199+}
200+return stripKeys;
201+}
202+203+type AcpSpawnRuntime = {
204+platform: NodeJS.Platform;
205+env: NodeJS.ProcessEnv;
206+execPath: string;
207+};
208+209+const DEFAULT_ACP_SPAWN_RUNTIME: AcpSpawnRuntime = {
210+platform: process.platform,
211+env: process.env,
212+execPath: process.execPath,
213+};
214+215+export function resolveAcpClientSpawnInvocation(
216+params: { serverCommand: string; serverArgs: string[] },
217+runtime: AcpSpawnRuntime = DEFAULT_ACP_SPAWN_RUNTIME,
218+): { command: string; args: string[]; shell?: boolean; windowsHide?: boolean } {
219+const program = resolveWindowsSpawnProgram({
220+command: params.serverCommand,
221+platform: runtime.platform,
222+env: runtime.env,
223+execPath: runtime.execPath,
224+packageName: "openclaw",
225+});
226+const resolved = materializeWindowsSpawnProgram(program, params.serverArgs);
227+return {
228+command: resolved.command,
229+args: resolved.argv,
230+shell: resolved.shell,
231+windowsHide: resolved.windowsHide,
232+};
233+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。