fix: make elevated full truthful

This commit is contained in:
Eva
2026-04-10 20:51:13 +07:00
committed by Peter Steinberger
parent 551b6a61e6
commit aed57c95ec
8 changed files with 200 additions and 13 deletions

View File

@@ -2,6 +2,7 @@ import type { ExecApprovalDecision } from "../infra/exec-approvals.js";
import type { ExecAsk, ExecHost, ExecSecurity, ExecTarget } from "../infra/exec-approvals.js";
import type { SafeBinProfileFixture } from "../infra/exec-safe-bin-policy.js";
import type { BashSandboxConfig } from "./bash-tools.shared.js";
import type { EmbeddedFullAccessBlockedReason } from "./pi-embedded-runner/types.js";
export type ExecToolDefaults = {
hasCronTool?: boolean;
@@ -37,6 +38,8 @@ export type ExecElevatedDefaults = {
enabled: boolean;
allowed: boolean;
defaultLevel: "on" | "off" | "ask" | "full";
fullAccessAvailable?: boolean;
fullAccessBlockedReason?: EmbeddedFullAccessBlockedReason;
};
export type ExecToolDetails =

View File

@@ -77,7 +77,40 @@ describe("buildEmbeddedSandboxInfo", () => {
workspaceAccess: "none",
agentWorkspaceMount: undefined,
hostBrowserAllowed: false,
elevated: { allowed: true, defaultLevel: "on" },
elevated: {
allowed: true,
defaultLevel: "on",
fullAccessAvailable: true,
},
});
});
it("keeps full-access unavailability truth when provided", () => {
const sandbox = createSandboxContext();
expect(
buildEmbeddedSandboxInfo(sandbox, {
enabled: true,
allowed: true,
defaultLevel: "full",
fullAccessAvailable: false,
fullAccessBlockedReason: "runtime",
}),
).toEqual({
enabled: true,
workspaceDir: "/tmp/openclaw-sandbox",
containerWorkspaceDir: "/workspace",
workspaceAccess: "none",
agentWorkspaceMount: undefined,
browserBridgeUrl: "http://localhost:9222",
browserNoVncUrl: "http://localhost:6080",
hostBrowserAllowed: true,
elevated: {
allowed: true,
defaultLevel: "full",
fullAccessAvailable: false,
fullAccessBlockedReason: "runtime",
},
});
});
});

View File

