feat: add elevated bash mode

This commit is contained in:
Peter Steinberger
2026-01-04 05:15:42 +00:00
parent b978cc4e91
commit fe0b3500cc
29 changed files with 509 additions and 7 deletions

View File

@@ -119,6 +119,19 @@ describe("bash tool backgrounding", () => {
expect(status).toBe("failed");
});
it("rejects elevated requests when not allowed", async () => {
const customBash = createBashTool({
elevated: { enabled: true, allowed: false, defaultLevel: "off" },
});
await expect(
customBash.execute("call1", {
command: "echo hi",
elevated: true,
}),
).rejects.toThrow("elevated is not available right now.");
});
it("logs line-based slices and defaults to last lines", async () => {
const result = await bashTool.execute("call1", {
command:

View File

@@ -26,6 +26,7 @@ import {
killProcessTree,
sanitizeBinaryOutput,
} from "./shell-utils.js";
import { logInfo } from "../logger.js";
const CHUNK_LIMIT = 8 * 1024;
const DEFAULT_MAX_OUTPUT = clampNumber(
@@ -53,6 +54,7 @@ export type BashToolDefaults = {
backgroundMs?: number;
timeoutSec?: number;
sandbox?: BashSandboxConfig;
elevated?: BashElevatedDefaults;
};
export type ProcessToolDefaults = {
@@ -66,6 +68,12 @@ export type BashSandboxConfig = {
env?: Record<string, string>;
};
export type BashElevatedDefaults = {
enabled: boolean;
allowed: boolean;
defaultLevel: "on" | "off";
};
const bashSchema = Type.Object({
command: Type.String({ description: "Bash command to execute" }),
workdir: Type.Optional(
@@ -85,6 +93,11 @@ const bashSchema = Type.Object({
description: "Timeout in seconds (optional, kills process on expiry)",
}),
),
elevated: Type.Optional(
Type.Boolean({
description: "Run on the host with elevated permissions (if allowed)",
}),
),
});
export type BashToolDetails =
@@ -131,6 +144,7 @@ export function createBashTool(
yieldMs?: number;
background?: boolean;
timeout?: number;
elevated?: boolean;
};
if (!params.command) {
@@ -149,7 +163,24 @@ export function createBashTool(
const startedAt = Date.now();
const sessionId = randomUUID();
const warnings: string[] = [];
const sandbox = defaults?.sandbox;
const elevatedDefaults = defaults?.elevated;
const elevatedRequested =
typeof params.elevated === "boolean"
? params.elevated
: elevatedDefaults?.defaultLevel === "on";
if (elevatedRequested) {
if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) {
throw new Error("elevated is not available right now.");
}
logInfo(
`bash: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle(
params.command,
120,
)}`,
);
}
const sandbox = elevatedRequested ? undefined : defaults?.sandbox;
const rawWorkdir = params.workdir?.trim() || process.cwd();
let workdir = rawWorkdir;
let containerWorkdir = sandbox?.containerWorkdir;

View File

@@ -34,6 +34,7 @@ import {
} from "../process/command-queue.js";
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
import { resolveClawdisAgentDir } from "./agent-paths.js";
import type { BashElevatedDefaults } from "./bash-tools.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
import { ensureClawdisModelsJson } from "./models-config.js";
import {
@@ -390,6 +391,7 @@ export async function runEmbeddedPiAgent(params: {
model?: string;
thinkLevel?: ThinkLevel;
verboseLevel?: VerboseLevel;
bashElevated?: BashElevatedDefaults;
timeoutMs: number;
runId: string;
abortSignal?: AbortSignal;
@@ -495,7 +497,10 @@ export async function runEmbeddedPiAgent(params: {
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
const tools = createClawdisCodingTools({
bash: params.config?.agent?.bash,
bash: {
...params.config?.agent?.bash,
elevated: params.bashElevated,
},
sandbox,
surface: params.surface,
sessionKey: params.sessionKey ?? params.sessionId,