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
|
## 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
|
```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,
|
readConfigFileSnapshot,
|
||||||
resolveGatewayPort,
|
resolveGatewayPort,
|
||||||
} from "../../config/config.js";
|
} from "../../config/config.js";
|
||||||
|
import { setConfigOverride } from "../../config/runtime-overrides.js";
|
||||||
import { resolveGatewayAuth } from "../../gateway/auth.js";
|
import { resolveGatewayAuth } from "../../gateway/auth.js";
|
||||||
import { startGatewayServer } from "../../gateway/server.js";
|
import { startGatewayServer } from "../../gateway/server.js";
|
||||||
import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
|
import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
|
||||||
@ -48,6 +49,7 @@ type GatewayRunOpts = {
|
|||||||
rawStreamPath?: unknown;
|
rawStreamPath?: unknown;
|
||||||
dev?: boolean;
|
dev?: boolean;
|
||||||
reset?: boolean;
|
reset?: boolean;
|
||||||
|
localOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const gatewayLog = createSubsystemLogger("gateway");
|
const gatewayLog = createSubsystemLogger("gateway");
|
||||||
@ -93,6 +95,15 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
|||||||
await ensureDevGatewayConfig({ reset: Boolean(opts.reset) });
|
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 cfg = loadConfig();
|
||||||
const portOverride = parsePort(opts.port);
|
const portOverride = parsePort(opts.port);
|
||||||
if (opts.port !== undefined && portOverride === null) {
|
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("--compact", 'Alias for "--ws-log compact"', false)
|
||||||
.option("--raw-stream", "Log raw model stream events to jsonl", false)
|
.option("--raw-stream", "Log raw model stream events to jsonl", false)
|
||||||
.option("--raw-stream-path <path>", "Raw stream jsonl path")
|
.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) => {
|
.action(async (opts) => {
|
||||||
await runGatewayCommand(opts);
|
await runGatewayCommand(opts);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user