diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 1665fdcc8..01a64361f 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -1981,7 +1981,7 @@ Per-agent override (further restrict): Notes: - `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow). -- `/elevated on|off` stores state per session key; inline directives apply to a single message. +- `/elevated on|off|ask|full` stores state per session key; inline directives apply to a single message. - Elevated `exec` runs on the host and bypasses sandboxing. - Tool policy still applies; if `exec` is denied, elevated cannot be used. diff --git a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md index 8c5fd19e8..d28481ebb 100644 --- a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md +++ b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @@ -91,7 +91,8 @@ Available groups: ## Elevated: exec-only “run on host” Elevated does **not** grant extra tools; it only affects `exec`. -- If you’re sandboxed, `/elevated on` (or `exec` with `elevated: true`) runs on the host. +- If you’re sandboxed, `/elevated on` (or `exec` with `elevated: true`) runs on the host (approvals may still apply). +- Use `/elevated full` to skip exec approvals for the session. - If you’re already running direct, elevated is effectively a no-op (still gated). - Elevated is **not** skill-scoped and does **not** override tool allow/deny. diff --git a/docs/refactor/exec-host.md b/docs/refactor/exec-host.md index c71a456ef..3b4e1a15c 100644 --- a/docs/refactor/exec-host.md +++ b/docs/refactor/exec-host.md @@ -216,7 +216,7 @@ Option B: ## Slash commands - `/exec host= security= ask= node=` - Per-agent, per-session overrides; non-persistent unless saved via config. -- `/elevated on|off` remains a shortcut for `host=gateway security=full`. +- `/elevated on|off|ask|full` remains a shortcut for `host=gateway security=full` (with `full` skipping approvals). ## Cross-platform story - The runner service is the portable execution target. diff --git a/docs/tools/elevated.md b/docs/tools/elevated.md index 2e74162c5..8b561b473 100644 --- a/docs/tools/elevated.md +++ b/docs/tools/elevated.md @@ -6,17 +6,20 @@ read_when: # Elevated Mode (/elevated directives) ## What it does -- `/elevated on` is a **shortcut** for `exec.host=gateway` + `exec.security=full`. +- `/elevated on` is a **shortcut** for `exec.host=gateway` + `exec.security=full` (approvals still apply). +- `/elevated full` runs on the gateway host **and** auto-approves exec (skips exec approvals). +- `/elevated ask` runs on the gateway host but keeps exec approvals (same as `/elevated on`). - Only changes behavior when the agent is **sandboxed** (otherwise exec already runs on the host). -- Directive forms: `/elevated on`, `/elevated off`, `/elev on`, `/elev off`. -- Only `on|off` are accepted; anything else returns a hint and does not change state. +- Directive forms: `/elevated on|off|ask|full`, `/elev on|off|ask|full`. +- Only `on|off|ask|full` are accepted; anything else returns a hint and does not change state. ## What it controls (and what it doesn’t) - **Availability gates**: `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can further restrict elevated per agent (both must allow). -- **Per-session state**: `/elevated on|off` sets the elevated level for the current session key. -- **Inline directive**: `/elevated on` inside a message applies to that message only. +- **Per-session state**: `/elevated on|off|ask|full` sets the elevated level for the current session key. +- **Inline directive**: `/elevated on|ask|full` inside a message applies to that message only. - **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. Command-only messages that bypass mention requirements are treated as mentioned. - **Host execution**: elevated forces `exec` onto the gateway host with full security. +- **Approvals**: `full` skips exec approvals; `on`/`ask` still honor them. - **Unsandboxed agents**: no-op for location; only affects gating, logging, and status. - **Tool policy still applies**: if `exec` is denied by tool policy, elevated cannot be used. @@ -26,8 +29,8 @@ read_when: 3. Global default (`agents.defaults.elevatedDefault` in config). ## Setting a session default -- Send a message that is **only** the directive (whitespace allowed), e.g. `/elevated on`. -- Confirmation reply is sent (`Elevated mode enabled.` / `Elevated mode disabled.`). +- Send a message that is **only** the directive (whitespace allowed), e.g. `/elevated full`. +- Confirmation reply is sent (`Elevated mode set to full...` / `Elevated mode disabled.`). - If elevated access is disabled or the sender is not on the approved allowlist, the directive replies with an actionable error and does not change session state. - Send `/elevated` (or `/elevated:`) with no argument to see the current elevated level. @@ -41,4 +44,4 @@ read_when: ## Logging + status - Elevated exec calls are logged at info level. -- Session status includes elevated mode (e.g. `elevated=on`). +- Session status includes elevated mode (e.g. `elevated=ask`, `elevated=full`). diff --git a/docs/tools/exec-approvals.md b/docs/tools/exec-approvals.md index 4bef999ae..59ac7d119 100644 --- a/docs/tools/exec-approvals.md +++ b/docs/tools/exec-approvals.md @@ -11,7 +11,7 @@ read_when: Exec approvals are the **companion app / node host guardrail** for letting a sandboxed agent run commands on a real host (`gateway` or `node`). Think of it like a safety interlock: commands are allowed only when policy + allowlist + (optional) user approval all agree. -Exec approvals are **in addition** to tool policy and elevated gating. +Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals). If the companion app UI is **not available**, any request that requires a prompt is resolved by the **ask fallback** (default: deny). diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index a0dfbf8c7..a96e1760f 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -78,7 +78,7 @@ Text + native (when enabled): - `/think ` (dynamic choices by model/provider; aliases: `/thinking`, `/t`) - `/verbose on|full|off` (alias: `/v`) - `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only) -- `/elevated on|off` (alias: `/elev`) +- `/elevated on|off|ask|full` (alias: `/elev`; `full` skips exec approvals) - `/exec host= security= ask= node=` (send `/exec` to show current) - `/model ` (alias: `/models`; or `/` from `agents.defaults.models.*.alias`) - `/queue ` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings) diff --git a/docs/tui.md b/docs/tui.md index 57fffa493..1c94aee1d 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -78,7 +78,7 @@ Session controls: - `/verbose ` - `/reasoning ` - `/usage ` -- `/elevated ` (alias: `/elev`) +- `/elevated ` (alias: `/elev`) - `/activation ` - `/deliver ` diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index 91b38dc3b..d7aaa218f 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -140,7 +140,7 @@ export type { BashSandboxConfig } from "./bash-tools.shared.js"; export type ExecElevatedDefaults = { enabled: boolean; allowed: boolean; - defaultLevel: "on" | "off"; + defaultLevel: "on" | "off" | "ask" | "full"; }; const execSchema = Type.Object({ @@ -706,12 +706,23 @@ export function createExecTool( : clampNumber(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000) : null; const elevatedDefaults = defaults?.elevated; - const elevatedDefaultOn = - elevatedDefaults?.defaultLevel === "on" && - elevatedDefaults.enabled && - elevatedDefaults.allowed; - const elevatedRequested = - typeof params.elevated === "boolean" ? params.elevated : elevatedDefaultOn; + const elevatedDefaultMode = + elevatedDefaults?.defaultLevel === "full" + ? "full" + : elevatedDefaults?.defaultLevel === "ask" + ? "ask" + : elevatedDefaults?.defaultLevel === "on" + ? "ask" + : "off"; + const elevatedMode = + typeof params.elevated === "boolean" + ? params.elevated + ? elevatedDefaultMode === "full" + ? "full" + : "ask" + : "off" + : elevatedDefaultMode; + const elevatedRequested = elevatedMode !== "off"; if (elevatedRequested) { if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) { const runtime = defaults?.sandbox ? "sandboxed" : "direct"; @@ -767,6 +778,10 @@ export function createExecTool( const configuredAsk = defaults?.ask ?? "on-miss"; const requestedAsk = normalizeExecAsk(params.ask); let ask = maxAsk(configuredAsk, requestedAsk ?? configuredAsk); + const bypassApprovals = elevatedRequested && elevatedMode === "full"; + if (bypassApprovals) { + ask = "off"; + } const sandbox = host === "sandbox" ? defaults?.sandbox : undefined; const rawWorkdir = params.workdir?.trim() || defaults?.cwd || process.cwd(); @@ -1031,7 +1046,7 @@ export function createExecTool( }; } - if (host === "gateway") { + if (host === "gateway" && !bypassApprovals) { const approvals = resolveExecApprovals(agentId, { security: "allowlist" }); const hostSecurity = minSecurity(security, approvals.agent.security); const hostAsk = maxAsk(ask, approvals.agent.ask); diff --git a/src/agents/pi-embedded-runner/types.ts b/src/agents/pi-embedded-runner/types.ts index a8aa3c48c..56380cd1d 100644 --- a/src/agents/pi-embedded-runner/types.ts +++ b/src/agents/pi-embedded-runner/types.ts @@ -76,6 +76,6 @@ export type EmbeddedSandboxInfo = { allowedControlPorts?: number[]; elevated?: { allowed: boolean; - defaultLevel: "on" | "off"; + defaultLevel: "on" | "off" | "ask" | "full"; }; }; diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 2f0e936e4..b5fe28556 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -322,7 +322,7 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("You are running in a sandboxed runtime"); expect(prompt).toContain("Sub-agents stay sandboxed"); - expect(prompt).toContain("User can toggle with /elevated on|off."); + expect(prompt).toContain("User can toggle with /elevated on|off|ask|full."); expect(prompt).toContain("Current elevated level: on"); }); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 772f154e4..6a20391c0 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -176,7 +176,7 @@ export function buildAgentSystemPrompt(params: { allowedControlPorts?: number[]; elevated?: { allowed: boolean; - defaultLevel: "on" | "off"; + defaultLevel: "on" | "off" | "ask" | "full"; }; }; /** Reaction guidance for the agent (for Telegram minimal/extensive modes). */ @@ -444,12 +444,14 @@ export function buildAgentSystemPrompt(params: { params.sandboxInfo.elevated?.allowed ? "Elevated exec is available for this session." : "", - params.sandboxInfo.elevated?.allowed ? "User can toggle with /elevated on|off." : "", params.sandboxInfo.elevated?.allowed - ? "You may also send /elevated on|off when needed." + ? "User can toggle with /elevated on|off|ask|full." : "", params.sandboxInfo.elevated?.allowed - ? `Current elevated level: ${params.sandboxInfo.elevated.defaultLevel} (on runs exec on host; off runs in sandbox).` + ? "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).` : "", ] .filter(Boolean) diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 4ce176b1d..7e6d76399 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -395,9 +395,9 @@ function buildChatCommands(): ChatCommandDefinition[] { args: [ { name: "mode", - description: "on or off", + description: "on, off, ask, or full", type: "string", - choices: ["on", "off"], + choices: ["on", "off", "ask", "full"], }, ], argsMenu: "auto", diff --git a/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts b/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts index b636b85d6..1997ebe3b 100644 --- a/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts @@ -219,7 +219,7 @@ describe("directive behavior", () => { ); const events = drainSystemEvents(MAIN_SESSION_KEY); - expect(events.some((e) => e.includes("Elevated ON"))).toBe(true); + expect(events.some((e) => e.includes("Elevated ASK"))).toBe(true); }); }); it("queues a system event when toggling reasoning", async () => { diff --git a/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts b/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts index bf0ac3df2..abf7eb0c1 100644 --- a/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts @@ -150,7 +150,7 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Elevated mode enabled"); + expect(text).toContain("Elevated mode set to ask"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts index 3ff09e217..4fb307f0d 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts @@ -143,7 +143,7 @@ describe("directive behavior", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Current elevated level: on"); - expect(text).toContain("Options: on, off."); + expect(text).toContain("Options: on, off, ask, full."); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply.directive.parse.test.ts b/src/auto-reply/reply.directive.parse.test.ts index 72f22262f..545c5e169 100644 --- a/src/auto-reply/reply.directive.parse.test.ts +++ b/src/auto-reply/reply.directive.parse.test.ts @@ -55,6 +55,16 @@ describe("directive parsing", () => { expect(res.hasDirective).toBe(true); expect(res.elevatedLevel).toBe("on"); }); + it("matches elevated ask", () => { + const res = extractElevatedDirective("/elevated ask please"); + expect(res.hasDirective).toBe(true); + expect(res.elevatedLevel).toBe("ask"); + }); + it("matches elevated full", () => { + const res = extractElevatedDirective("/elevated full please"); + expect(res.hasDirective).toBe(true); + expect(res.elevatedLevel).toBe("full"); + }); it("matches think at start of line", () => { const res = extractThinkDirective("/think:high run slow"); diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts index 193172535..43cc0e5b2 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts @@ -129,7 +129,7 @@ describe("trigger handling", () => { cfg, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Elevated mode enabled"); + expect(text).toContain("Elevated mode set to ask"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); const store = JSON.parse(storeRaw) as Record; @@ -223,7 +223,7 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); - expect(text).not.toContain("Elevated mode enabled"); + expect(text).not.toContain("Elevated mode set to ask"); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts index 8ace7a4bc..fe4479df1 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts @@ -184,7 +184,7 @@ describe("trigger handling", () => { cfg, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Elevated mode enabled"); + expect(text).toContain("Elevated mode set to ask"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); const store = JSON.parse(storeRaw) as Record; @@ -226,7 +226,7 @@ describe("trigger handling", () => { cfg, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Elevated mode enabled"); + expect(text).toContain("Elevated mode set to ask"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); const store = JSON.parse(storeRaw) as Record; diff --git a/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts b/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts index 0d6a0b303..183c4a67e 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts @@ -167,7 +167,7 @@ describe("trigger handling", () => { cfg, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Elevated mode enabled"); + expect(text).toContain("Elevated mode set to ask"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); const store = JSON.parse(storeRaw) as Record; diff --git a/src/auto-reply/reply/commands-context-report.ts b/src/auto-reply/reply/commands-context-report.ts index ac93e5fed..5cdc9f3d7 100644 --- a/src/auto-reply/reply/commands-context-report.ts +++ b/src/auto-reply/reply/commands-context-report.ts @@ -120,7 +120,7 @@ async function resolveContextReport( workspaceAccess: "rw" as const, elevated: { allowed: params.elevated.allowed, - defaultLevel: params.resolvedElevatedLevel === "off" ? ("off" as const) : ("on" as const), + defaultLevel: (params.resolvedElevatedLevel ?? "off") as "on" | "off" | "ask" | "full", }, } : { enabled: false }; diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index 33f19ee3f..7f056e6cd 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -205,7 +205,7 @@ export async function handleDirectiveOnly(params: { const level = currentElevatedLevel ?? "off"; return { text: [ - withOptions(`Current elevated level: ${level}.`, "on, off"), + withOptions(`Current elevated level: ${level}.`, "on, off, ask, full"), shouldHintDirectRuntime ? formatElevatedRuntimeHint() : null, ] .filter(Boolean) @@ -213,7 +213,7 @@ export async function handleDirectiveOnly(params: { }; } return { - text: `Unrecognized elevated level "${directives.rawElevatedLevel}". Valid levels: off, on.`, + text: `Unrecognized elevated level "${directives.rawElevatedLevel}". Valid levels: off, on, ask, full.`, }; } if (directives.hasElevatedDirective && (!elevatedEnabled || !elevatedAllowed)) { @@ -426,7 +426,9 @@ export async function handleDirectiveOnly(params: { parts.push( directives.elevatedLevel === "off" ? formatDirectiveAck("Elevated mode disabled.") - : formatDirectiveAck("Elevated mode enabled."), + : directives.elevatedLevel === "full" + ? formatDirectiveAck("Elevated mode set to full (auto-approve).") + : formatDirectiveAck("Elevated mode set to ask (approvals may still apply)."), ); if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint()); } diff --git a/src/auto-reply/reply/directive-handling.shared.ts b/src/auto-reply/reply/directive-handling.shared.ts index 961fe50a7..2fa4fd1ed 100644 --- a/src/auto-reply/reply/directive-handling.shared.ts +++ b/src/auto-reply/reply/directive-handling.shared.ts @@ -16,10 +16,15 @@ export const withOptions = (line: string, options: string) => export const formatElevatedRuntimeHint = () => `${SYSTEM_MARK} Runtime is direct; sandboxing does not apply.`; -export const formatElevatedEvent = (level: ElevatedLevel) => - level === "on" - ? "Elevated ON — exec runs on host; set elevated:false to stay sandboxed." - : "Elevated OFF — exec stays in sandbox."; +export const formatElevatedEvent = (level: ElevatedLevel) => { + if (level === "full") { + return "Elevated FULL — exec runs on host with auto-approval."; + } + if (level === "ask" || level === "on") { + return "Elevated ASK — exec runs on host; approvals may still apply."; + } + return "Elevated OFF — exec stays in sandbox."; +}; export const formatReasoningEvent = (level: ReasoningLevel) => { if (level === "stream") return "Reasoning STREAM — emit live ."; diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 2184c5f9a..eaf2d20a8 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -324,7 +324,12 @@ export function buildStatusMessage(args: StatusArgs): string { const queueDetails = formatQueueDetails(args.queue); const verboseLabel = verboseLevel === "full" ? "verbose:full" : verboseLevel === "on" ? "verbose" : null; - const elevatedLabel = elevatedLevel === "on" ? "elevated" : null; + const elevatedLabel = + elevatedLevel && elevatedLevel !== "off" + ? elevatedLevel === "on" + ? "elevated" + : `elevated:${elevatedLevel}` + : null; const optionParts = [ `Runtime: ${runtime.label}`, `Think: ${thinkLevel}`, @@ -395,7 +400,7 @@ export function buildHelpMessage(cfg?: ClawdbotConfig): string { "/think ", "/verbose on|full|off", "/reasoning on|off", - "/elevated on|off", + "/elevated on|off|ask|full", "/model ", "/usage off|tokens|full", ]; diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 6f9637dbd..aabb2cf17 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -1,6 +1,7 @@ export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; export type VerboseLevel = "off" | "on" | "full"; -export type ElevatedLevel = "off" | "on"; +export type ElevatedLevel = "off" | "on" | "ask" | "full"; +export type ElevatedMode = "off" | "ask" | "full"; export type ReasoningLevel = "off" | "on" | "stream"; export type UsageDisplayLevel = "off" | "tokens" | "full"; @@ -112,10 +113,18 @@ export function normalizeElevatedLevel(raw?: string | null): ElevatedLevel | und if (!raw) return undefined; const key = raw.toLowerCase(); if (["off", "false", "no", "0"].includes(key)) return "off"; + if (["full", "auto", "auto-approve", "autoapprove"].includes(key)) return "full"; + if (["ask", "prompt", "approval", "approve"].includes(key)) return "ask"; if (["on", "true", "yes", "1"].includes(key)) return "on"; return undefined; } +export function resolveElevatedMode(level?: ElevatedLevel | null): ElevatedMode { + if (!level || level === "off") return "off"; + if (level === "full") return "full"; + return "ask"; +} + // Normalize reasoning visibility flags used to toggle reasoning exposure. export function normalizeReasoningLevel(raw?: string | null): ReasoningLevel | undefined { if (!raw) return undefined; diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 46bd25d64..d4bac779c 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -136,7 +136,7 @@ export type AgentDefaultsConfig = { /** Default verbose level when no /verbose directive is present. */ verboseDefault?: "off" | "on" | "full"; /** Default elevated level when no /elevated directive is present. */ - elevatedDefault?: "off" | "on"; + elevatedDefault?: "off" | "on" | "ask" | "full"; /** Default block streaming level when no override is present. */ blockStreamingDefault?: "off" | "on"; /** diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index fd624bfe3..c4b8a8f2c 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -113,7 +113,9 @@ export const AgentDefaultsSchema = z ]) .optional(), verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(), - elevatedDefault: z.union([z.literal("off"), z.literal("on")]).optional(), + elevatedDefault: z + .union([z.literal("off"), z.literal("on"), z.literal("ask"), z.literal("full")]) + .optional(), blockStreamingDefault: z.union([z.literal("off"), z.literal("on")]).optional(), blockStreamingBreak: z.union([z.literal("text_end"), z.literal("message_end")]).optional(), blockStreamingChunk: BlockStreamingChunkSchema.optional(), diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index 1a3736971..1d34217b7 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -169,7 +169,7 @@ export async function applySessionsPatchToStore(params: { delete next.elevatedLevel; } else if (raw !== undefined) { const normalized = normalizeElevatedLevel(String(raw)); - if (!normalized) return invalid('invalid elevatedLevel (use "on"|"off")'); + if (!normalized) return invalid('invalid elevatedLevel (use "on"|"off"|"ask"|"full")'); // Persist "off" explicitly so patches can override defaults. next.elevatedLevel = normalized; } diff --git a/src/tui/commands.ts b/src/tui/commands.ts index b85049472..59806cfbd 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -3,7 +3,7 @@ import { formatThinkingLevels, listThinkingLevelLabels } from "../auto-reply/thi const VERBOSE_LEVELS = ["on", "off"]; const REASONING_LEVELS = ["on", "off"]; -const ELEVATED_LEVELS = ["on", "off"]; +const ELEVATED_LEVELS = ["on", "off", "ask", "full"]; const ACTIVATION_LEVELS = ["mention", "always"]; const USAGE_FOOTER_LEVELS = ["off", "tokens", "full"]; @@ -83,7 +83,7 @@ export function getSlashCommands(options: SlashCommandOptions = {}): SlashComman }, { name: "elevated", - description: "Set elevated on/off", + description: "Set elevated on/off/ask/full", getArgumentCompletions: (prefix) => ELEVATED_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map((value) => ({ value, @@ -130,8 +130,8 @@ export function helpText(options: SlashCommandOptions = {}): string { "/verbose ", "/reasoning ", "/usage ", - "/elevated ", - "/elev ", + "/elevated ", + "/elev ", "/activation ", "/new or /reset", "/abort", diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index 40584da0e..79765b5fc 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -371,7 +371,11 @@ export function createCommandHandlers(context: CommandHandlerContext) { } case "elevated": if (!args) { - chatLog.addSystem("usage: /elevated "); + chatLog.addSystem("usage: /elevated "); + break; + } + if (!["on", "off", "ask", "full"].includes(args)) { + chatLog.addSystem("usage: /elevated "); break; } try {