mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
fix: make elevated full truthful
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user