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

@@ -187,6 +187,7 @@ export async function runReplyAgent(params: {
model: followupRun.run.model,
thinkLevel: followupRun.run.thinkLevel,
verboseLevel: followupRun.run.verboseLevel,
bashElevated: followupRun.run.bashElevated,
timeoutMs: followupRun.run.timeoutMs,
runId,
blockReplyBreak: resolvedBlockStreamingBreak,

View File

@@ -18,8 +18,10 @@ import { extractModelDirective } from "../model.js";
import type { MsgContext } from "../templating.js";
import type { ReplyPayload } from "../types.js";
import {
extractElevatedDirective,
extractThinkDirective,
extractVerboseDirective,
type ElevatedLevel,
type ThinkLevel,
type VerboseLevel,
} from "./directives.js";
@@ -44,6 +46,9 @@ export type InlineDirectives = {
hasVerboseDirective: boolean;
verboseLevel?: VerboseLevel;
rawVerboseLevel?: string;
hasElevatedDirective: boolean;
elevatedLevel?: ElevatedLevel;
rawElevatedLevel?: string;
hasModelDirective: boolean;
rawModelDirective?: string;
hasQueueDirective: boolean;
@@ -72,11 +77,17 @@ export function parseInlineDirectives(body: string): InlineDirectives {
rawLevel: rawVerboseLevel,
hasDirective: hasVerboseDirective,
} = extractVerboseDirective(thinkCleaned);
const {
cleaned: elevatedCleaned,
elevatedLevel,
rawLevel: rawElevatedLevel,
hasDirective: hasElevatedDirective,
} = extractElevatedDirective(verboseCleaned);
const {
cleaned: modelCleaned,
rawModel,
hasDirective: hasModelDirective,
} = extractModelDirective(verboseCleaned);
} = extractModelDirective(elevatedCleaned);
const {
cleaned: queueCleaned,
queueMode,
@@ -100,6 +111,9 @@ export function parseInlineDirectives(body: string): InlineDirectives {
hasVerboseDirective,
verboseLevel,
rawVerboseLevel,
hasElevatedDirective,
elevatedLevel,
rawElevatedLevel,
hasModelDirective,
rawModelDirective: rawModel,
hasQueueDirective,
@@ -127,6 +141,7 @@ export function isDirectiveOnly(params: {
if (
!directives.hasThinkDirective &&
!directives.hasVerboseDirective &&
!directives.hasElevatedDirective &&
!directives.hasModelDirective &&
!directives.hasQueueDirective
)
@@ -142,6 +157,8 @@ export async function handleDirectiveOnly(params: {
sessionStore?: Record<string, SessionEntry>;
sessionKey?: string;
storePath?: string;
elevatedEnabled: boolean;
elevatedAllowed: boolean;
defaultProvider: string;
defaultModel: string;
aliasIndex: ModelAliasIndex;
@@ -161,6 +178,8 @@ export async function handleDirectiveOnly(params: {
sessionStore,
sessionKey,
storePath,
elevatedEnabled,
elevatedAllowed,
defaultProvider,
defaultModel,
aliasIndex,
@@ -213,6 +232,17 @@ export async function handleDirectiveOnly(params: {
text: `Unrecognized verbose level "${directives.rawVerboseLevel ?? ""}". Valid levels: off, on.`,
};
}
if (directives.hasElevatedDirective && !directives.elevatedLevel) {
return {
text: `Unrecognized elevated level "${directives.rawElevatedLevel ?? ""}". Valid levels: off, on.`,
};
}
if (
directives.hasElevatedDirective &&
(!elevatedEnabled || !elevatedAllowed)
) {
return { text: "elevated is not available right now." };
}
const queueModeInvalid =
directives.hasQueueDirective &&
@@ -296,6 +326,10 @@ export async function handleDirectiveOnly(params: {
if (directives.verboseLevel === "off") delete sessionEntry.verboseLevel;
else sessionEntry.verboseLevel = directives.verboseLevel;
}
if (directives.hasElevatedDirective && directives.elevatedLevel) {
if (directives.elevatedLevel === "off") delete sessionEntry.elevatedLevel;
else sessionEntry.elevatedLevel = directives.elevatedLevel;
}
if (modelSelection) {
if (modelSelection.isDefault) {
delete sessionEntry.providerOverride;
@@ -344,6 +378,13 @@ export async function handleDirectiveOnly(params: {
: `${SYSTEM_MARK} Verbose logging enabled.`,
);
}
if (directives.hasElevatedDirective && directives.elevatedLevel) {
parts.push(
directives.elevatedLevel === "off"
? `${SYSTEM_MARK} Elevated mode disabled.`
: `${SYSTEM_MARK} Elevated mode enabled.`,
);
}
if (modelSelection) {
const label = `${modelSelection.provider}/${modelSelection.model}`;
const labelWithAlias = modelSelection.alias
@@ -385,6 +426,8 @@ export async function persistInlineDirectives(params: {
sessionStore?: Record<string, SessionEntry>;
sessionKey?: string;
storePath?: string;
elevatedEnabled: boolean;
elevatedAllowed: boolean;
defaultProvider: string;
defaultModel: string;
aliasIndex: ModelAliasIndex;
@@ -401,6 +444,8 @@ export async function persistInlineDirectives(params: {
sessionStore,
sessionKey,
storePath,
elevatedEnabled,
elevatedAllowed,
defaultProvider,
defaultModel,
aliasIndex,
@@ -429,6 +474,19 @@ export async function persistInlineDirectives(params: {
}
updated = true;
}
if (
directives.hasElevatedDirective &&
directives.elevatedLevel &&
elevatedEnabled &&
elevatedAllowed
) {
if (directives.elevatedLevel === "off") {
delete sessionEntry.elevatedLevel;
} else {
sessionEntry.elevatedLevel = directives.elevatedLevel;
}
updated = true;
}
const modelDirective =
directives.hasModelDirective && params.effectiveModelDirective
? params.effectiveModelDirective

View File

@@ -1,6 +1,8 @@
import {
normalizeElevatedLevel,
normalizeThinkLevel,
normalizeVerboseLevel,
type ElevatedLevel,
type ThinkLevel,
type VerboseLevel,
} from "../thinking.js";
@@ -50,4 +52,26 @@ export function extractVerboseDirective(body?: string): {
};
}
export type { ThinkLevel, VerboseLevel };
export function extractElevatedDirective(body?: string): {
cleaned: string;
elevatedLevel?: ElevatedLevel;
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
const match = body.match(
/(?:^|\s)\/(?:elevated|elev)(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)\b/i,
);
const elevatedLevel = normalizeElevatedLevel(match?.[1]);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
elevatedLevel,
rawLevel: match?.[1],
hasDirective: !!match,
};
}
export type { ElevatedLevel, ThinkLevel, VerboseLevel };

View File

@@ -78,6 +78,7 @@ export function createFollowupRunner(params: {
model: queued.run.model,
thinkLevel: queued.run.thinkLevel,
verboseLevel: queued.run.verboseLevel,
bashElevated: queued.run.bashElevated,
timeoutMs: queued.run.timeoutMs,
runId,
blockReplyBreak: queued.run.blockReplyBreak,

View File

@@ -3,7 +3,7 @@ import { parseDurationMs } from "../../cli/parse-duration.js";
import type { ClawdisConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { defaultRuntime } from "../../runtime.js";
import type { ThinkLevel, VerboseLevel } from "./directives.js";
import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "./directives.js";
export type QueueMode =
| "steer"
| "followup"
@@ -34,6 +34,12 @@ export type FollowupRun = {
model: string;
thinkLevel?: ThinkLevel;
verboseLevel?: VerboseLevel;
elevatedLevel?: ElevatedLevel;
bashElevated?: {
enabled: boolean;
allowed: boolean;
defaultLevel: ElevatedLevel;
};
timeoutMs: number;
blockReplyBreak: "text_end" | "message_end";
ownerNumbers?: string[];