feat(sandbox): add sandbox explain inspector
This commit is contained in:
@@ -828,7 +828,7 @@ describe("directive behavior", () => {
|
||||
);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("elevated is not available right now.");
|
||||
expect(text).toContain("agents.list[].tools.elevated.enabled");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -875,7 +875,7 @@ describe("directive behavior", () => {
|
||||
);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("elevated is not available right now.");
|
||||
expect(text).toContain("agents.list[].tools.elevated.allowFrom.whatsapp");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -556,7 +556,7 @@ describe("trigger handling", () => {
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("elevated is not available right now.");
|
||||
expect(text).toContain("tools.elevated.enabled");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
@@ -795,7 +795,7 @@ describe("trigger handling", () => {
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).not.toBe("elevated is not available right now.");
|
||||
expect(text).not.toContain("elevated is not available right now");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -876,7 +876,7 @@ describe("trigger handling", () => {
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("elevated is not available right now.");
|
||||
expect(text).toContain("tools.elevated.allowFrom.discord");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
isEmbeddedPiRunStreaming,
|
||||
resolveEmbeddedSessionLane,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js";
|
||||
import {
|
||||
ensureSandboxWorkspaceForSession,
|
||||
resolveSandboxRuntimeStatus,
|
||||
} from "../agents/sandbox.js";
|
||||
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
||||
import {
|
||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||
@@ -216,15 +219,22 @@ function resolveElevatedPermissions(params: {
|
||||
agentId: string;
|
||||
ctx: MsgContext;
|
||||
provider: string;
|
||||
}): { enabled: boolean; allowed: boolean } {
|
||||
}): { enabled: boolean; allowed: boolean; failures: Array<{ gate: string; key: string }> } {
|
||||
const globalConfig = params.cfg.tools?.elevated;
|
||||
const agentConfig = resolveAgentConfig(params.cfg, params.agentId)?.tools
|
||||
?.elevated;
|
||||
const globalEnabled = globalConfig?.enabled !== false;
|
||||
const agentEnabled = agentConfig?.enabled !== false;
|
||||
const enabled = globalEnabled && agentEnabled;
|
||||
if (!enabled) return { enabled, allowed: false };
|
||||
if (!params.provider) return { enabled, allowed: false };
|
||||
const failures: Array<{ gate: string; key: string }> = [];
|
||||
if (!globalEnabled) failures.push({ gate: "enabled", key: "tools.elevated.enabled" });
|
||||
if (!agentEnabled)
|
||||
failures.push({ gate: "enabled", key: "agents.list[].tools.elevated.enabled" });
|
||||
if (!enabled) return { enabled, allowed: false, failures };
|
||||
if (!params.provider) {
|
||||
failures.push({ gate: "provider", key: "ctx.Provider" });
|
||||
return { enabled, allowed: false, failures };
|
||||
}
|
||||
|
||||
const discordFallback =
|
||||
params.provider === "discord"
|
||||
@@ -236,7 +246,16 @@ function resolveElevatedPermissions(params: {
|
||||
allowFrom: globalConfig?.allowFrom,
|
||||
discordFallback,
|
||||
});
|
||||
if (!globalAllowed) return { enabled, allowed: false };
|
||||
if (!globalAllowed) {
|
||||
failures.push({
|
||||
gate: "allowFrom",
|
||||
key:
|
||||
params.provider === "discord" && discordFallback
|
||||
? "tools.elevated.allowFrom.discord (or discord.dm.allowFrom fallback)"
|
||||
: `tools.elevated.allowFrom.${params.provider}`,
|
||||
});
|
||||
return { enabled, allowed: false, failures };
|
||||
}
|
||||
|
||||
const agentAllowed = agentConfig?.allowFrom
|
||||
? isApprovedElevatedSender({
|
||||
@@ -245,7 +264,44 @@ function resolveElevatedPermissions(params: {
|
||||
allowFrom: agentConfig.allowFrom,
|
||||
})
|
||||
: true;
|
||||
return { enabled, allowed: globalAllowed && agentAllowed };
|
||||
if (!agentAllowed) {
|
||||
failures.push({
|
||||
gate: "allowFrom",
|
||||
key: `agents.list[].tools.elevated.allowFrom.${params.provider}`,
|
||||
});
|
||||
}
|
||||
return { enabled, allowed: globalAllowed && agentAllowed, failures };
|
||||
}
|
||||
|
||||
function formatElevatedUnavailableMessage(params: {
|
||||
runtimeSandboxed: boolean;
|
||||
failures: Array<{ gate: string; key: string }>;
|
||||
sessionKey?: string;
|
||||
}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(
|
||||
`elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
|
||||
);
|
||||
if (params.failures.length > 0) {
|
||||
lines.push(
|
||||
`Failing gates: ${params.failures
|
||||
.map((f) => `${f.gate} (${f.key})`)
|
||||
.join(", ")}`,
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>).",
|
||||
);
|
||||
}
|
||||
lines.push("Fix-it keys:");
|
||||
lines.push("- tools.elevated.enabled");
|
||||
lines.push("- tools.elevated.allowFrom.<provider>");
|
||||
lines.push("- agents.list[].tools.elevated.enabled");
|
||||
lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
|
||||
if (params.sessionKey) {
|
||||
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function getReplyFromConfig(
|
||||
@@ -473,19 +529,31 @@ export async function getReplyFromConfig(
|
||||
sessionCtx.Provider?.trim().toLowerCase() ??
|
||||
ctx.Provider?.trim().toLowerCase() ??
|
||||
"";
|
||||
const { enabled: elevatedEnabled, allowed: elevatedAllowed } =
|
||||
resolveElevatedPermissions({
|
||||
cfg,
|
||||
agentId,
|
||||
ctx,
|
||||
provider: messageProviderKey,
|
||||
});
|
||||
const elevated = resolveElevatedPermissions({
|
||||
cfg,
|
||||
agentId,
|
||||
ctx,
|
||||
provider: messageProviderKey,
|
||||
});
|
||||
const elevatedEnabled = elevated.enabled;
|
||||
const elevatedAllowed = elevated.allowed;
|
||||
const elevatedFailures = elevated.failures;
|
||||
if (
|
||||
directives.hasElevatedDirective &&
|
||||
(!elevatedEnabled || !elevatedAllowed)
|
||||
) {
|
||||
typing.cleanup();
|
||||
return { text: "elevated is not available right now." };
|
||||
const runtimeSandboxed = resolveSandboxRuntimeStatus({
|
||||
cfg,
|
||||
sessionKey: ctx.SessionKey,
|
||||
}).sandboxed;
|
||||
return {
|
||||
text: formatElevatedUnavailableMessage({
|
||||
runtimeSandboxed,
|
||||
failures: elevatedFailures,
|
||||
sessionKey: ctx.SessionKey,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const requireMention = resolveGroupRequireMention({
|
||||
@@ -621,6 +689,8 @@ export async function getReplyFromConfig(
|
||||
storePath,
|
||||
elevatedEnabled,
|
||||
elevatedAllowed,
|
||||
elevatedFailures,
|
||||
messageProviderKey,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
aliasIndex,
|
||||
|
||||
@@ -29,10 +29,9 @@ import {
|
||||
resolveConfiguredModelRef,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import { resolveSandboxConfigForAgent } from "../../agents/sandbox.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
resolveAgentMainSessionKey,
|
||||
type SessionEntry,
|
||||
saveSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
@@ -72,6 +71,31 @@ const withOptions = (line: string, options: string) =>
|
||||
const formatElevatedRuntimeHint = () =>
|
||||
`${SYSTEM_MARK} Runtime is direct; sandboxing does not apply.`;
|
||||
|
||||
function formatElevatedUnavailableText(params: {
|
||||
runtimeSandboxed: boolean;
|
||||
failures?: Array<{ gate: string; key: string }>;
|
||||
sessionKey?: string;
|
||||
}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(
|
||||
`elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
|
||||
);
|
||||
const failures = params.failures ?? [];
|
||||
if (failures.length > 0) {
|
||||
lines.push(
|
||||
`Failing gates: ${failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`,
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"Fix-it keys: tools.elevated.enabled, tools.elevated.allowFrom.<provider>, agents.list[].tools.elevated.*",
|
||||
);
|
||||
}
|
||||
if (params.sessionKey) {
|
||||
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const maskApiKey = (value: string): string => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "missing";
|
||||
@@ -452,6 +476,8 @@ export async function handleDirectiveOnly(params: {
|
||||
storePath?: string;
|
||||
elevatedEnabled: boolean;
|
||||
elevatedAllowed: boolean;
|
||||
elevatedFailures?: Array<{ gate: string; key: string }>;
|
||||
messageProviderKey?: string;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
@@ -496,22 +522,10 @@ export async function handleDirectiveOnly(params: {
|
||||
config: params.cfg,
|
||||
});
|
||||
const agentDir = resolveAgentDir(params.cfg, activeAgentId);
|
||||
const runtimeIsSandboxed = (() => {
|
||||
const sessionKey = params.sessionKey?.trim();
|
||||
if (!sessionKey) return false;
|
||||
const agentId = resolveSessionAgentId({
|
||||
sessionKey,
|
||||
config: params.cfg,
|
||||
});
|
||||
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, agentId);
|
||||
if (sandboxCfg.mode === "off") return false;
|
||||
const mainKey = resolveAgentMainSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId,
|
||||
});
|
||||
if (sandboxCfg.mode === "all") return true;
|
||||
return sessionKey !== mainKey;
|
||||
})();
|
||||
const runtimeIsSandboxed = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.sessionKey,
|
||||
}).sandboxed;
|
||||
const shouldHintDirectRuntime =
|
||||
directives.hasElevatedDirective && !runtimeIsSandboxed;
|
||||
|
||||
@@ -709,7 +723,13 @@ export async function handleDirectiveOnly(params: {
|
||||
if (directives.hasElevatedDirective && !directives.elevatedLevel) {
|
||||
if (!directives.rawElevatedLevel) {
|
||||
if (!elevatedEnabled || !elevatedAllowed) {
|
||||
return { text: "elevated is not available right now." };
|
||||
return {
|
||||
text: formatElevatedUnavailableText({
|
||||
runtimeSandboxed: runtimeIsSandboxed,
|
||||
failures: params.elevatedFailures,
|
||||
sessionKey: params.sessionKey,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const level = currentElevatedLevel ?? "off";
|
||||
return {
|
||||
@@ -729,7 +749,13 @@ export async function handleDirectiveOnly(params: {
|
||||
directives.hasElevatedDirective &&
|
||||
(!elevatedEnabled || !elevatedAllowed)
|
||||
) {
|
||||
return { text: "elevated is not available right now." };
|
||||
return {
|
||||
text: formatElevatedUnavailableText({
|
||||
runtimeSandboxed: runtimeIsSandboxed,
|
||||
failures: params.elevatedFailures,
|
||||
sessionKey: params.sessionKey,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user