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:
parent
d3dbf002e4
commit
862e6a765c
@ -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
|
||||
{
|
||||
|
||||
99
src/cli/gateway-cli/local-only.test.ts
Normal file
99
src/cli/gateway-cli/local-only.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user