@@ -1,6 +1,34 @@
import type { ExecElevatedDefaults } from "../bash-tools.js";
import type { resolveSandboxContext } from "../sandbox.js";
import type { EmbeddedSandboxInfo } from "./types.js";
import type { EmbeddedFullAccessBlockedReason, EmbeddedSandboxInfo } from "./types.js";
export function resolveEmbeddedFullAccessState(params: {
sandboxEnabled: boolean;
execElevated?: ExecElevatedDefaults;
}): { available: boolean; blockedReason?: EmbeddedFullAccessBlockedReason } {
if (!params.sandboxEnabled) {
return {
available: false,
blockedReason: "runtime",
};
}
if (params.execElevated?.fullAccessAvailable === true) {
return { available: true };
}
if (params.execElevated?.fullAccessAvailable === false) {
return {
available: false,
blockedReason: params.execElevated.fullAccessBlockedReason ?? "host-policy",
};
}
if (!params.execElevated?.enabled || !params.execElevated.allowed) {
return {
available: false,
blockedReason: "host-policy",
};
}
return { available: true };
}
export function buildEmbeddedSandboxInfo(
sandbox?: Awaited<ReturnType<typeof resolveSandboxContext>>,
@@ -9,7 +37,12 @@ export function buildEmbeddedSandboxInfo(
if (!sandbox?.enabled) {
return undefined;
}
const elevatedConfigured = execElevated?.enabled === true;
const elevatedAllowed = Boolean(execElevated?.enabled && execElevated.allowed);
const fullAccess = resolveEmbeddedFullAccessState({
sandboxEnabled: true,
execElevated,
});
return {
enabled: true,
workspaceDir: sandbox.workspaceDir,
@@ -18,11 +51,15 @@ export function buildEmbeddedSandboxInfo(
agentWorkspaceMount: sandbox.workspaceAccess === "ro" ? "/agent" : undefined,
browserBridgeUrl: sandbox.browser?.bridgeUrl,
hostBrowserAllowed: sandbox.browserAllowHostControl,
...(elevatedAllowed
...(elevatedConfigured
? {
elevated: {
allowed: true,
allowed: elevatedAllowed,
defaultLevel: execElevated?.defaultLevel ?? "off",
fullAccessAvailable: fullAccess.available,
...(fullAccess.blockedReason
? { fullAccessBlockedReason: fullAccess.blockedReason }
: {}),
},
}
: {}),

View File

@@ -93,6 +93,8 @@ export type EmbeddedPiCompactResult = {
};
};
export type EmbeddedFullAccessBlockedReason = "sandbox" | "host-policy" | "channel" | "runtime";
export type EmbeddedSandboxInfo = {
enabled: boolean;
workspaceDir?: string;
@@ -104,5 +106,7 @@ export type EmbeddedSandboxInfo = {
elevated?: {
allowed: boolean;
defaultLevel: "on" | "off" | "ask" | "full";
fullAccessAvailable: boolean;
fullAccessBlockedReason?: EmbeddedFullAccessBlockedReason;
};
};

View File

@@ -939,7 +939,7 @@ describe("buildAgentSystemPrompt", () => {
containerWorkspaceDir: "/workspace",
workspaceAccess: "ro",
agentWorkspaceMount: "/agent",
elevated: { allowed: true, defaultLevel: "on" },
elevated: { allowed: true, defaultLevel: "on", fullAccessAvailable: true },
},
});
@@ -957,6 +957,35 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("Current elevated level: on");
});
it("does not advertise /elevated full when auto-approved full access is unavailable", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
sandboxInfo: {
enabled: true,
workspaceDir: "/tmp/sandbox",
containerWorkspaceDir: "/workspace",
workspaceAccess: "ro",
agentWorkspaceMount: "/agent",
elevated: {
allowed: true,
defaultLevel: "full",
fullAccessAvailable: false,
fullAccessBlockedReason: "runtime",
},
},
});
expect(prompt).toContain("Elevated exec is available for this session.");
expect(prompt).toContain("User can toggle with /elevated on|off|ask.");
expect(prompt).not.toContain("User can toggle with /elevated on|off|ask|full.");
expect(prompt).toContain(
"Auto-approved /elevated full is unavailable here (runtime constraints).",
);
expect(prompt).toContain(
"Current elevated level: full (full auto-approval unavailable here; use ask/on instead).",
);
});
it("includes reaction guidance when provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",

View File

@@ -12,7 +12,10 @@ import {
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
import type { ResolvedTimeFormat } from "./date-time.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
import type { EmbeddedSandboxInfo } from "./pi-embedded-runner/types.js";
import type {
EmbeddedFullAccessBlockedReason,
EmbeddedSandboxInfo,
} from "./pi-embedded-runner/types.js";
import {
normalizePromptCapabilityIds,
normalizeStructuredPromptSection,
@@ -336,6 +339,19 @@ function buildExecApprovalPromptGuidance(params: {
return "When exec returns approval-pending, include the concrete /approve command from tool output as plain chat text for the user, and do not ask for a different or rotated code.";
}
function formatFullAccessBlockedReason(reason?: EmbeddedFullAccessBlockedReason): string {
if (reason === "host-policy") {
return "host policy";
}
if (reason === "channel") {
return "channel constraints";
}
if (reason === "sandbox") {
return "sandbox constraints";
}
return "runtime constraints";
}
export function buildAgentSystemPrompt(params: {
workspaceDir: string;
defaultThinkLevel?: ThinkLevel;
@@ -470,6 +486,11 @@ export function buildAgentSystemPrompt(params: {
const sanitizedSandboxContainerWorkspace = sandboxContainerWorkspace
? sanitizeForPromptLiteral(sandboxContainerWorkspace)
: "";
const elevated = params.sandboxInfo?.elevated;
const fullAccessBlockedReasonLabel =
elevated?.fullAccessAvailable === false
? formatFullAccessBlockedReason(elevated.fullAccessBlockedReason)
: undefined;
const displayWorkspaceDir =
params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
? sanitizedSandboxContainerWorkspace
@@ -652,17 +673,35 @@ export function buildAgentSystemPrompt(params: {
: params.sandboxInfo.hostBrowserAllowed === false
? "Host browser control: blocked."
: "",
params.sandboxInfo.elevated?.allowed
elevated?.allowed
? "Elevated exec is available for this session."
: "",
params.sandboxInfo.elevated?.allowed
: elevated
? "Elevated exec is unavailable for this session."
: "",
elevated?.allowed && elevated.fullAccessAvailable
? "User can toggle with /elevated on|off|ask|full."
: "",
params.sandboxInfo.elevated?.allowed
elevated?.allowed && !elevated.fullAccessAvailable
? "User can toggle with /elevated on|off|ask."
: "",
elevated?.allowed && elevated.fullAccessAvailable
? "You may also send /elevated on|off|ask|full when needed."
: "",
params.sandboxInfo.elevated?.allowed
? `Current elevated level: ${params.sandboxInfo.elevated.defaultLevel} (ask runs exec on host with approvals; full auto-approves).`
elevated?.allowed && !elevated.fullAccessAvailable
? "You may also send /elevated on|off|ask when needed."
: "",
elevated?.fullAccessAvailable === false
? `Auto-approved /elevated full is unavailable here (${fullAccessBlockedReasonLabel}).`
: "",
elevated?.allowed && elevated.fullAccessAvailable
? `Current elevated level: ${elevated.defaultLevel} (ask runs exec on host with approvals; full auto-approves).`
: elevated?.allowed
? `Current elevated level: ${elevated.defaultLevel} (full auto-approval unavailable here; use ask/on instead).`
: elevated
? "Current elevated level: off (elevated exec unavailable)."
: "",
elevated && !elevated.allowed
? "Do not tell the user to switch to /elevated full in this session."
: "",
]
.filter(Boolean)

View File

@@ -38,4 +38,17 @@ describe("buildExecOverridePromptHint", () => {
);
expect(result).toContain("Current elevated level: full.");
});
it("warns when auto-approved full access is unavailable", () => {
const result = buildExecOverridePromptHint({
elevatedLevel: "full",
fullAccessAvailable: false,
fullAccessBlockedReason: "runtime",
});
expect(result).toContain("Current elevated level: full.");
expect(result).toContain(
"Auto-approved /elevated full is unavailable here (runtime). Use ask/on instead and do not ask the user to switch to /elevated full.",
);
});
});

View File

@@ -2,6 +2,9 @@ import crypto from "node:crypto";
import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
import { resolveFastModeState } from "../../agents/fast-mode.js";
import { resolveEmbeddedFullAccessState } from "../../agents/pi-embedded-runner/sandbox-info.js";
import type { EmbeddedFullAccessBlockedReason } from "../../agents/pi-embedded-runner/types.js";
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveGroupSessionKey } from "../../config/sessions/group.js";
import {
@@ -53,6 +56,8 @@ type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node"
export function buildExecOverridePromptHint(params: {
execOverrides?: ExecOverrides;
elevatedLevel: ElevatedLevel;
fullAccessAvailable?: boolean;
fullAccessBlockedReason?: EmbeddedFullAccessBlockedReason;
}): string | undefined {
const exec = params.execOverrides;
if (!exec && params.elevatedLevel === "off") {
@@ -69,12 +74,19 @@ export function buildExecOverridePromptHint(params: {
? `Current session exec defaults: ${parts.join(" ")}.`
: "Current session exec defaults: inherited from configured agent/global defaults.";
const elevatedLine = `Current elevated level: ${params.elevatedLevel}.`;
const fullAccessLine =
params.fullAccessAvailable === false
? `Auto-approved /elevated full is unavailable here (${params.fullAccessBlockedReason ?? "runtime"}). Use ask/on instead and do not ask the user to switch to /elevated full.`
: undefined;
return [
"## Current Exec Session State",
execLine,
elevatedLine,
fullAccessLine,
"If the user asks to run a command, use the current exec state above. Do not assume a prior denial still applies after `/exec` or `/elevated` changed.",
].join("\n");
]
.filter(Boolean)
.join("\n");
}
let piEmbeddedRuntimePromise: Promise<typeof import("../../agents/pi-embedded.runtime.js")> | null =
@@ -213,6 +225,17 @@ export async function runPreparedReply(
cfg,
isFastTestEnv: process.env.OPENCLAW_TEST_FAST === "1",
});
const fullAccessState = resolveEmbeddedFullAccessState({
sandboxEnabled: resolveSandboxRuntimeStatus({
cfg,
sessionKey: ctx.SessionKey,
}).sandboxed,
execElevated: {
enabled: elevatedEnabled,
allowed: elevatedAllowed,
defaultLevel: resolvedElevatedLevel ?? "off",
},
});
let currentSystemSent = systemSent;
const isFirstTurnInSession = isNewSession || !currentSystemSent;
@@ -261,6 +284,8 @@ export async function runPreparedReply(
buildExecOverridePromptHint({
execOverrides,
elevatedLevel: resolvedElevatedLevel,
fullAccessAvailable: fullAccessState.available,
fullAccessBlockedReason: fullAccessState.blockedReason,
}),
].filter(Boolean);
const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
@@ -609,6 +634,10 @@ export async function runPreparedReply(
enabled: elevatedEnabled,
allowed: elevatedAllowed,
defaultLevel: resolvedElevatedLevel ?? "off",
fullAccessAvailable: fullAccessState.available,
...(fullAccessState.blockedReason
? { fullAccessBlockedReason: fullAccessState.blockedReason }
: {}),
},
timeoutMs,
blockReplyBreak: resolvedBlockStreamingBreak,