Skip to content

Commit d89258d

Browse files
committed
fix: approval prompt misleading text for one-shot commands
- Add unavailableReasons field to ExecApprovalRequestParams schema - Pass unavailableReasons through gateway server-methods - Parse unavailableReasons in UI app and component layers - Display reason-specific messaging for no-reusable-pattern case - Add i18n string allowAlwaysUnavailableOneShot Fixes issue #97069 where commands with shell redirection (e.g., openclaw --version 2>&1) showed misleading text claiming 'effective policy requires approval every time' when the real reason was the command being non-persistable.
1 parent 30b3a7c commit d89258d

6 files changed

Lines changed: 45 additions & 6 deletions

File tree

packages/gateway-protocol/src/schema/exec-approvals.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,17 @@ export const ExecApprovalRequestParamsSchema = Type.Object(
267267
maxItems: 1,
268268
}),
269269
),
270+
unavailableReasons: Type.Optional(
271+
Type.Array(
272+
Type.String({
273+
enum: ["no-reusable-pattern", "prompt-only", "runtime-payload", "unplanned"],
274+
}),
275+
{
276+
description:
277+
"Reasons why allow-always is unavailable. 'no-reusable-pattern' indicates shell redirection or runtime payloads prevent safe persistence.",
278+
},
279+
),
280+
),
270281
commandSpans: Type.Optional(
271282
Type.Array(
272283
Type.Object(

src/gateway/server-methods/exec-approval.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ export function createExecApprovalHandlers(
171171
ask?: string;
172172
warningText?: string | null;
173173
unavailableDecisions?: string[];
174+
unavailableReasons?: ("no-reusable-pattern" | "prompt-only" | "runtime-payload" | "unplanned")[];
174175
commandSpans?: {
175176
startIndex: number;
176177
endIndex: number;
@@ -324,6 +325,7 @@ export function createExecApprovalHandlers(
324325
commandAnalysis,
325326
commandSpans,
326327
unavailableDecisions: unavailableDecisions.length > 0 ? unavailableDecisions : undefined,
328+
unavailableReasons: p.unavailableReasons?.length ? p.unavailableReasons : undefined,
327329
allowedDecisions: resolveExecApprovalRequestAllowedDecisions({
328330
ask: p.ask ?? null,
329331
unavailableDecisions,

src/infra/exec-approval-reply.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type ExecApprovalPendingReplyParams = {
5454
ask?: string | null;
5555
agentId?: string | null;
5656
allowedDecisions?: readonly ExecApprovalReplyDecision[];
57+
unavailableReasons?: readonly ("no-reusable-pattern" | "prompt-only" | "runtime-payload" | "unplanned")[];
5758
command: string;
5859
cwd?: string;
5960
host: ExecHost;
@@ -371,9 +372,17 @@ export function buildExecApprovalPendingReplyPayload(
371372
lines.push(secondaryFence);
372373
}
373374
if (!allowedDecisions.includes("allow-always")) {
374-
lines.push(
375-
"The effective approval policy requires approval every time, so Allow Always is unavailable.",
376-
);
375+
// Check if we have unavailable reasons to provide more specific messaging
376+
const isOneShot = params.unavailableReasons?.includes("no-reusable-pattern");
377+
if (isOneShot) {
378+
lines.push(
379+
"Allow Always is unavailable for commands with shell redirection or runtime payloads that cannot be safely persisted.",
380+
);
381+
} else {
382+
lines.push(
383+
"The effective approval policy requires approval every time, so Allow Always is unavailable.",
384+
);
385+
}
377386
}
378387
const info: string[] = [];
379388
info.push(`Host: ${params.host}`);

ui/src/app/exec-approval.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type ExecApprovalRequestPayload = {
1515
endIndex: number;
1616
}[];
1717
allowedDecisions?: readonly ExecApprovalDecision[];
18+
unavailableReasons?: readonly ("no-reusable-pattern" | "prompt-only" | "runtime-payload" | "unplanned")[];
1819
};
1920

2021
export type ExecApprovalDecision = "allow-once" | "allow-always" | "deny";
@@ -137,6 +138,13 @@ export function parseExecApprovalRequested(payload: unknown): ExecApprovalReques
137138
sessionKey: typeof request.sessionKey === "string" ? request.sessionKey : null,
138139
commandSpans: parseCommandSpans(request.commandSpans, command.length),
139140
allowedDecisions: parseAllowedDecisions(request.allowedDecisions),
141+
unavailableReasons: Array.isArray(request.unavailableReasons)
142+
? request.unavailableReasons.filter(
143+
(r): r is "no-reusable-pattern" | "prompt-only" | "runtime-payload" | "unplanned" =>
144+
typeof r === "string" &&
145+
["no-reusable-pattern", "prompt-only", "runtime-payload", "unplanned"].includes(r),
146+
)
147+
: undefined,
140148
},
141149
createdAtMs,
142150
expiresAtMs,

ui/src/components/exec-approval.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,16 @@ function renderUnavailableDecisionWarning(
160160
active: ExecApprovalRequest,
161161
decisions: readonly ExecApprovalDecision[],
162162
) {
163-
return active.kind !== "exec" || decisions.includes("allow-always")
164-
? nothing
165-
: html`<div class="exec-approval-warning">${t("execApproval.allowAlwaysUnavailable")}</div>`;
163+
if (active.kind !== "exec" || decisions.includes("allow-always")) {
164+
return nothing;
165+
}
166+
// Check if we have unavailable reasons to provide more specific messaging
167+
const unavailableReasons = active.request.unavailableReasons;
168+
const isOneShot = unavailableReasons?.includes("no-reusable-pattern");
169+
const messageKey = isOneShot
170+
? "allowAlwaysUnavailableOneShot"
171+
: "allowAlwaysUnavailable";
172+
return html`<div class="exec-approval-warning">${t("execApproval." + messageKey)}</div>`;
166173
}
167174

168175
function renderExecApprovalPrompt(props: ExecApprovalProps) {

ui/src/i18n/locales/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,8 @@ export const en: TranslationMap = {
445445
alwaysAllow: "Always allow",
446446
allowAlwaysUnavailable:
447447
"The effective approval policy requires approval every time, so Allow Always is unavailable.",
448+
allowAlwaysUnavailableOneShot:
449+
"Allow Always is unavailable for commands with shell redirection or runtime payloads that cannot be safely persisted.",
448450
deny: "Deny",
449451
labels: {
450452
host: "Host",

0 commit comments

Comments
 (0)