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>
This commit is contained in:
google-labs-jules[bot] 2026-01-28 09:11:21 +00:00
parent d3dbf002e4
commit 862e6a765c
3 changed files with 132 additions and 1 deletions

View File

@ -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
{

View File

@ -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<unknown> }) => {
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();
});
});

View File

@ -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 <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);
});