diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 3125780b4..2f931e3ec 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -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) diff --git a/docs/tools/index.md b/docs/tools/index.md index cd256cc5a..1835a74ac 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -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. diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index 6b306b798..b426fffc3 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -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. diff --git a/src/agents/clawdbot-gateway-tool.test.ts b/src/agents/clawdbot-gateway-tool.test.ts index e9ac32622..7bdc9f5b4 100644 --- a/src/agents/clawdbot-gateway-tool.test.ts +++ b/src/agents/clawdbot-gateway-tool.test.ts @@ -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"); diff --git a/src/agents/clawdbot-tools.ts b/src/agents/clawdbot-tools.ts index 0d6888627..40f647f16 100644 --- a/src/agents/clawdbot-tools.ts +++ b/src/agents/clawdbot-tools.ts @@ -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, diff --git a/src/agents/tools/gateway-tool.ts b/src/agents/tools/gateway-tool.ts index 552d7e23e..036e91d63 100644 --- a/src/agents/tools/gateway-tool.ts +++ b/src/agents/tools/gateway-tool.ts @@ -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; 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) diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 03872bc6d..e801ace34 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -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"), diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index 31ef07b86..f955525bb 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -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" }); diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index f3408e4d5..c7930c7ae 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -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 | /verbose on|off | /reasoning on|off | /elevated on|off | /model | /cost on|off", ].join("\n"); } diff --git a/src/config/schema.ts b/src/config/schema.ts index 7c20fc02d..34906f28a 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -98,6 +98,7 @@ const FIELD_LABELS: Record = { "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 = { "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": diff --git a/src/config/types.ts b/src/config/types.ts index 0aa90d22d..a4c7bae17 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -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; }; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index cd1899548..9045d7b3c 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -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();