Exec approvals: soften effective-policy triage regressions

This commit is contained in:
Gustavo Madeira Santana
2026-04-01 19:50:34 -04:00
parent 0ccdf73508
commit f9dbb236b6
8 changed files with 60 additions and 11 deletions

View File

@@ -368,11 +368,13 @@ export function buildApprovalPendingMessage(params: {
lines.push(
allowedDecisions.includes("allow-always")
? "Background mode requires pre-approved policy (allow-always or ask=off)."
: "Background mode requires host policy that allows pre-approval (for example ask=off).",
: "Background mode requires an effective policy that allows pre-approval (for example ask=off).",
);
lines.push(`Reply with: /approve ${params.approvalSlug} ${decisionText}`);
if (!allowedDecisions.includes("allow-always")) {
lines.push("Host policy requires approval every time, so Allow Always is unavailable.");
lines.push(
"The effective approval policy requires approval every time, so Allow Always is unavailable.",
);
}
lines.push("If the short code is ambiguous, use the full id in /approve.");
return lines.join("\n");

View File

@@ -251,6 +251,38 @@ describe("exec approvals CLI", () => {
);
});
it("keeps gateway approvals output when config.get fails", async () => {
callGatewayFromCli.mockImplementation(
async (method: string, _opts: unknown, params?: unknown) => {
if (method === "config.get") {
throw new Error("gateway config unavailable");
}
if (method === "exec.approvals.get") {
return {
path: "/tmp/exec-approvals.json",
exists: true,
hash: "hash-1",
file: { version: 1, agents: {} },
};
}
return { method, params };
},
);
await runApprovalsCommand(["approvals", "get", "--gateway", "--json"]);
expect(defaultRuntime.writeJson).toHaveBeenCalledWith(
expect.objectContaining({
effectivePolicy: {
note: "Config unavailable.",
scopes: [],
},
}),
0,
);
expect(runtimeErrors).toHaveLength(0);
});
it("reports agent scopes with inherited global requested policy", async () => {
localSnapshot.file = {
version: 1,

View File

@@ -168,8 +168,16 @@ async function loadConfigForApprovalsTarget(params: {
if (params.source === "local") {
return await readBestEffortConfig();
}
const snapshot = (await callGatewayFromCli("config.get", params.opts, {})) as ConfigSnapshotLike;
return snapshot.config && typeof snapshot.config === "object" ? snapshot.config : null;
try {
const snapshot = (await callGatewayFromCli(
"config.get",
params.opts,
{},
)) as ConfigSnapshotLike;
return snapshot.config && typeof snapshot.config === "object" ? snapshot.config : null;
} catch {
return null;
}
}
function collectExecPolicySnapshots(params: { cfg: OpenClawConfig; approvals: ExecApprovalsFile }) {

View File

@@ -337,7 +337,7 @@ export function createExecApprovalHandlers(
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
"allow-always is unavailable because host policy requires approval every time",
"allow-always is unavailable because the effective policy requires approval every time",
),
);
return;

View File

@@ -597,7 +597,8 @@ describe("exec approval handlers", () => {
false,
undefined,
expect.objectContaining({
message: "allow-always is unavailable because host policy requires approval every time",
message:
"allow-always is unavailable because the effective policy requires approval every time",
}),
);

View File

@@ -238,11 +238,13 @@ function buildRequestMessage(request: ExecApprovalRequest, nowMs: number) {
lines.push(
allowedDecisions.includes("allow-always")
? "Background mode note: non-interactive runs cannot wait for chat approvals; use pre-approved policy (allow-always or ask=off)."
: "Background mode note: non-interactive runs cannot wait for chat approvals; host policy still requires per-run approval unless ask=off.",
: "Background mode note: non-interactive runs cannot wait for chat approvals; the effective policy still requires per-run approval unless ask=off.",
);
lines.push(`Reply with: /approve <id> ${decisionText}`);
if (!allowedDecisions.includes("allow-always")) {
lines.push("Allow Always is unavailable because host policy requires approval every time.");
lines.push(
"Allow Always is unavailable because the effective policy requires approval every time.",
);
}
return lines.join("\n");
}

View File

@@ -138,7 +138,7 @@ describe("exec approval reply helpers", () => {
expect(payload.text).toContain("Full id: `req-1`");
});
it("omits allow-always actions when host policy requires approval every time", () => {
it("omits allow-always actions when the effective policy requires approval every time", () => {
const payload = buildExecApprovalPendingReplyPayload({
approvalId: "req-ask-always",
approvalSlug: "slug-always",
@@ -156,7 +156,9 @@ describe("exec approval reply helpers", () => {
});
expect(payload.text).toContain("```txt\n/approve slug-always allow-once\n```");
expect(payload.text).not.toContain("allow-always");
expect(payload.text).toContain("Allow Always is unavailable");
expect(payload.text).toContain(
"The effective approval policy requires approval every time, so Allow Always is unavailable.",
);
expect(payload.interactive).toEqual({
blocks: [
{

View File

@@ -267,7 +267,9 @@ export function buildExecApprovalPendingReplyPayload(
lines.push(secondaryFence);
}
if (!allowedDecisions.includes("allow-always")) {
lines.push("Host policy requires approval every time, so Allow Always is unavailable.");
lines.push(
"The effective approval policy requires approval every time, so Allow Always is unavailable.",
);
}
const info: string[] = [];
info.push(`Host: ${params.host}`);