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

@@ -16,6 +16,7 @@ Key parameters:
- `yieldMs` (default 10000): autobackground after this delay - `yieldMs` (default 10000): autobackground after this delay
- `background` (bool): background immediately - `background` (bool): background immediately
- `timeout` (seconds, default 1800): kill the process after this timeout - `timeout` (seconds, default 1800): kill the process after this timeout
- `elevated` (bool): run on host if elevated mode is enabled/allowed
- Need a real TTY? Use the tmux skill. - Need a real TTY? Use the tmux skill.
- `workdir`, `env` - `workdir`, `env`

View File

@@ -15,6 +15,7 @@ Run shell commands in the workspace. Supports foreground + background execution
- `yieldMs` (default 10000): auto-background after delay - `yieldMs` (default 10000): auto-background after delay
- `background` (bool): background immediately - `background` (bool): background immediately
- `timeout` (seconds, default 1800): kill on expiry - `timeout` (seconds, default 1800): kill on expiry
- `elevated` (bool): run on host if elevated mode is enabled/allowed
- Need a real TTY? Use the tmux skill. - Need a real TTY? Use the tmux skill.
## Examples ## Examples

View File

@@ -388,6 +388,7 @@ Controls the embedded agent runtime (model/thinking/verbose/timeouts).
}, },
thinkingDefault: "low", thinkingDefault: "low",
verboseDefault: "off", verboseDefault: "off",
elevatedDefault: "off",
timeoutSeconds: 600, timeoutSeconds: 600,
mediaMaxMb: 5, mediaMaxMb: 5,
heartbeat: { heartbeat: {
@@ -439,6 +440,31 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800) - `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000) - `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
`agent.elevated` controls elevated (host) bash access:
- `enabled`: allow elevated mode (default true)
- `allowFrom`: per-surface allowlists (required to enable; empty = disabled)
- `whatsapp`: E.164 numbers
- `telegram`: chat ids or usernames
- `discord`: user ids or usernames
- `signal`: E.164 numbers
- `imessage`: handles/chat ids
- `webchat`: session ids or usernames
Example:
```json5
{
agent: {
elevated: {
enabled: true,
allowFrom: {
whatsapp: ["+15555550123"],
discord: ["steipete", "1234567890123"]
}
}
}
}
```
`agent.maxConcurrent` sets the maximum number of embedded agent runs that can `agent.maxConcurrent` sets the maximum number of embedded agent runs that can
execute in parallel across sessions. Each session is still serialized (one run execute in parallel across sessions. Each session is still serialized (one run
per session key at a time). Default: 1. per session key at a time). Default: 1.

View File

@@ -494,6 +494,7 @@ Quick reference (send these in chat):
| `/new` or `/reset` | Reset the session | | `/new` or `/reset` | Reset the session |
| `/think <level>` | Set thinking level (off\|minimal\|low\|medium\|high) | | `/think <level>` | Set thinking level (off\|minimal\|low\|medium\|high) |
| `/verbose on\|off` | Toggle verbose mode | | `/verbose on\|off` | Toggle verbose mode |
| `/elevated on\|off` | Toggle elevated bash mode (approved senders only) |
| `/activation mention\|always` | Group activation (owner-only) | | `/activation mention\|always` | Group activation (owner-only) |
| `/model <name>` | Switch AI model (see below) | | `/model <name>` | Switch AI model (see below) |
| `/queue instant\|batch\|serial` | Message queuing mode | | `/queue instant\|batch\|serial` | Message queuing mode |

View File

@@ -34,6 +34,11 @@ read_when:
- Inline directive affects only that message; session/global defaults apply otherwise. - Inline directive affects only that message; session/global defaults apply otherwise.
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting. - When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting.
## Elevated directives (/elevated or /elev)
- Levels: `on` or `off` (default).
- Directive-only message toggles session elevated mode and replies `Elevated mode enabled.` / `Elevated mode disabled.`.
- If elevated access is disabled or the sender is not on the approved allowlist, the directive replies `elevated is not available right now.` and does not change session state.
## Heartbeats ## Heartbeats
- Heartbeat probe body is `HEARTBEAT`. Inline directives in a heartbeat message apply as usual (but avoid changing session defaults from heartbeats). - Heartbeat probe body is `HEARTBEAT`. Inline directives in a heartbeat message apply as usual (but avoid changing session defaults from heartbeats).

View File

@@ -21,6 +21,7 @@ Core parameters:
- `yieldMs` (auto-background after timeout, default 10000) - `yieldMs` (auto-background after timeout, default 10000)
- `background` (immediate background) - `background` (immediate background)
- `timeout` (seconds; kills the process if exceeded, default 1800) - `timeout` (seconds; kills the process if exceeded, default 1800)
- `elevated` (bool; run on host if elevated mode is enabled/allowed)
- Need a real TTY? Use the tmux skill. - Need a real TTY? Use the tmux skill.
Notes: Notes:

View File

@@ -51,6 +51,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
- `/model <provider/model>` (or `/models`) - `/model <provider/model>` (or `/models`)
- `/think <off|minimal|low|medium|high>` - `/think <off|minimal|low|medium|high>`
- `/verbose <on|off>` - `/verbose <on|off>`
- `/elevated <on|off>`
- `/activation <mention|always>` - `/activation <mention|always>`
- `/deliver <on|off>` - `/deliver <on|off>`
- `/new` or `/reset` - `/new` or `/reset`

View File

@@ -119,6 +119,19 @@ describe("bash tool backgrounding", () => {
expect(status).toBe("failed"); 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 () => { it("logs line-based slices and defaults to last lines", async () => {
const result = await bashTool.execute("call1", { const result = await bashTool.execute("call1", {
command: command:

View File

@@ -26,6 +26,7 @@ import {
killProcessTree, killProcessTree,
sanitizeBinaryOutput, sanitizeBinaryOutput,
} from "./shell-utils.js"; } from "./shell-utils.js";
import { logInfo } from "../logger.js";
const CHUNK_LIMIT = 8 * 1024; const CHUNK_LIMIT = 8 * 1024;
const DEFAULT_MAX_OUTPUT = clampNumber( const DEFAULT_MAX_OUTPUT = clampNumber(
@@ -53,6 +54,7 @@ export type BashToolDefaults = {
backgroundMs?: number; backgroundMs?: number;
timeoutSec?: number; timeoutSec?: number;
sandbox?: BashSandboxConfig; sandbox?: BashSandboxConfig;
elevated?: BashElevatedDefaults;
}; };
export type ProcessToolDefaults = { export type ProcessToolDefaults = {
@@ -66,6 +68,12 @@ export type BashSandboxConfig = {
env?: Record<string, string>; env?: Record<string, string>;
}; };
export type BashElevatedDefaults = {
enabled: boolean;
allowed: boolean;
defaultLevel: "on" | "off";
};
const bashSchema = Type.Object({ const bashSchema = Type.Object({
command: Type.String({ description: "Bash command to execute" }), command: Type.String({ description: "Bash command to execute" }),
workdir: Type.Optional( workdir: Type.Optional(
@@ -85,6 +93,11 @@ const bashSchema = Type.Object({
description: "Timeout in seconds (optional, kills process on expiry)", 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 = export type BashToolDetails =
@@ -131,6 +144,7 @@ export function createBashTool(
yieldMs?: number; yieldMs?: number;
background?: boolean; background?: boolean;
timeout?: number; timeout?: number;
elevated?: boolean;
}; };
if (!params.command) { if (!params.command) {
@@ -149,7 +163,24 @@ export function createBashTool(
const startedAt = Date.now(); const startedAt = Date.now();
const sessionId = randomUUID(); const sessionId = randomUUID();
const warnings: string[] = []; 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(); const rawWorkdir = params.workdir?.trim() || process.cwd();
let workdir = rawWorkdir; let workdir = rawWorkdir;
let containerWorkdir = sandbox?.containerWorkdir; let containerWorkdir = sandbox?.containerWorkdir;

View File

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

View File

@@ -13,6 +13,7 @@ import {
} from "../config/sessions.js"; } from "../config/sessions.js";
import { drainSystemEvents } from "../infra/system-events.js"; import { drainSystemEvents } from "../infra/system-events.js";
import { import {
extractElevatedDirective,
extractQueueDirective, extractQueueDirective,
extractReplyToTag, extractReplyToTag,
extractThinkDirective, extractThinkDirective,
@@ -85,6 +86,12 @@ describe("directive parsing", () => {
expect(res.verboseLevel).toBe("on"); expect(res.verboseLevel).toBe("on");
}); });
it("matches elevated with leading space", () => {
const res = extractElevatedDirective(" please /elevated on now");
expect(res.hasDirective).toBe(true);
expect(res.elevatedLevel).toBe("on");
});
it("matches think at start of line", () => { it("matches think at start of line", () => {
const res = extractThinkDirective("/think:high run slow"); const res = extractThinkDirective("/think:high run slow");
expect(res.hasDirective).toBe(true); expect(res.hasDirective).toBe(true);

View File

@@ -143,6 +143,85 @@ describe("trigger handling", () => {
}); });
}); });
it("allows approved sender to toggle elevated mode", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
elevated: {
allowFrom: { whatsapp: ["+1000"] },
},
},
whatsapp: {
allowFrom: ["+1000"],
},
session: { store: join(home, "sessions.json") },
};
const res = await getReplyFromConfig(
{
Body: "/elevated on",
From: "+1000",
To: "+2000",
Surface: "whatsapp",
SenderE164: "+1000",
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Elevated mode enabled");
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
const store = JSON.parse(storeRaw) as Record<
string,
{ elevatedLevel?: string }
>;
expect(store.main?.elevatedLevel).toBe("on");
});
});
it("rejects elevated toggles when disabled", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
elevated: {
enabled: false,
allowFrom: { whatsapp: ["+1000"] },
},
},
whatsapp: {
allowFrom: ["+1000"],
},
session: { store: join(home, "sessions.json") },
};
const res = await getReplyFromConfig(
{
Body: "/elevated on",
From: "+1000",
To: "+2000",
Surface: "whatsapp",
SenderE164: "+1000",
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("elevated is not available right now.");
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
const store = JSON.parse(storeRaw) as Record<
string,
{ elevatedLevel?: string }
>;
expect(store.main?.elevatedLevel).toBeUndefined();
});
});
it("returns a context overflow fallback when the embedded agent throws", async () => { it("returns a context overflow fallback when the embedded agent throws", async () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockRejectedValue( vi.mocked(runEmbeddedPiAgent).mockRejectedValue(

View File

@@ -11,7 +11,11 @@ import {
DEFAULT_AGENT_WORKSPACE_DIR, DEFAULT_AGENT_WORKSPACE_DIR,
ensureAgentWorkspace, ensureAgentWorkspace,
} from "../agents/workspace.js"; } from "../agents/workspace.js";
import { type ClawdisConfig, loadConfig } from "../config/config.js"; import {
type AgentElevatedAllowFromConfig,
type ClawdisConfig,
loadConfig,
} from "../config/config.js";
import { resolveSessionTranscriptPath } from "../config/sessions.js"; import { resolveSessionTranscriptPath } from "../config/sessions.js";
import { logVerbose } from "../globals.js"; import { logVerbose } from "../globals.js";
import { clearCommandLane, getQueueSize } from "../process/command-queue.js"; import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
@@ -47,6 +51,7 @@ import { createTypingController } from "./reply/typing.js";
import type { MsgContext } from "./templating.js"; import type { MsgContext } from "./templating.js";
import { import {
normalizeThinkLevel, normalizeThinkLevel,
type ElevatedLevel,
type ThinkLevel, type ThinkLevel,
type VerboseLevel, type VerboseLevel,
} from "./thinking.js"; } from "./thinking.js";
@@ -55,6 +60,7 @@ import { isAudio, transcribeInboundAudio } from "./transcription.js";
import type { GetReplyOptions, ReplyPayload } from "./types.js"; import type { GetReplyOptions, ReplyPayload } from "./types.js";
export { export {
extractElevatedDirective,
extractThinkDirective, extractThinkDirective,
extractVerboseDirective, extractVerboseDirective,
} from "./reply/directives.js"; } from "./reply/directives.js";
@@ -65,6 +71,99 @@ export type { GetReplyOptions, ReplyPayload } from "./types.js";
const BARE_SESSION_RESET_PROMPT = const BARE_SESSION_RESET_PROMPT =
"A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning."; "A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning.";
function normalizeAllowToken(value?: string) {
if (!value) return "";
return value.trim().toLowerCase();
}
function slugAllowToken(value?: string) {
if (!value) return "";
let text = value.trim().toLowerCase();
if (!text) return "";
text = text.replace(/^[@#]+/, "");
text = text.replace(/[\s_]+/g, "-");
text = text.replace(/[^a-z0-9-]+/g, "-");
return text.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
}
function stripSenderPrefix(value?: string) {
if (!value) return "";
const trimmed = value.trim();
return trimmed.replace(
/^(whatsapp|telegram|discord|signal|imessage|webchat|user|group|channel):/i,
"",
);
}
function resolveElevatedAllowList(
allowFrom: AgentElevatedAllowFromConfig | undefined,
surface: string,
): Array<string | number> | undefined {
switch (surface) {
case "whatsapp":
return allowFrom?.whatsapp;
case "telegram":
return allowFrom?.telegram;
case "discord":
return allowFrom?.discord;
case "signal":
return allowFrom?.signal;
case "imessage":
return allowFrom?.imessage;
case "webchat":
return allowFrom?.webchat;
default:
return undefined;
}
}
function isApprovedElevatedSender(params: {
surface: string;
ctx: MsgContext;
allowFrom?: AgentElevatedAllowFromConfig;
}): boolean {
const rawAllow = resolveElevatedAllowList(params.allowFrom, params.surface);
if (!rawAllow || rawAllow.length === 0) return false;
const allowTokens = rawAllow
.map((entry) => String(entry).trim())
.filter(Boolean);
if (allowTokens.length === 0) return false;
if (allowTokens.some((entry) => entry === "*")) return true;
const tokens = new Set<string>();
const addToken = (value?: string) => {
if (!value) return;
const trimmed = value.trim();
if (!trimmed) return;
tokens.add(trimmed);
const normalized = normalizeAllowToken(trimmed);
if (normalized) tokens.add(normalized);
const slugged = slugAllowToken(trimmed);
if (slugged) tokens.add(slugged);
};
addToken(params.ctx.SenderName);
addToken(params.ctx.SenderE164);
addToken(params.ctx.From);
addToken(stripSenderPrefix(params.ctx.From));
addToken(params.ctx.To);
addToken(stripSenderPrefix(params.ctx.To));
for (const rawEntry of allowTokens) {
const entry = rawEntry.trim();
if (!entry) continue;
const stripped = stripSenderPrefix(entry);
if (tokens.has(entry) || tokens.has(stripped)) return true;
const normalized = normalizeAllowToken(stripped);
if (normalized && tokens.has(normalized)) return true;
const slugged = slugAllowToken(stripped);
if (slugged && tokens.has(slugged)) return true;
}
return false;
}
export async function getReplyFromConfig( export async function getReplyFromConfig(
ctx: MsgContext, ctx: MsgContext,
opts?: GetReplyOptions, opts?: GetReplyOptions,
@@ -146,6 +245,27 @@ export async function getReplyFromConfig(
sessionCtx.Body = directives.cleaned; sessionCtx.Body = directives.cleaned;
sessionCtx.BodyStripped = directives.cleaned; sessionCtx.BodyStripped = directives.cleaned;
const surfaceKey =
sessionCtx.Surface?.trim().toLowerCase() ??
ctx.Surface?.trim().toLowerCase() ??
"";
const elevatedConfig = agentCfg?.elevated;
const elevatedEnabled = elevatedConfig?.enabled !== false;
const elevatedAllowed =
elevatedEnabled &&
Boolean(
surfaceKey &&
isApprovedElevatedSender({
surface: surfaceKey,
ctx,
allowFrom: elevatedConfig?.allowFrom,
}),
);
if (directives.hasElevatedDirective && (!elevatedEnabled || !elevatedAllowed)) {
typing.cleanup();
return { text: "elevated is not available right now." };
}
const requireMention = resolveGroupRequireMention({ const requireMention = resolveGroupRequireMention({
cfg, cfg,
ctx: sessionCtx, ctx: sessionCtx,
@@ -161,6 +281,12 @@ export async function getReplyFromConfig(
(directives.verboseLevel as VerboseLevel | undefined) ?? (directives.verboseLevel as VerboseLevel | undefined) ??
(sessionEntry?.verboseLevel as VerboseLevel | undefined) ?? (sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
(agentCfg?.verboseDefault as VerboseLevel | undefined); (agentCfg?.verboseDefault as VerboseLevel | undefined);
const resolvedElevatedLevel = elevatedAllowed
? ((directives.elevatedLevel as ElevatedLevel | undefined) ??
(sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
(agentCfg?.elevatedDefault as ElevatedLevel | undefined) ??
"off")
: "off";
const resolvedBlockStreaming = const resolvedBlockStreaming =
agentCfg?.blockStreamingDefault === "off" ? "off" : "on"; agentCfg?.blockStreamingDefault === "off" ? "off" : "on";
const resolvedBlockStreamingBreak = const resolvedBlockStreamingBreak =
@@ -220,6 +346,8 @@ export async function getReplyFromConfig(
sessionStore, sessionStore,
sessionKey, sessionKey,
storePath, storePath,
elevatedEnabled,
elevatedAllowed,
defaultProvider, defaultProvider,
defaultModel, defaultModel,
aliasIndex, aliasIndex,
@@ -242,6 +370,8 @@ export async function getReplyFromConfig(
sessionStore, sessionStore,
sessionKey, sessionKey,
storePath, storePath,
elevatedEnabled,
elevatedAllowed,
defaultProvider, defaultProvider,
defaultModel, defaultModel,
aliasIndex, aliasIndex,
@@ -466,6 +596,12 @@ export async function getReplyFromConfig(
model, model,
thinkLevel: resolvedThinkLevel, thinkLevel: resolvedThinkLevel,
verboseLevel: resolvedVerboseLevel, verboseLevel: resolvedVerboseLevel,
elevatedLevel: resolvedElevatedLevel,
bashElevated: {
enabled: elevatedEnabled,
allowed: elevatedAllowed,
defaultLevel: resolvedElevatedLevel ?? "off",
},
timeoutMs, timeoutMs,
blockReplyBreak: resolvedBlockStreamingBreak, blockReplyBreak: resolvedBlockStreamingBreak,
ownerNumbers: ownerNumbers:

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import { import {
normalizeElevatedLevel,
normalizeThinkLevel, normalizeThinkLevel,
normalizeVerboseLevel, normalizeVerboseLevel,
type ElevatedLevel,
type ThinkLevel, type ThinkLevel,
type VerboseLevel, type VerboseLevel,
} from "../thinking.js"; } 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, model: queued.run.model,
thinkLevel: queued.run.thinkLevel, thinkLevel: queued.run.thinkLevel,
verboseLevel: queued.run.verboseLevel, verboseLevel: queued.run.verboseLevel,
bashElevated: queued.run.bashElevated,
timeoutMs: queued.run.timeoutMs, timeoutMs: queued.run.timeoutMs,
runId, runId,
blockReplyBreak: queued.run.blockReplyBreak, 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 { ClawdisConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js"; import type { SessionEntry } from "../../config/sessions.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import type { ThinkLevel, VerboseLevel } from "./directives.js"; import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "./directives.js";
export type QueueMode = export type QueueMode =
| "steer" | "steer"
| "followup" | "followup"
@@ -34,6 +34,12 @@ export type FollowupRun = {
model: string; model: string;
thinkLevel?: ThinkLevel; thinkLevel?: ThinkLevel;
verboseLevel?: VerboseLevel; verboseLevel?: VerboseLevel;
elevatedLevel?: ElevatedLevel;
bashElevated?: {
enabled: boolean;
allowed: boolean;
defaultLevel: ElevatedLevel;
};
timeoutMs: number; timeoutMs: number;
blockReplyBreak: "text_end" | "message_end"; blockReplyBreak: "text_end" | "message_end";
ownerNumbers?: string[]; ownerNumbers?: string[];

View File

@@ -161,6 +161,8 @@ export function buildStatusMessage(args: StatusArgs): string {
const thinkLevel = args.resolvedThink ?? args.agent?.thinkingDefault ?? "off"; const thinkLevel = args.resolvedThink ?? args.agent?.thinkingDefault ?? "off";
const verboseLevel = const verboseLevel =
args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off"; args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off";
const elevatedLevel =
args.entry?.elevatedLevel ?? args.agent?.elevatedDefault ?? "off";
const webLine = (() => { const webLine = (() => {
if (args.webLinked === false) { if (args.webLinked === false) {
@@ -200,7 +202,7 @@ export function buildStatusMessage(args: StatusArgs): string {
contextTokens ?? null, contextTokens ?? null,
)}${entry?.abortedLastRun ? " • last run aborted" : ""}`; )}${entry?.abortedLastRun ? " • last run aborted" : ""}`;
const optionsLine = `Options: thinking=${thinkLevel} | verbose=${verboseLevel} (set with /think <level>, /verbose on|off, /model <id>)`; const optionsLine = `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | elevated=${elevatedLevel} (set with /think <level>, /verbose on|off, /elevated on|off, /model <id>)`;
const modelLabel = model ? `${resolved.provider}/${model}` : "unknown"; const modelLabel = model ? `${resolved.provider}/${model}` : "unknown";

View File

@@ -1,5 +1,6 @@
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high"; export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
export type VerboseLevel = "off" | "on"; export type VerboseLevel = "off" | "on";
export type ElevatedLevel = "off" | "on";
// Normalize user-provided thinking level strings to the canonical enum. // Normalize user-provided thinking level strings to the canonical enum.
export function normalizeThinkLevel( export function normalizeThinkLevel(
@@ -39,3 +40,14 @@ export function normalizeVerboseLevel(
if (["on", "full", "true", "yes", "1"].includes(key)) return "on"; if (["on", "full", "true", "yes", "1"].includes(key)) return "on";
return undefined; return undefined;
} }
// Normalize elevated flags used to toggle elevated bash permissions.
export function normalizeElevatedLevel(
raw?: string | null,
): ElevatedLevel | undefined {
if (!raw) return undefined;
const key = raw.toLowerCase();
if (["off", "false", "no", "0"].includes(key)) return "off";
if (["on", "true", "yes", "1"].includes(key)) return "on";
return undefined;
}

View File

@@ -32,6 +32,7 @@ export type SessionStatus = {
age: number | null; age: number | null;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
elevatedLevel?: string;
systemSent?: boolean; systemSent?: boolean;
abortedLastRun?: boolean; abortedLastRun?: boolean;
inputTokens?: number; inputTokens?: number;
@@ -108,6 +109,7 @@ export async function getStatusSummary(): Promise<StatusSummary> {
age, age,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
elevatedLevel: entry?.elevatedLevel,
systemSent: entry?.systemSent, systemSent: entry?.systemSent,
abortedLastRun: entry?.abortedLastRun, abortedLastRun: entry?.abortedLastRun,
inputTokens: entry?.inputTokens, inputTokens: entry?.inputTokens,
@@ -194,6 +196,9 @@ const buildFlags = (entry: SessionEntry): string[] => {
const verbose = entry?.verboseLevel; const verbose = entry?.verboseLevel;
if (typeof verbose === "string" && verbose.length > 0) if (typeof verbose === "string" && verbose.length > 0)
flags.push(`verbose:${verbose}`); flags.push(`verbose:${verbose}`);
const elevated = entry?.elevatedLevel;
if (typeof elevated === "string" && elevated.length > 0)
flags.push(`elevated:${elevated}`);
if (entry?.systemSent) flags.push("system"); if (entry?.systemSent) flags.push("system");
if (entry?.abortedLastRun) flags.push("aborted"); if (entry?.abortedLastRun) flags.push("aborted");
const sessionId = entry?.sessionId as unknown; const sessionId = entry?.sessionId as unknown;

View File

@@ -79,6 +79,15 @@ export type WebConfig = {
reconnect?: WebReconnectConfig; reconnect?: WebReconnectConfig;
}; };
export type AgentElevatedAllowFromConfig = {
whatsapp?: string[];
telegram?: Array<string | number>;
discord?: Array<string | number>;
signal?: Array<string | number>;
imessage?: Array<string | number>;
webchat?: Array<string | number>;
};
export type WhatsAppConfig = { export type WhatsAppConfig = {
/** Optional allowlist for WhatsApp direct chats (E.164). */ /** Optional allowlist for WhatsApp direct chats (E.164). */
allowFrom?: string[]; allowFrom?: string[];
@@ -619,6 +628,8 @@ export type ClawdisConfig = {
thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high"; thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high";
/** Default verbose level when no /verbose directive is present. */ /** Default verbose level when no /verbose directive is present. */
verboseDefault?: "off" | "on"; verboseDefault?: "off" | "on";
/** Default elevated level when no /elevated directive is present. */
elevatedDefault?: "off" | "on";
/** Default block streaming level when no override is present. */ /** Default block streaming level when no override is present. */
blockStreamingDefault?: "off" | "on"; blockStreamingDefault?: "off" | "on";
/** /**
@@ -668,6 +679,13 @@ export type ClawdisConfig = {
/** How long to keep finished sessions in memory (ms). */ /** How long to keep finished sessions in memory (ms). */
cleanupMs?: number; cleanupMs?: number;
}; };
/** Elevated bash permissions for the host machine. */
elevated?: {
/** Enable or disable elevated mode (default: true). */
enabled?: boolean;
/** Approved senders for /elevated (per-surface allowlists). */
allowFrom?: AgentElevatedAllowFromConfig;
};
/** Optional sandbox settings for non-main sessions. */ /** Optional sandbox settings for non-main sessions. */
sandbox?: { sandbox?: {
/** Enable sandboxing for sessions. */ /** Enable sandboxing for sessions. */
@@ -1149,6 +1167,7 @@ export const ClawdisSchema = z.object({
]) ])
.optional(), .optional(),
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(), verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
elevatedDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
blockStreamingDefault: z blockStreamingDefault: z
.union([z.literal("off"), z.literal("on")]) .union([z.literal("off"), z.literal("on")])
.optional(), .optional(),
@@ -1180,6 +1199,21 @@ export const ClawdisSchema = z.object({
cleanupMs: z.number().int().positive().optional(), cleanupMs: z.number().int().positive().optional(),
}) })
.optional(), .optional(),
elevated: z
.object({
enabled: z.boolean().optional(),
allowFrom: z
.object({
whatsapp: z.array(z.string()).optional(),
telegram: z.array(z.union([z.string(), z.number()])).optional(),
discord: z.array(z.union([z.string(), z.number()])).optional(),
signal: z.array(z.union([z.string(), z.number()])).optional(),
imessage: z.array(z.union([z.string(), z.number()])).optional(),
webchat: z.array(z.union([z.string(), z.number()])).optional(),
})
.optional(),
})
.optional(),
sandbox: z sandbox: z
.object({ .object({
mode: z mode: z

View File

@@ -30,6 +30,7 @@ export type SessionEntry = {
chatType?: SessionChatType; chatType?: SessionChatType;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
elevatedLevel?: string;
providerOverride?: string; providerOverride?: string;
modelOverride?: string; modelOverride?: string;
groupActivation?: "mention" | "always"; groupActivation?: "mention" | "always";

View File

@@ -309,6 +309,7 @@ export const SessionsPatchParamsSchema = Type.Object(
key: NonEmptyString, key: NonEmptyString,
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
sendPolicy: Type.Optional( sendPolicy: Type.Optional(
Type.Union([Type.Literal("allow"), Type.Literal("deny"), Type.Null()]), Type.Union([Type.Literal("allow"), Type.Literal("deny"), Type.Null()]),

View File

@@ -18,6 +18,7 @@ import {
} from "../agents/pi-embedded.js"; } from "../agents/pi-embedded.js";
import { normalizeGroupActivation } from "../auto-reply/group-activation.js"; import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
import { import {
normalizeElevatedLevel,
normalizeThinkLevel, normalizeThinkLevel,
normalizeVerboseLevel, normalizeVerboseLevel,
} from "../auto-reply/thinking.js"; } from "../auto-reply/thinking.js";
@@ -384,6 +385,25 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
} }
} }
if ("elevatedLevel" in p) {
const raw = p.elevatedLevel;
if (raw === null) {
delete next.elevatedLevel;
} else if (raw !== undefined) {
const normalized = normalizeElevatedLevel(String(raw));
if (!normalized) {
return {
ok: false,
error: {
code: ErrorCodes.INVALID_REQUEST,
message: `invalid elevatedLevel: ${String(raw)}`,
},
};
}
next.elevatedLevel = normalized;
}
}
if ("model" in p) { if ("model" in p) {
const raw = p.model; const raw = p.model;
if (raw === null) { if (raw === null) {

View File

@@ -36,6 +36,7 @@ export type GatewaySessionRow = {
abortedLastRun?: boolean; abortedLastRun?: boolean;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
elevatedLevel?: string;
sendPolicy?: "allow" | "deny"; sendPolicy?: "allow" | "deny";
inputTokens?: number; inputTokens?: number;
outputTokens?: number; outputTokens?: number;
@@ -276,6 +277,7 @@ export function listSessionsFromStore(params: {
abortedLastRun: entry?.abortedLastRun, abortedLastRun: entry?.abortedLastRun,
thinkingLevel: entry?.thinkingLevel, thinkingLevel: entry?.thinkingLevel,
verboseLevel: entry?.verboseLevel, verboseLevel: entry?.verboseLevel,
elevatedLevel: entry?.elevatedLevel,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
inputTokens: entry?.inputTokens, inputTokens: entry?.inputTokens,
outputTokens: entry?.outputTokens, outputTokens: entry?.outputTokens,

View File

@@ -2,6 +2,7 @@ import type { SlashCommand } from "@mariozechner/pi-tui";
const THINK_LEVELS = ["off", "minimal", "low", "medium", "high"]; const THINK_LEVELS = ["off", "minimal", "low", "medium", "high"];
const VERBOSE_LEVELS = ["on", "off"]; const VERBOSE_LEVELS = ["on", "off"];
const ELEVATED_LEVELS = ["on", "off"];
const ACTIVATION_LEVELS = ["mention", "always"]; const ACTIVATION_LEVELS = ["mention", "always"];
const TOGGLE = ["on", "off"]; const TOGGLE = ["on", "off"];
@@ -44,6 +45,14 @@ export function getSlashCommands(): SlashCommand[] {
(value) => ({ value, label: value }), (value) => ({ value, label: value }),
), ),
}, },
{
name: "elevated",
description: "Set elevated on/off",
getArgumentCompletions: (prefix) =>
ELEVATED_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map(
(value) => ({ value, label: value }),
),
},
{ {
name: "activation", name: "activation",
description: "Set group activation", description: "Set group activation",
@@ -78,6 +87,7 @@ export function helpText(): string {
"/model <provider/model> (or /models)", "/model <provider/model> (or /models)",
"/think <off|minimal|low|medium|high>", "/think <off|minimal|low|medium|high>",
"/verbose <on|off>", "/verbose <on|off>",
"/elevated <on|off>",
"/activation <mention|always>", "/activation <mention|always>",
"/deliver <on|off>", "/deliver <on|off>",
"/new or /reset", "/new or /reset",

View File

@@ -586,6 +586,22 @@ export async function runTui(opts: TuiOptions) {
chatLog.addSystem(`verbose failed: ${String(err)}`); chatLog.addSystem(`verbose failed: ${String(err)}`);
} }
break; break;
case "elevated":
if (!args) {
chatLog.addSystem("usage: /elevated <on|off>");
break;
}
try {
await client.patchSession({
key: currentSessionKey,
elevatedLevel: args,
});
chatLog.addSystem(`elevated set to ${args}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`elevated failed: ${String(err)}`);
}
break;
case "activation": case "activation":
if (!args) { if (!args) {
chatLog.addSystem("usage: /activation <mention|always>"); chatLog.addSystem("usage: /activation <mention|always>");

View File

@@ -194,6 +194,7 @@ export type GatewaySessionRow = {
abortedLastRun?: boolean; abortedLastRun?: boolean;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
elevatedLevel?: string;
inputTokens?: number; inputTokens?: number;
outputTokens?: number; outputTokens?: number;
totalTokens?: number; totalTokens?: number;
@@ -218,6 +219,7 @@ export type SessionsPatchResult = {
updatedAt?: number; updatedAt?: number;
thinkingLevel?: string; thinkingLevel?: string;
verboseLevel?: string; verboseLevel?: string;
elevatedLevel?: string;
}; };
}; };