fix: disable restart by default

This commit is contained in:
Peter Steinberger
2026-01-09 05:49:11 +00:00
parent db22207014
commit 0a026fea1c
12 changed files with 53 additions and 6 deletions

View File

@@ -591,6 +591,7 @@ Controls how chat commands are enabled across connectors.
commands: {
native: false, // register native commands when supported
text: true, // parse slash commands in chat messages
restart: false, // allow /restart + gateway restart tool
useAccessGroups: true // enforce access-group allowlists/policies for commands
}
}
@@ -601,6 +602,7 @@ Notes:
- `commands.text: false` disables parsing chat messages for commands.
- `commands.native: true` registers native commands on supported connectors (Discord/Slack/Telegram). Platforms without native commands still rely on text commands.
- `commands.native: false` skips native registration; Discord/Telegram clear previously registered commands on startup. Slack commands are managed in the Slack app.
- `commands.restart: true` enables `/restart` and the gateway tool restart action.
- `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies.
### `web` (WhatsApp web provider)

View File

@@ -182,6 +182,7 @@ Core actions:
Notes:
- Use `delayMs` (defaults to 2000) to avoid interrupting an in-flight reply.
- `restart` is disabled by default; enable with `commands.restart: true`.
### `sessions_list` / `sessions_history` / `sessions_send` / `sessions_spawn`
List sessions, inspect transcript history, or send to another session.

View File

@@ -18,6 +18,7 @@ Directives (`/think`, `/verbose`, `/reasoning`, `/elevated`) are parsed even whe
commands: {
native: false,
text: true,
restart: false,
useAccessGroups: true
}
}
@@ -54,6 +55,7 @@ Text-only:
Notes:
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
- `/cost` appends per-response token usage; it only shows dollar cost when the model uses an API key (OAuth hides cost).
- `/restart` is disabled by default; set `commands.restart: true` to enable it.
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
- `/reasoning` (and `/verbose`) are risky in group settings: they may reveal internal reasoning or tool output you did not intend to expose. Prefer leaving them off, especially in group chats.

View File

@@ -12,9 +12,9 @@ describe("gateway tool", () => {
const kill = vi.spyOn(process, "kill").mockImplementation(() => true);
try {
const tool = createClawdbotTools().find(
(candidate) => candidate.name === "gateway",
);
const tool = createClawdbotTools({
config: { commands: { restart: true } },
}).find((candidate) => candidate.name === "gateway");
expect(tool).toBeDefined();
if (!tool) throw new Error("missing gateway tool");

View File

@@ -43,7 +43,10 @@ export function createClawdbotTools(options?: {
}),
createTelegramTool(),
createWhatsAppTool(),
createGatewayTool({ agentSessionKey: options?.agentSessionKey }),
createGatewayTool({
agentSessionKey: options?.agentSessionKey,
config: options?.config,
}),
createAgentsListTool({ agentSessionKey: options?.agentSessionKey }),
createSessionsListTool({
agentSessionKey: options?.agentSessionKey,

View File

@@ -1,5 +1,6 @@
import { Type } from "@sinclair/typebox";
import type { ClawdbotConfig } from "../../config/config.js";
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool } from "./gateway.js";
@@ -45,6 +46,7 @@ const GatewayToolSchema = Type.Union([
export function createGatewayTool(opts?: {
agentSessionKey?: string;
config?: ClawdbotConfig;
}): AnyAgentTool {
return {
label: "Gateway",
@@ -56,6 +58,11 @@ export function createGatewayTool(opts?: {
const params = args as Record<string, unknown>;
const action = readStringParam(params, "action", { required: true });
if (action === "restart") {
if (opts?.config?.commands?.restart !== true) {
throw new Error(
"Gateway restart is disabled. Set commands.restart=true to enable.",
);
}
const delayMs =
typeof params.delayMs === "number" && Number.isFinite(params.delayMs)
? Math.floor(params.delayMs)

View File

@@ -181,7 +181,7 @@ describe("trigger handling", () => {
});
});
it("restarts even with prefix/whitespace", async () => {
it("rejects /restart by default", async () => {
await withTempHome(async (home) => {
const res = await getReplyFromConfig(
{
@@ -193,6 +193,24 @@ describe("trigger handling", () => {
makeCfg(home),
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("/restart is disabled");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
});
});
it("restarts when enabled", async () => {
await withTempHome(async (home) => {
const cfg = { ...makeCfg(home), commands: { restart: true } };
const res = await getReplyFromConfig(
{
Body: "/restart",
From: "+1001",
To: "+2000",
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(
text?.startsWith("⚙️ Restarting") ||
text?.startsWith("⚠️ Restart failed"),

View File

@@ -504,6 +504,14 @@ export async function handleCommands(params: {
);
return { shouldContinue: false };
}
if (cfg.commands?.restart !== true) {
return {
shouldContinue: false,
reply: {
text: "⚠️ /restart is disabled. Set commands.restart=true to enable.",
},
};
}
const hasSigusr1Listener = process.listenerCount("SIGUSR1") > 0;
if (hasSigusr1Listener) {
scheduleGatewaySigusr1Restart({ reason: "/restart" });

View File

@@ -350,7 +350,7 @@ export function buildStatusMessage(args: StatusArgs): string {
export function buildHelpMessage(): string {
return [
" Help",
"Shortcuts: /new reset | /compact [instructions] | /restart relink",
"Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)",
"Options: /think <level> | /verbose on|off | /reasoning on|off | /elevated on|off | /model <id> | /cost on|off",
].join("\n");
}

View File

@@ -98,6 +98,7 @@ const FIELD_LABELS: Record<string, string> = {
"agent.imageModel.fallbacks": "Image Model Fallbacks",
"commands.native": "Native Commands",
"commands.text": "Text Commands",
"commands.restart": "Allow Restart",
"commands.useAccessGroups": "Use Access Groups",
"ui.seamColor": "Accent Color",
"browser.controlUrl": "Browser Control URL",
@@ -159,6 +160,8 @@ 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.restart":
"Allow /restart and gateway restart tool actions (default: false).",
"commands.useAccessGroups":
"Enforce access-group allowlists/policies for commands.",
"session.agentToAgent.maxPingPongTurns":

View File

@@ -789,6 +789,8 @@ export type CommandsConfig = {
native?: boolean;
/** Enable text command parsing (default: true). */
text?: boolean;
/** Allow restart commands/tools (default: false). */
restart?: boolean;
/** Enforce access-group allowlists/policies for commands (default: true). */
useAccessGroups?: boolean;
};

View File

@@ -520,6 +520,7 @@ const CommandsSchema = z
.object({
native: z.boolean().optional(),
text: z.boolean().optional(),
restart: z.boolean().optional(),
useAccessGroups: z.boolean().optional(),
})
.optional();