diff --git a/README.md b/README.md index 39f0978eb..ebb386032 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,7 @@ Debug endpoints are available at `/debug/*` when enabled (requires `DEBUG_ROUTES - `GET /debug/processes` - List all container processes - `GET /debug/logs?id=` - Get logs for a specific process - `GET /debug/version` - Get container and moltbot version info +- `GET /debug/cli?cmd=` - Run only `openclaw --help` or `openclaw --version`; defaults to help and returns 400 for other commands ## Optional: Chat Channels diff --git a/src/routes/debug.test.ts b/src/routes/debug.test.ts new file mode 100644 index 000000000..c600bf462 --- /dev/null +++ b/src/routes/debug.test.ts @@ -0,0 +1,96 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { Hono } from 'hono'; +import type { Sandbox } from '@cloudflare/sandbox'; +import type { AppEnv } from '../types'; +import { createMockEnv, createMockProcess } from '../test-utils'; + +const { findExistingGatewayProcess, handleScheduled, killGateway, waitForProcess } = vi.hoisted( + () => ({ + findExistingGatewayProcess: vi.fn(), + handleScheduled: vi.fn(), + killGateway: vi.fn(), + waitForProcess: vi.fn(), + }), +); + +vi.mock('../gateway', () => ({ + findExistingGatewayProcess, + killGateway, + waitForProcess, +})); + +vi.mock('../cron/handler', () => ({ handleScheduled })); + +import { debug } from './debug'; + +afterEach(() => { + vi.clearAllMocks(); +}); + +function appFor(sandbox: Sandbox): Hono { + const app = new Hono(); + app.use('*', async (c, next) => { + c.set('sandbox', sandbox); + await next(); + }); + app.route('/debug', debug); + return app; +} + +function sandboxForCli() { + const startProcess = vi.fn().mockResolvedValue(createMockProcess('command output')); + return { + sandbox: { startProcess } as unknown as Sandbox, + startProcess, + }; +} + +describe('GET /debug/cli', () => { + it.each([ + ['missing cmd', '/debug/cli'], + ['empty cmd', '/debug/cli?cmd='], + ])('defaults %s to openclaw --help', async (_description, path) => { + const { sandbox, startProcess } = sandboxForCli(); + + const response = await appFor(sandbox).request(path, {}, createMockEnv()); + + expect(response.status).toBe(200); + expect(startProcess).toHaveBeenCalledWith('openclaw --help'); + expect(await response.json()).toMatchObject({ command: 'openclaw --help' }); + }); + + it('accepts the exact openclaw --version command', async () => { + const { sandbox, startProcess } = sandboxForCli(); + + const response = await appFor(sandbox).request( + '/debug/cli?cmd=openclaw%20--version', + {}, + createMockEnv(), + ); + + expect(response.status).toBe(200); + expect(startProcess).toHaveBeenCalledWith('openclaw --version'); + expect(await response.json()).toMatchObject({ command: 'openclaw --version' }); + }); + + it.each([ + ['env', 'env'], + ['config file', 'cat /root/.openclaw/openclaw.json'], + ['semicolon injection', 'openclaw --help; env'], + ['and injection', 'openclaw --help && env'], + ])('rejects %s without starting a process', async (_description, cmd) => { + const { sandbox, startProcess } = sandboxForCli(); + + const response = await appFor(sandbox).request( + `/debug/cli?cmd=${encodeURIComponent(cmd)}`, + {}, + createMockEnv(), + ); + + expect(response.status).toBe(400); + expect(startProcess).not.toHaveBeenCalled(); + const body = await response.text(); + expect(body).toBe('{"error":"Unsupported debug CLI command"}'); + expect(body).not.toContain(cmd); + }); +}); diff --git a/src/routes/debug.ts b/src/routes/debug.ts index 6e146ddc5..05d791e8e 100644 --- a/src/routes/debug.ts +++ b/src/routes/debug.ts @@ -9,6 +9,10 @@ import { handleScheduled } from '../cron/handler'; * when mounted in the main app */ const debug = new Hono(); +const ALLOWED_CLI_COMMANDS: ReadonlySet = new Set([ + 'openclaw --help', + 'openclaw --version', +]); // GET /debug/version - Returns version info from inside the container debug.get('/version', async (c) => { @@ -131,6 +135,10 @@ debug.get('/cli', async (c) => { const sandbox = c.get('sandbox'); const cmd = c.req.query('cmd') || 'openclaw --help'; + if (!ALLOWED_CLI_COMMANDS.has(cmd)) { + return c.json({ error: 'Unsupported debug CLI command' }, 400); + } + try { const proc = await sandbox.startProcess(cmd); await waitForProcess(proc, 120000);