Overview
Beige is a secure, sandboxed agent system where:- A gateway process orchestrates everything: LLM calls, policy enforcement, audit logging, sandbox lifecycle.
- Each agent runs inside its own Docker container (sandbox). All code execution happens there.
- The gateway exposes exactly 4 core tools to the LLM:
read,write,patch,exec. - Additional tools (like
git,browser,slack) are mounted into sandboxes as executable launchers. Agents invoke them viaexec. - Tool launchers are thin clients that call back to the gateway over a Unix domain socket for actual execution.
- The gateway uses the pi SDK (
@mariozechner/pi-coding-agent) for all LLM interaction, supporting Anthropic, OpenAI/ZAI, and any compatible provider.
Data Flow
Core Tool Call Flow (e.g. exec curl https://example.com)
- LLM calls
execwith args["curl", "https://example.com"] - Gateway receives the tool call (it’s a pi SDK custom tool)
- Gateway logs:
{agent: "travel", tool: "exec", args: ["curl", "..."], decision: "allowed"} - Gateway runs
docker exec <container> curl https://example.com - Gateway captures stdout/stderr, returns to LLM
Tool Launcher Call Flow (e.g. exec git status)
Since /tools/bin is on $PATH, the agent calls tools naturally. When the LLM runs exec git status:
- LLM calls
execwith args["git", "status"] - Gateway receives the tool call, logs it as a core
execcall - Gateway runs
docker exec <container> sh -c "git status" - The shell finds
/tools/bin/giton$PATH— a generated launcher script that:- Connects to
/beige/gateway.sock(Unix socket mounted into container) - Sends:
{"tool": "git", "args": ["status"]} - Waits for response
- Connects to
- Gateway socket server receives the request:
- Identifies agent from socket identity (each agent has its own socket)
- Logs:
{agent: "travel", tool: "git", args: ["status"], decision: "allowed"} - Checks policy (is “travel” agent allowed to use “git”?)
- Executes the git tool handler on the gateway host
- Returns result through socket
- Launcher prints the result to stdout, exits
docker execcaptures output, returns to gateway- Gateway returns exec result to LLM
Components
1. Config (config.json5)
Single config file (JSON5 — JSON with comments) drives the entire system. No defaults — everything is explicit.
2. Gateway Core (src/gateway/)
- Config loader: Reads and validates
config.json5, resolves env vars. - Agent manager: Creates/destroys agent sessions. Maps session keys → pi SDK
AgentSession+ Docker container. Supports multiple concurrent sessions per agent (one per channel/chat/thread). Handles model fallback and rate limit tracking. - Provider health tracker: Monitors per-provider rate limits and cooldowns. Persisted to
~/.beige/data/provider-health.json. - Concurrency limiter: Per-provider concurrency control. When a provider has a
concurrencylimit configured, requests beyond that limit queue up (FIFO) until a slot frees up. Prevents overwhelming providers with too many parallel LLM calls. - LLM proxy: The
/api/chat/streamendpoint proxies LLM calls for the TUI. Resolves API keys server-side, executes prePrompt/postResponse hooks, applies fallback model logic on rate limits, and audit-logs every attempt. The TUI never needs API keys. - Sandbox manager: Creates Docker containers with correct mounts, generates tool launchers, manages lifecycle.
- Socket server: One Unix domain socket per agent at
~/.beige/sockets/<agent>.sock, mounted into container at/beige/gateway.sock. - Policy engine: Checks if agent is allowed to use a tool. Deny by default.
- Audit logger: Logs every tool invocation with agent, tool, args, decision, timing, result summary.
Model Fallback
When an agent hasfallbackModels configured, the gateway uses a multi-pass retry loop:
- Tries each model in order (primary first, then fallbacks)
- If a model fails, moves to the next one — but keeps it eligible for retry on the next pass
- Loops through the model list up to 3 passes before giving up
- A model is removed from the retry pool when it either:
- Has a rate-limit cooldown active (
retryAfterin the future), OR - Has accumulated 3 consecutive failures in this prompt run
- Has a rate-limit cooldown active (
- Returns the first successful response, or an error if all models are exhausted
- Rate limits (HTTP 429,
rate_limit/too many requestspatterns): marked as globally cooling down — all sessions skip this model until the cooldown expires - Transient capacity errors (
overloaded,capacity,temporarily unavailable): NOT marked as globally cooling down — other sessions can still use the model. The failing session retries via the multi-pass loop. - If a rate-limit response includes a
retry-afterheader, that time is used for the cooldown - Otherwise, hard rate limits (HTTP 429) default to 5-minute cooldown; soft rate limits (pattern-matched) default to 60 seconds
- Cooldown state is persisted in
~/.beige/data/provider-health.jsonand survives restarts - A
modelSwitchedhook fires when a session ends up on a non-primary model, allowing plugins to send notifications
Provider Concurrency Limits
When multiple agents or sessions share a provider, the total number of parallel LLM requests can spike — especially with channels that handle many conversations simultaneously. Theconcurrency setting on a provider caps how many requests can be in-flight at once:
- Requests beyond the limit are queued (FIFO) and await until a slot frees up — they are never rejected.
- Default: no limit (
-1/ omitted). All requests pass through immediately. - The limiter sits inside
executePromptWithModel, so it applies to bothprompt()andpromptStreaming()calls uniformly. - Concurrency is tracked per provider, not per model — all models on the same provider share the same pool.
3. Core Tools (src/tools/core.ts)
Implemented as pi SDK ToolDefinition objects:
read: Read a file from the sandbox. Gateway runsdocker exec <container> cat <path>.write: Write content to a file in the sandbox. Gateway pipes content viadocker exec.patch: Apply a find-and-replace patch to a file in the sandbox. Gateway reads, patches, writes back.exec: Execute a command in the sandbox. Gateway runsdocker exec <container> <command>.
4. Plugins (src/plugins/)
Each plugin is a directory:
plugin.json:
- Loads all plugins from config (calls
createPlugin()andregister()) - Plugins register tools, channels, hooks, and skills via
PluginRegistrar - Validates that all agent tool references resolve to registered tools
- Generates a launcher script per tool per agent
- Mounts launchers read-only into
/tools/bin/(which is prepended to$PATH) - Mounts plugin packages read-only into
/tools/packages/
ToolRunner routes tool calls to the handler registered by the plugin that provides that tool.
5. Tool Launcher (generated, mounted into sandbox)
A small shell script that connects to the gateway socket:/beige/tool-client is a small binary/script (mounted read-only) that:
- Connects to
/beige/gateway.sock - Sends JSON request with tool name + args
- Reads JSON response
- Prints result to stdout
- Exits with appropriate code
6. Socket Protocol
Request (sandbox → gateway):7. Channels
The TUI is the only built-in channel. Other channels (Telegram, Discord, etc.) are provided by plugins.- TUI (built-in, separate process): Connects to gateway HTTP API. Runs pi’s
InteractiveModelocally for the full pi experience. LLM calls are proxied through the gateway’s/api/chat/streamendpoint (the TUI never needs API keys). Tool execution is proxied through the gateway’s/api/agents/:name/execendpoint. The gateway handles auth, hooks, fallback models, and audit logging for both paths. - Plugin channels (in-process): Plugins can register channel adapters and run background processes (e.g. Telegram polling). They create sessions via
PluginContext.prompt()and receive responses through the channel adapter.
Session Settings System
Sessions support layered settings with three levels of precedence (highest wins):- System default — hardcoded in the gateway
- Plugin/channel config default — set in plugin config
- Session override — set by user via commands, persisted in
~/.beige/sessions/session-settings.json
Verbose Mode
When verbose mode is enabled, the channel receives a callback (onToolStart) before each tool execution and can notify the user:
This gives users visibility into what the agent is doing without cluttering the main response.
Channel Commands
Commands are handled locally by the channel and not sent to the LLM:
On startup, the Telegram channel registers its commands with the bot API (deleting stale commands first).
8. Sandbox Docker Image
Minimal image with:- Deno runtime (for TypeScript execution)
- Common utilities (curl, jq, etc.)
/beige/tool-client— socket client script (baked into image, not mounted)- No secrets, no env vars from host
- Mounts:
/workspace(read-write) →~/.beige/agents/<name>/workspace//tools/bin(read-only) → generated launchers/tools/packages(read-only) → tool source packages/beige/gateway.sock(Unix socket)
9. Identity & Auth Model
- Each agent has its own Unix socket file. Gateway creates socket, mounts it.
- Gateway identifies agent by which socket received the connection.
- No payload-based identity. Agent cannot claim to be another agent.
- Threat model: even if an agent somehow accesses another agent’s socket file (impossible due to separate containers), the gateway still enforces per-socket identity.
10. Audit Log Format
JSONL file at~/.beige/logs/audit.jsonl:
