






















@@ -0,0 +1,166 @@
1+import type {
2+PermissionOption,
3+RequestPermissionRequest,
4+RequestPermissionResponse,
5+} from "@agentclientprotocol/sdk";
6+7+export type GatewayExecApprovalDecision = "allow-once" | "allow-always" | "deny";
8+9+export type GatewayExecApprovalEvent = {
10+approvalId: string;
11+command?: string;
12+host?: string;
13+title?: string;
14+toolCallId?: string;
15+};
16+17+export type GatewayExecApprovalDetails = {
18+allowedDecisions?: unknown;
19+commandPreview?: unknown;
20+commandText?: unknown;
21+host?: unknown;
22+};
23+24+const FALLBACK_EXEC_APPROVAL_DECISIONS = ["allow-once", "deny"] as const;
25+26+function readNonEmptyString(value: unknown): string | undefined {
27+return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
28+}
29+30+function normalizeGatewayExecApprovalDecision(
31+value: unknown,
32+): GatewayExecApprovalDecision | undefined {
33+if (value === "allow-once" || value === "allow-always" || value === "deny") {
34+return value;
35+}
36+return undefined;
37+}
38+39+export function normalizeGatewayExecApprovalDecisions(
40+value: unknown,
41+): GatewayExecApprovalDecision[] {
42+const normalized = Array.isArray(value)
43+ ? value
44+.map(normalizeGatewayExecApprovalDecision)
45+.filter((decision): decision is GatewayExecApprovalDecision => Boolean(decision))
46+ : [];
47+return normalized.length > 0 ? normalized : [...FALLBACK_EXEC_APPROVAL_DECISIONS];
48+}
49+50+export function buildAcpPermissionOptions(
51+decisions: readonly GatewayExecApprovalDecision[],
52+): PermissionOption[] {
53+const unique = new Set<GatewayExecApprovalDecision>(decisions);
54+const options: PermissionOption[] = [];
55+if (unique.has("allow-once")) {
56+options.push({
57+optionId: "allow-once",
58+name: "Allow once",
59+kind: "allow_once",
60+});
61+}
62+if (unique.has("allow-always")) {
63+options.push({
64+optionId: "allow-always",
65+name: "Allow always",
66+kind: "allow_always",
67+});
68+}
69+if (unique.has("deny")) {
70+options.push({
71+optionId: "deny",
72+name: "Deny",
73+kind: "reject_once",
74+});
75+}
76+return options.length > 0 ? options : buildAcpPermissionOptions(FALLBACK_EXEC_APPROVAL_DECISIONS);
77+}
78+79+export function parseGatewayExecApprovalEventData(
80+data: Record<string, unknown>,
81+): GatewayExecApprovalEvent | null {
82+if (data.phase !== "requested" || data.kind !== "exec" || data.status !== "pending") {
83+return null;
84+}
85+const approvalId = readNonEmptyString(data.approvalId);
86+if (!approvalId) {
87+return null;
88+}
89+return {
90+ approvalId,
91+command: readNonEmptyString(data.command),
92+host: readNonEmptyString(data.host),
93+title: readNonEmptyString(data.title),
94+toolCallId: readNonEmptyString(data.toolCallId),
95+};
96+}
97+98+export function parseGatewayExecApprovalRequestEventPayload(
99+payload: Record<string, unknown>,
100+): GatewayExecApprovalEvent | null {
101+const approvalId = readNonEmptyString(payload.id);
102+const request = payload.request;
103+if (!approvalId || !request || typeof request !== "object" || Array.isArray(request)) {
104+return null;
105+}
106+const requestRecord = request as Record<string, unknown>;
107+return {
108+ approvalId,
109+command:
110+readNonEmptyString(requestRecord.command) ?? readNonEmptyString(requestRecord.commandPreview),
111+host: readNonEmptyString(requestRecord.host),
112+};
113+}
114+115+export function buildAcpPermissionRequest(params: {
116+sessionId: string;
117+event: GatewayExecApprovalEvent;
118+details?: GatewayExecApprovalDetails | null;
119+}): RequestPermissionRequest {
120+const command =
121+readNonEmptyString(params.details?.commandText) ??
122+readNonEmptyString(params.details?.commandPreview) ??
123+params.event.command;
124+const host = readNonEmptyString(params.details?.host) ?? params.event.host;
125+const decisions = normalizeGatewayExecApprovalDecisions(params.details?.allowedDecisions);
126+const rawInput: Record<string, string> = {
127+name: "exec",
128+approvalId: params.event.approvalId,
129+};
130+if (command) {
131+rawInput.command = command;
132+}
133+if (host) {
134+rawInput.host = host;
135+}
136+137+return {
138+sessionId: params.sessionId,
139+toolCall: {
140+// Raw approval events can arrive before Gateway emits a tool call id; the
141+// approval id remains the stable correlation key for those early prompts.
142+toolCallId: params.event.toolCallId ?? `exec:${params.event.approvalId}`,
143+title: params.event.title ?? "Command approval requested",
144+kind: "execute",
145+status: "pending",
146+ rawInput,
147+_meta: {
148+toolName: "exec",
149+approvalId: params.event.approvalId,
150+},
151+},
152+options: buildAcpPermissionOptions(decisions),
153+};
154+}
155+156+export function resolveGatewayDecisionFromPermissionOutcome(
157+response: RequestPermissionResponse | undefined,
158+options: readonly PermissionOption[],
159+): GatewayExecApprovalDecision | undefined {
160+const outcome = response?.outcome;
161+if (!outcome || outcome.outcome !== "selected") {
162+return undefined;
163+}
164+const selected = options.find((option) => option.optionId === outcome.optionId);
165+return normalizeGatewayExecApprovalDecision(selected?.optionId);
166+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。