Auto-reply: add host-only /bash + ! bash command
This commit is contained in:
@@ -251,6 +251,13 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
||||
textAlias: "/queue",
|
||||
acceptsArgs: true,
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "bash",
|
||||
description: "Run host shell commands (host-only).",
|
||||
textAlias: "/bash",
|
||||
scope: "text",
|
||||
acceptsArgs: true,
|
||||
}),
|
||||
];
|
||||
|
||||
registerAlias(commands, "status", "/usage");
|
||||
@@ -314,6 +321,7 @@ export function isCommandEnabled(
|
||||
): boolean {
|
||||
if (commandKey === "config") return cfg.commands?.config === true;
|
||||
if (commandKey === "debug") return cfg.commands?.debug === true;
|
||||
if (commandKey === "bash") return cfg.commands?.bash === true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -976,6 +976,11 @@ export async function getReplyFromConfig(
|
||||
command: inlineCommandContext,
|
||||
agentId,
|
||||
directives,
|
||||
elevated: {
|
||||
enabled: elevatedEnabled,
|
||||
allowed: elevatedAllowed,
|
||||
failures: elevatedFailures,
|
||||
},
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -1033,6 +1038,11 @@ export async function getReplyFromConfig(
|
||||
command,
|
||||
agentId,
|
||||
directives,
|
||||
elevated: {
|
||||
enabled: elevatedEnabled,
|
||||
allowed: elevatedAllowed,
|
||||
failures: elevatedFailures,
|
||||
},
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
|
||||
416
src/auto-reply/reply/bash-command.ts
Normal file
416
src/auto-reply/reply/bash-command.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import {
|
||||
getFinishedSession,
|
||||
getSession,
|
||||
markExited,
|
||||
} from "../../agents/bash-process-registry.js";
|
||||
import { createExecTool } from "../../agents/bash-tools.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import { killProcessTree } from "../../agents/shell-utils.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
|
||||
const CHAT_BASH_SCOPE_KEY = "chat:bash";
|
||||
const DEFAULT_FOREGROUND_MS = 2000;
|
||||
const MAX_FOREGROUND_MS = 30_000;
|
||||
|
||||
type BashRequest =
|
||||
| { action: "help" }
|
||||
| { action: "run"; command: string }
|
||||
| { action: "poll"; sessionId?: string }
|
||||
| { action: "stop"; sessionId?: string };
|
||||
|
||||
type ActiveBashJob =
|
||||
| { state: "starting"; startedAt: number; command: string }
|
||||
| {
|
||||
state: "running";
|
||||
sessionId: string;
|
||||
startedAt: number;
|
||||
command: string;
|
||||
watcherAttached: boolean;
|
||||
};
|
||||
|
||||
let activeJob: ActiveBashJob | null = null;
|
||||
|
||||
function clampNumber(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function resolveForegroundMs(cfg: ClawdbotConfig): number {
|
||||
const raw = cfg.commands?.bashForegroundMs;
|
||||
if (typeof raw !== "number" || Number.isNaN(raw))
|
||||
return DEFAULT_FOREGROUND_MS;
|
||||
return clampNumber(Math.floor(raw), 0, MAX_FOREGROUND_MS);
|
||||
}
|
||||
|
||||
function formatSessionSnippet(sessionId: string) {
|
||||
const trimmed = sessionId.trim();
|
||||
if (trimmed.length <= 12) return trimmed;
|
||||
return `${trimmed.slice(0, 8)}…`;
|
||||
}
|
||||
|
||||
function formatOutputBlock(text: string) {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return "(no output)";
|
||||
return `\`\`\`txt\n${trimmed}\n\`\`\``;
|
||||
}
|
||||
|
||||
function parseBashRequest(raw: string): BashRequest | null {
|
||||
const trimmed = raw.trimStart();
|
||||
let restSource = "";
|
||||
if (trimmed.toLowerCase().startsWith("/bash")) {
|
||||
const match = trimmed.match(/^\/bash(?:\s*:\s*|\s+|$)([\s\S]*)$/i);
|
||||
if (!match) return null;
|
||||
restSource = match[1] ?? "";
|
||||
} else if (trimmed.startsWith("!")) {
|
||||
restSource = trimmed.slice(1);
|
||||
if (restSource.trimStart().startsWith(":")) {
|
||||
restSource = restSource.trimStart().slice(1);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rest = restSource.trimStart();
|
||||
if (!rest) return { action: "help" };
|
||||
const tokenMatch = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
||||
const token = tokenMatch?.[1]?.trim() ?? "";
|
||||
const remainder = tokenMatch?.[2]?.trim() ?? "";
|
||||
const lowered = token.toLowerCase();
|
||||
if (lowered === "poll") {
|
||||
return { action: "poll", sessionId: remainder || undefined };
|
||||
}
|
||||
if (lowered === "stop") {
|
||||
return { action: "stop", sessionId: remainder || undefined };
|
||||
}
|
||||
if (lowered === "help") {
|
||||
return { action: "help" };
|
||||
}
|
||||
return { action: "run", command: rest };
|
||||
}
|
||||
|
||||
function resolveRawCommandBody(params: {
|
||||
ctx: MsgContext;
|
||||
cfg: ClawdbotConfig;
|
||||
agentId?: string;
|
||||
isGroup: boolean;
|
||||
}) {
|
||||
const source =
|
||||
params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body ?? "";
|
||||
const stripped = stripStructuralPrefixes(source);
|
||||
return params.isGroup
|
||||
? stripMentions(stripped, params.ctx, params.cfg, params.agentId)
|
||||
: stripped;
|
||||
}
|
||||
|
||||
function getScopedSession(sessionId: string) {
|
||||
const running = getSession(sessionId);
|
||||
if (running && running.scopeKey === CHAT_BASH_SCOPE_KEY) return { running };
|
||||
const finished = getFinishedSession(sessionId);
|
||||
if (finished && finished.scopeKey === CHAT_BASH_SCOPE_KEY)
|
||||
return { finished };
|
||||
return {};
|
||||
}
|
||||
|
||||
function ensureActiveJobState() {
|
||||
if (!activeJob) return null;
|
||||
if (activeJob.state === "starting") return activeJob;
|
||||
const { running, finished } = getScopedSession(activeJob.sessionId);
|
||||
if (running) return activeJob;
|
||||
if (finished) {
|
||||
activeJob = null;
|
||||
return null;
|
||||
}
|
||||
activeJob = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function attachActiveWatcher(sessionId: string) {
|
||||
if (!activeJob || activeJob.state !== "running") return;
|
||||
if (activeJob.sessionId !== sessionId) return;
|
||||
if (activeJob.watcherAttached) return;
|
||||
const { running } = getScopedSession(sessionId);
|
||||
const child = running?.child;
|
||||
if (!child) return;
|
||||
activeJob.watcherAttached = true;
|
||||
child.once("close", () => {
|
||||
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
|
||||
activeJob = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildUsageReply(): ReplyPayload {
|
||||
return {
|
||||
text: [
|
||||
"⚙️ Usage:",
|
||||
"- ! <command>",
|
||||
"- !poll | ! poll",
|
||||
"- !stop | ! stop",
|
||||
"- /bash ... (alias; same subcommands as !)",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function formatElevatedUnavailableMessage(params: {
|
||||
runtimeSandboxed: boolean;
|
||||
failures: Array<{ gate: string; key: string }>;
|
||||
sessionKey?: string;
|
||||
}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(
|
||||
`elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
|
||||
);
|
||||
if (params.failures.length > 0) {
|
||||
lines.push(
|
||||
`Failing gates: ${params.failures
|
||||
.map((f) => `${f.gate} (${f.key})`)
|
||||
.join(", ")}`,
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>).",
|
||||
);
|
||||
}
|
||||
lines.push("Fix-it keys:");
|
||||
lines.push("- tools.elevated.enabled");
|
||||
lines.push("- tools.elevated.allowFrom.<provider>");
|
||||
lines.push("- agents.list[].tools.elevated.enabled");
|
||||
lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
|
||||
if (params.sessionKey) {
|
||||
lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function handleBashChatCommand(params: {
|
||||
ctx: MsgContext;
|
||||
cfg: ClawdbotConfig;
|
||||
agentId?: string;
|
||||
sessionKey: string;
|
||||
isGroup: boolean;
|
||||
elevated: {
|
||||
enabled: boolean;
|
||||
allowed: boolean;
|
||||
failures: Array<{ gate: string; key: string }>;
|
||||
};
|
||||
}): Promise<ReplyPayload> {
|
||||
if (params.cfg.commands?.bash !== true) {
|
||||
return {
|
||||
text: "⚠️ bash is disabled. Set commands.bash=true to enable.",
|
||||
};
|
||||
}
|
||||
|
||||
const agentId =
|
||||
params.agentId ??
|
||||
resolveSessionAgentId({
|
||||
sessionKey: params.sessionKey,
|
||||
config: params.cfg,
|
||||
});
|
||||
|
||||
if (!params.elevated.enabled || !params.elevated.allowed) {
|
||||
const runtimeSandboxed = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.ctx.SessionKey,
|
||||
}).sandboxed;
|
||||
return {
|
||||
text: formatElevatedUnavailableMessage({
|
||||
runtimeSandboxed,
|
||||
failures: params.elevated.failures,
|
||||
sessionKey: params.ctx.SessionKey,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const rawBody = resolveRawCommandBody({
|
||||
ctx: params.ctx,
|
||||
cfg: params.cfg,
|
||||
agentId,
|
||||
isGroup: params.isGroup,
|
||||
}).trim();
|
||||
const request = parseBashRequest(rawBody);
|
||||
if (!request) {
|
||||
return { text: "⚠️ Unrecognized bash request." };
|
||||
}
|
||||
|
||||
const liveJob = ensureActiveJobState();
|
||||
|
||||
if (request.action === "help") {
|
||||
return buildUsageReply();
|
||||
}
|
||||
|
||||
if (request.action === "poll") {
|
||||
const sessionId =
|
||||
request.sessionId?.trim() ||
|
||||
(liveJob?.state === "running" ? liveJob.sessionId : "");
|
||||
if (!sessionId) {
|
||||
return { text: "⚙️ No active bash job." };
|
||||
}
|
||||
const { running, finished } = getScopedSession(sessionId);
|
||||
if (running) {
|
||||
attachActiveWatcher(sessionId);
|
||||
const runtimeSec = Math.max(
|
||||
0,
|
||||
Math.floor((Date.now() - running.startedAt) / 1000),
|
||||
);
|
||||
const tail = running.tail || "(no output yet)";
|
||||
return {
|
||||
text: [
|
||||
`⚙️ bash still running (session ${formatSessionSnippet(sessionId)}, ${runtimeSec}s).`,
|
||||
formatOutputBlock(tail),
|
||||
"Hint: !stop (or /bash stop)",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
if (finished) {
|
||||
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
|
||||
activeJob = null;
|
||||
}
|
||||
const exitLabel = finished.exitSignal
|
||||
? `signal ${String(finished.exitSignal)}`
|
||||
: `code ${String(finished.exitCode ?? 0)}`;
|
||||
const prefix = finished.status === "completed" ? "⚙️" : "⚠️";
|
||||
return {
|
||||
text: [
|
||||
`${prefix} bash finished (session ${formatSessionSnippet(sessionId)}).`,
|
||||
`Exit: ${exitLabel}`,
|
||||
formatOutputBlock(finished.aggregated || finished.tail),
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
|
||||
activeJob = null;
|
||||
}
|
||||
return {
|
||||
text: `⚙️ No bash session found for ${formatSessionSnippet(sessionId)}.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (request.action === "stop") {
|
||||
const sessionId =
|
||||
request.sessionId?.trim() ||
|
||||
(liveJob?.state === "running" ? liveJob.sessionId : "");
|
||||
if (!sessionId) {
|
||||
return { text: "⚙️ No active bash job." };
|
||||
}
|
||||
const { running } = getScopedSession(sessionId);
|
||||
if (!running) {
|
||||
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
|
||||
activeJob = null;
|
||||
}
|
||||
return {
|
||||
text: `⚙️ No running bash job found for ${formatSessionSnippet(sessionId)}.`,
|
||||
};
|
||||
}
|
||||
if (!running.backgrounded) {
|
||||
return {
|
||||
text: `⚠️ Session ${formatSessionSnippet(sessionId)} is not backgrounded.`,
|
||||
};
|
||||
}
|
||||
const pid = running.pid ?? running.child?.pid;
|
||||
if (pid) {
|
||||
killProcessTree(pid);
|
||||
}
|
||||
markExited(running, null, "SIGKILL", "failed");
|
||||
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
|
||||
activeJob = null;
|
||||
}
|
||||
return {
|
||||
text: `⚙️ bash stopped (session ${formatSessionSnippet(sessionId)}).`,
|
||||
};
|
||||
}
|
||||
|
||||
// request.action === "run"
|
||||
if (liveJob) {
|
||||
const label =
|
||||
liveJob.state === "running"
|
||||
? formatSessionSnippet(liveJob.sessionId)
|
||||
: "starting";
|
||||
return {
|
||||
text: `⚠️ A bash job is already running (${label}). Use !poll / !stop (or /bash poll / /bash stop).`,
|
||||
};
|
||||
}
|
||||
|
||||
const commandText = request.command.trim();
|
||||
if (!commandText) return buildUsageReply();
|
||||
|
||||
activeJob = {
|
||||
state: "starting",
|
||||
startedAt: Date.now(),
|
||||
command: commandText,
|
||||
};
|
||||
|
||||
try {
|
||||
const foregroundMs = resolveForegroundMs(params.cfg);
|
||||
const shouldBackgroundImmediately = foregroundMs <= 0;
|
||||
const timeoutSec =
|
||||
params.cfg.tools?.exec?.timeoutSec ?? params.cfg.tools?.bash?.timeoutSec;
|
||||
const execTool = createExecTool({
|
||||
scopeKey: CHAT_BASH_SCOPE_KEY,
|
||||
allowBackground: true,
|
||||
timeoutSec,
|
||||
elevated: {
|
||||
enabled: params.elevated.enabled,
|
||||
allowed: params.elevated.allowed,
|
||||
defaultLevel: "on",
|
||||
},
|
||||
});
|
||||
const result = await execTool.execute("chat-bash", {
|
||||
command: commandText,
|
||||
background: shouldBackgroundImmediately,
|
||||
yieldMs: shouldBackgroundImmediately ? undefined : foregroundMs,
|
||||
timeout: timeoutSec,
|
||||
elevated: true,
|
||||
});
|
||||
|
||||
if (result.details?.status === "running") {
|
||||
const sessionId = result.details.sessionId;
|
||||
activeJob = {
|
||||
state: "running",
|
||||
sessionId,
|
||||
startedAt: result.details.startedAt,
|
||||
command: commandText,
|
||||
watcherAttached: false,
|
||||
};
|
||||
attachActiveWatcher(sessionId);
|
||||
const snippet = formatSessionSnippet(sessionId);
|
||||
logVerbose(`Started bash session ${snippet}: ${commandText}`);
|
||||
return {
|
||||
text: `⚙️ bash started (session ${sessionId}). Still running; use !poll / !stop (or /bash poll / /bash stop).`,
|
||||
};
|
||||
}
|
||||
|
||||
// Completed in foreground.
|
||||
activeJob = null;
|
||||
const exitCode =
|
||||
result.details?.status === "completed" ? result.details.exitCode : 0;
|
||||
const output =
|
||||
result.details?.status === "completed"
|
||||
? result.details.aggregated
|
||||
: result.content
|
||||
.map((chunk) => (chunk.type === "text" ? chunk.text : ""))
|
||||
.join("\n");
|
||||
return {
|
||||
text: [
|
||||
`⚙️ bash: ${commandText}`,
|
||||
`Exit: ${exitCode}`,
|
||||
formatOutputBlock(output || "(no output)"),
|
||||
].join("\n"),
|
||||
};
|
||||
} catch (err) {
|
||||
activeJob = null;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
text: [`⚠️ bash failed: ${commandText}`, formatOutputBlock(message)].join(
|
||||
"\n",
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function resetBashChatCommandForTests() {
|
||||
activeJob = null;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { resetBashChatCommandForTests } from "./bash-command.js";
|
||||
import { buildCommandContext, handleCommands } from "./commands.js";
|
||||
import { parseInlineDirectives } from "./directive-handling.js";
|
||||
|
||||
@@ -33,6 +34,7 @@ function buildParams(
|
||||
cfg,
|
||||
command,
|
||||
directives: parseInlineDirectives(commandBody),
|
||||
elevated: { enabled: true, allowed: true, failures: [] },
|
||||
sessionKey: "agent:main:main",
|
||||
workspaceDir: "/tmp",
|
||||
defaultGroupActivation: () => "mention",
|
||||
@@ -47,6 +49,37 @@ function buildParams(
|
||||
}
|
||||
|
||||
describe("handleCommands gating", () => {
|
||||
it("blocks /bash when disabled", async () => {
|
||||
resetBashChatCommandForTests();
|
||||
const cfg = {
|
||||
commands: { bash: false, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("/bash echo hi", cfg);
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("bash is disabled");
|
||||
});
|
||||
|
||||
it("blocks /bash when elevated is not allowlisted", async () => {
|
||||
resetBashChatCommandForTests();
|
||||
const cfg = {
|
||||
commands: { bash: true, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("/bash echo hi", cfg);
|
||||
params.elevated = {
|
||||
enabled: true,
|
||||
allowed: false,
|
||||
failures: [
|
||||
{ gate: "allowFrom", key: "tools.elevated.allowFrom.whatsapp" },
|
||||
],
|
||||
};
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("elevated is not available");
|
||||
});
|
||||
|
||||
it("blocks /config when disabled", async () => {
|
||||
const cfg = {
|
||||
commands: { config: false, debug: false, text: true },
|
||||
@@ -70,6 +103,32 @@ describe("handleCommands gating", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleCommands bash alias", () => {
|
||||
it("routes !poll through the /bash handler", async () => {
|
||||
resetBashChatCommandForTests();
|
||||
const cfg = {
|
||||
commands: { bash: true, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("!poll", cfg);
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("No active bash job");
|
||||
});
|
||||
|
||||
it("routes !stop through the /bash handler", async () => {
|
||||
resetBashChatCommandForTests();
|
||||
const cfg = {
|
||||
commands: { bash: true, text: true },
|
||||
whatsapp: { allowFrom: ["*"] },
|
||||
} as ClawdbotConfig;
|
||||
const params = buildParams("!stop", cfg);
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("No active bash job");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleCommands identity", () => {
|
||||
it("returns sender details for /whoami", async () => {
|
||||
const cfg = {
|
||||
|
||||
@@ -83,6 +83,7 @@ import type {
|
||||
} from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { isAbortTrigger, setAbortMemory } from "./abort.js";
|
||||
import { handleBashChatCommand } from "./bash-command.js";
|
||||
import { parseConfigCommand } from "./config-commands.js";
|
||||
import { parseDebugCommand } from "./debug-commands.js";
|
||||
import type { InlineDirectives } from "./directive-handling.js";
|
||||
@@ -400,6 +401,11 @@ export async function handleCommands(params: {
|
||||
command: CommandContext;
|
||||
agentId?: string;
|
||||
directives: InlineDirectives;
|
||||
elevated: {
|
||||
enabled: boolean;
|
||||
allowed: boolean;
|
||||
failures: Array<{ gate: string; key: string }>;
|
||||
};
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey: string;
|
||||
@@ -425,6 +431,7 @@ export async function handleCommands(params: {
|
||||
cfg,
|
||||
command,
|
||||
directives,
|
||||
elevated,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -465,6 +472,30 @@ export async function handleCommands(params: {
|
||||
commandSource: ctx.CommandSource,
|
||||
});
|
||||
|
||||
const bashSlashRequested =
|
||||
allowTextCommands &&
|
||||
(command.commandBodyNormalized === "/bash" ||
|
||||
command.commandBodyNormalized.startsWith("/bash "));
|
||||
const bashBangRequested =
|
||||
allowTextCommands && command.commandBodyNormalized.startsWith("!");
|
||||
if (bashSlashRequested || (bashBangRequested && command.isAuthorizedSender)) {
|
||||
if (!command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
`Ignoring /bash from unauthorized sender: ${command.senderId || "<unknown>"}`,
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
const reply = await handleBashChatCommand({
|
||||
ctx,
|
||||
cfg,
|
||||
agentId: params.agentId,
|
||||
sessionKey,
|
||||
isGroup,
|
||||
elevated,
|
||||
});
|
||||
return { shouldContinue: false, reply };
|
||||
}
|
||||
|
||||
if (allowTextCommands && activationCommand.hasCommand) {
|
||||
if (!isGroup) {
|
||||
return {
|
||||
|
||||
@@ -153,6 +153,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.cliBackends": "CLI Backends",
|
||||
"commands.native": "Native Commands",
|
||||
"commands.text": "Text Commands",
|
||||
"commands.bash": "Allow Bash Chat Command",
|
||||
"commands.bashForegroundMs": "Bash Foreground Window (ms)",
|
||||
"commands.config": "Allow /config",
|
||||
"commands.debug": "Allow /debug",
|
||||
"commands.restart": "Allow Restart",
|
||||
@@ -287,6 +289,10 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"commands.native":
|
||||
"Register native commands with connectors that support it (Discord/Slack/Telegram).",
|
||||
"commands.text": "Allow text command parsing (slash commands only).",
|
||||
"commands.bash":
|
||||
"Allow bash chat command (`!`; `/bash` alias) to run host shell commands (default: false; requires tools.elevated).",
|
||||
"commands.bashForegroundMs":
|
||||
"How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately).",
|
||||
"commands.config":
|
||||
"Allow /config chat command to read/write config on disk (default: false).",
|
||||
"commands.debug":
|
||||
|
||||
@@ -1229,6 +1229,10 @@ export type CommandsConfig = {
|
||||
native?: NativeCommandsSetting;
|
||||
/** Enable text command parsing (default: true). */
|
||||
text?: boolean;
|
||||
/** Allow bash chat command (`!`; `/bash` alias) (default: false). */
|
||||
bash?: boolean;
|
||||
/** How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately). */
|
||||
bashForegroundMs?: number;
|
||||
/** Allow /config command (default: false). */
|
||||
config?: boolean;
|
||||
/** Allow /debug command (default: false). */
|
||||
|
||||
@@ -724,6 +724,8 @@ const CommandsSchema = z
|
||||
.object({
|
||||
native: NativeCommandsSettingSchema.optional().default("auto"),
|
||||
text: z.boolean().optional(),
|
||||
bash: z.boolean().optional(),
|
||||
bashForegroundMs: z.number().int().min(0).max(30_000).optional(),
|
||||
config: z.boolean().optional(),
|
||||
debug: z.boolean().optional(),
|
||||
restart: z.boolean().optional(),
|
||||
|
||||
Reference in New Issue
Block a user