feat: add elevated bash mode
This commit is contained in:
@@ -16,6 +16,7 @@ Key parameters:
|
|||||||
- `yieldMs` (default 10000): auto‑background after this delay
|
- `yieldMs` (default 10000): auto‑background 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`
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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()]),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>");
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user