mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
fix(codex): explain timed-out MCP approvals (#125860)
This commit is contained in:
committed by
GitHub
parent
8131f56cb1
commit
1ed682f883
@@ -356,6 +356,28 @@ describe("Codex app-server elicitation bridge", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("declines timed-out MCP approvals with explanatory metadata", async () => {
|
||||
mockCallGatewayTool
|
||||
.mockResolvedValueOnce({ id: "plugin:approval-timeout", status: "accepted" })
|
||||
.mockResolvedValueOnce({
|
||||
id: "plugin:approval-timeout",
|
||||
decision: "deny",
|
||||
terminalReason: "timeout",
|
||||
});
|
||||
|
||||
const result = await handleCodexAppServerElicitationRequest({
|
||||
requestParams: buildApprovalElicitation(),
|
||||
paramsForRun: createParams(),
|
||||
...codexTestTurnIds(),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
action: "decline",
|
||||
content: null,
|
||||
_meta: { message: "Approval timed out before an operator responded." },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat inherited request-time MCP decisions as final", async () => {
|
||||
const inheritedDecisionResult = Object.assign(Object.create({ decision: null }), {
|
||||
id: "plugin:approval-inherited",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { formatCodexDisplayText } from "../command-formatters.js";
|
||||
import {
|
||||
approvalRequestExplicitlyUnavailable,
|
||||
codexApprovalTimeoutText,
|
||||
mapExecDecisionToOutcome,
|
||||
requestPluginApproval,
|
||||
sanitizeCodexApprovalVisibleText,
|
||||
@@ -37,6 +38,8 @@ type BridgeableApprovalElicitation = {
|
||||
allowedDecisions?: ExecApprovalDecision[];
|
||||
};
|
||||
|
||||
type ElicitationApprovalOutcome = AppServerApprovalOutcome | "timed-out";
|
||||
|
||||
type PluginElicitationResolution =
|
||||
| { kind: "not_plugin" }
|
||||
| { kind: "matched"; entry: CodexAppPolicyContextEntry }
|
||||
@@ -309,7 +312,7 @@ async function buildPluginPolicyElicitationResponse(params: {
|
||||
});
|
||||
return buildElicitationResponse(
|
||||
approvalPrompt,
|
||||
oneShotPluginPolicyApprovalOutcome(mode, outcome),
|
||||
mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome,
|
||||
);
|
||||
}
|
||||
logPluginElicitationDecline("unmappable_schema", params.requestParams);
|
||||
@@ -333,13 +336,6 @@ function allowedPluginPolicyApprovalDecisions(
|
||||
return allowedDecisions.filter((decision) => decision !== "allow-always");
|
||||
}
|
||||
|
||||
function oneShotPluginPolicyApprovalOutcome(
|
||||
mode: "allow" | "deny" | "auto" | "ask",
|
||||
outcome: AppServerApprovalOutcome,
|
||||
): AppServerApprovalOutcome {
|
||||
return mode === "ask" && outcome === "approved-session" ? "approved-once" : outcome;
|
||||
}
|
||||
|
||||
function readPluginApprovalElicitation(
|
||||
entry: CodexAppPolicyContextEntry,
|
||||
requestParams: JsonObject,
|
||||
@@ -408,8 +404,8 @@ function canMapPersistentApproval(requestedSchema: JsonObject, meta: JsonObject)
|
||||
});
|
||||
}
|
||||
|
||||
function declineElicitationResponse(): JsonValue {
|
||||
return { action: "decline", content: null, _meta: null };
|
||||
function declineElicitationResponse(message?: string): JsonValue {
|
||||
return { action: "decline", content: null, _meta: message ? { message } : null };
|
||||
}
|
||||
|
||||
function logPluginElicitationDecline(reason: string, requestParams: JsonObject | undefined): void {
|
||||
@@ -655,7 +651,7 @@ async function requestPluginApprovalOutcome(params: {
|
||||
description: string;
|
||||
allowedDecisions?: ExecApprovalDecision[];
|
||||
signal?: AbortSignal;
|
||||
}): Promise<AppServerApprovalOutcome> {
|
||||
}): Promise<ElicitationApprovalOutcome> {
|
||||
try {
|
||||
const requestResult = await requestPluginApproval({
|
||||
hostCapabilities: params.paramsForRun.hostCapabilities,
|
||||
@@ -678,6 +674,12 @@ async function requestPluginApprovalOutcome(params: {
|
||||
approvalId,
|
||||
signal: params.signal,
|
||||
});
|
||||
if (params.signal?.aborted) {
|
||||
return "cancelled";
|
||||
}
|
||||
if (approvalResult?.terminalReason === "timeout") {
|
||||
return "timed-out";
|
||||
}
|
||||
return mapExecDecisionToOutcome(approvalResult?.decision);
|
||||
} catch {
|
||||
return params.signal?.aborted ? "cancelled" : "denied";
|
||||
@@ -689,14 +691,17 @@ function buildElicitationResponse(
|
||||
BridgeableApprovalElicitation,
|
||||
"requestedSchema" | "meta" | "persistHintsMode"
|
||||
>,
|
||||
outcome: AppServerApprovalOutcome,
|
||||
outcome: ElicitationApprovalOutcome,
|
||||
): JsonValue {
|
||||
const { requestedSchema, meta } = approvalPrompt;
|
||||
if (outcome === "cancelled") {
|
||||
return { action: "cancel", content: null, _meta: null };
|
||||
}
|
||||
if (outcome === "timed-out") {
|
||||
return declineElicitationResponse(codexApprovalTimeoutText("other"));
|
||||
}
|
||||
if (outcome === "denied" || outcome === "unavailable") {
|
||||
return { action: "decline", content: null, _meta: null };
|
||||
return declineElicitationResponse();
|
||||
}
|
||||
|
||||
const content = buildAcceptedContent(approvalPrompt, outcome);
|
||||
|
||||
Reference in New Issue
Block a user