From 862e6a765cfc1901aaba334ce49fe74609f396ad Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:11:21 +0000 Subject: [PATCH] Add --local-only flag and strictly local security assessment This commit introduces a new `--local-only` flag for the `gateway` command that hardens the configuration for strictly local usage by: - Disabling update checks on start - Disabling diagnostics - Clearing model fallbacks (to prevent accidental cloud usage) - Denying external tools (web_search, web_fetch, browser) - Enabling Docker sandboxing for non-main sessions It also adds a detailed `SECURITY_ASSESSMENT_LOCAL.md` report and a unit test for the new flag. Co-authored-by: samibs <1743891+samibs@users.noreply.github.com> --- SECURITY_ASSESSMENT_LOCAL.md | 18 ++++- src/cli/gateway-cli/local-only.test.ts | 99 ++++++++++++++++++++++++++ src/cli/gateway-cli/run.ts | 16 +++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/cli/gateway-cli/local-only.test.ts diff --git a/SECURITY_ASSESSMENT_LOCAL.md b/SECURITY_ASSESSMENT_LOCAL.md index 578796544..1af27c233 100644 --- a/SECURITY_ASSESSMENT_LOCAL.md +++ b/SECURITY_ASSESSMENT_LOCAL.md @@ -63,9 +63,25 @@ When using external channels, the bot receives untrusted input. --- +## New: "Strictly Local" Startup Option + +Moltbot now includes a `--local-only` flag for the `gateway` command. This flag automatically applies all the hardening steps described in this report at startup, without requiring you to manually edit your configuration file. + +To use it, start the gateway with: +```bash +moltbot gateway --local-only +``` + +This will: +- Disable update checks on start. +- Disable diagnostics. +- Clear all model fallbacks. +- Deny web tools, browser, and skills-install. +- Enable Docker sandboxing for non-main sessions. + ## Recommended Configuration for "Strictly Local" Setup -Add/update these keys in your `~/.clawdbot/moltbot.json`: +If you prefer to make these settings permanent in your configuration, add/update these keys in your `~/.clawdbot/moltbot.json`: ```json5 { diff --git a/src/cli/gateway-cli/local-only.test.ts b/src/cli/gateway-cli/local-only.test.ts new file mode 100644 index 000000000..2f6adc67b --- /dev/null +++ b/src/cli/gateway-cli/local-only.test.ts @@ -0,0 +1,99 @@ +import { Command } from "commander"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +const setConfigOverride = vi.fn(); +const loadConfig = vi.fn(() => ({ gateway: { mode: "local" } })); +const startGatewayServer = vi.fn(async () => ({ + close: vi.fn(async () => {}), +})); + +const defaultRuntime = { + log: vi.fn(), + error: vi.fn(), + exit: (code: number) => { + throw new Error(`__exit__:${code}`); + }, +}; + +vi.mock("../../config/config.js", () => ({ + CONFIG_PATH: "/tmp/moltbot.json", + STATE_DIR: "/tmp/.clawdbot", + loadConfig: () => loadConfig(), + readConfigFileSnapshot: async () => ({ exists: true, valid: true, config: loadConfig() }), + resolveGatewayPort: () => 18789, +})); + +vi.mock("../../config/runtime-overrides.js", () => ({ + setConfigOverride: (path: string, value: unknown) => setConfigOverride(path, value), +})); + +vi.mock("../../gateway/server.js", () => ({ + startGatewayServer: (port: number, opts?: unknown) => startGatewayServer(port, opts), +})); + +vi.mock("./run-loop.js", () => ({ + runGatewayLoop: async (params: { start: () => Promise }) => { + await params.start(); + }, +})); + +vi.mock("../../runtime.js", () => ({ + defaultRuntime, +})); + +// Mock other dependencies to avoid side effects +vi.mock("../../globals.js", () => ({ + setVerbose: vi.fn(), +})); +vi.mock("../../gateway/ws-logging.js", () => ({ + setGatewayWsLogStyle: vi.fn(), +})); +vi.mock("../../logging/console.js", () => ({ + setConsoleTimestampPrefix: vi.fn(), +})); + +describe("gateway-cli --local-only", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("applies hardening overrides when --local-only is passed", async () => { + const { registerGatewayCli } = await import("../gateway-cli.js"); + const program = new Command(); + program.exitOverride(); + registerGatewayCli(program); + + // We expect it to try to start the server and fail because we don't mock the loop + // or just check if setConfigOverride was called. + try { + await program.parseAsync(["gateway", "--local-only", "--token", "test-token"], { + from: "user", + }); + } catch (err) { + // ignore exit + } + + expect(setConfigOverride).toHaveBeenCalledWith("update.checkOnStart", false); + expect(setConfigOverride).toHaveBeenCalledWith("diagnostics.enabled", false); + expect(setConfigOverride).toHaveBeenCalledWith("agents.defaults.model.fallbacks", []); + expect(setConfigOverride).toHaveBeenCalledWith("tools.deny", ["group:web", "browser", "skills-install"]); + expect(setConfigOverride).toHaveBeenCalledWith("agents.defaults.sandbox.mode", "non-main"); + }); + + it("does not apply hardening overrides by default", async () => { + const { registerGatewayCli } = await import("../gateway-cli.js"); + const program = new Command(); + program.exitOverride(); + registerGatewayCli(program); + + try { + await program.parseAsync(["gateway", "--token", "test-token"], { + from: "user", + }); + } catch (err) { + // ignore exit + } + + expect(setConfigOverride).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index cb26aa98d..eb2fbfb12 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -8,6 +8,7 @@ import { readConfigFileSnapshot, resolveGatewayPort, } from "../../config/config.js"; +import { setConfigOverride } from "../../config/runtime-overrides.js"; import { resolveGatewayAuth } from "../../gateway/auth.js"; import { startGatewayServer } from "../../gateway/server.js"; import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js"; @@ -48,6 +49,7 @@ type GatewayRunOpts = { rawStreamPath?: unknown; dev?: boolean; reset?: boolean; + localOnly?: boolean; }; const gatewayLog = createSubsystemLogger("gateway"); @@ -93,6 +95,15 @@ async function runGatewayCommand(opts: GatewayRunOpts) { await ensureDevGatewayConfig({ reset: Boolean(opts.reset) }); } + if (opts.localOnly) { + gatewayLog.info("local-only: hardening configuration for strictly local usage"); + setConfigOverride("update.checkOnStart", false); + setConfigOverride("diagnostics.enabled", false); + setConfigOverride("agents.defaults.model.fallbacks", []); + setConfigOverride("tools.deny", ["group:web", "browser", "skills-install"]); + setConfigOverride("agents.defaults.sandbox.mode", "non-main"); + } + const cfg = loadConfig(); const portOverride = parsePort(opts.port); if (opts.port !== undefined && portOverride === null) { @@ -348,6 +359,11 @@ export function addGatewayRunCommand(cmd: Command): Command { .option("--compact", 'Alias for "--ws-log compact"', false) .option("--raw-stream", "Log raw model stream events to jsonl", false) .option("--raw-stream-path ", "Raw stream jsonl path") + .option( + "--local-only", + "Hardens configuration for strictly local usage (disables updates, diagnostics, and external tools)", + false, + ) .action(async (opts) => { await runGatewayCommand(opts); });