Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<process_id>` - Get logs for a specific process
- `GET /debug/version` - Get container and moltbot version info
- `GET /debug/cli?cmd=<command>` - Run only `openclaw --help` or `openclaw --version`; defaults to help and returns 400 for other commands

## Optional: Chat Channels

Expand Down
96 changes: 96 additions & 0 deletions src/routes/debug.test.ts
Original file line number Diff line number Diff line change
@@ -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<AppEnv> {
const app = new Hono<AppEnv>();
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);
});
});
8 changes: 8 additions & 0 deletions src/routes/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { handleScheduled } from '../cron/handler';
* when mounted in the main app
*/
const debug = new Hono<AppEnv>();
const ALLOWED_CLI_COMMANDS: ReadonlySet<string> = new Set([
'openclaw --help',
'openclaw --version',
]);

// GET /debug/version - Returns version info from inside the container
debug.get('/version', async (c) => {
Expand Down Expand Up @@ -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);
Expand Down