Skip to main content
This document provides detailed technical reference. For a beginner-friendly overview, see The Gateway.

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 via exec.
  • 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)

  1. LLM calls exec with args ["curl", "https://example.com"]
  2. Gateway receives the tool call (it’s a pi SDK custom tool)
  3. Gateway logs: {agent: "travel", tool: "exec", args: ["curl", "..."], decision: "allowed"}
  4. Gateway runs docker exec <container> curl https://example.com
  5. 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:
  1. LLM calls exec with args ["git", "status"]
  2. Gateway receives the tool call, logs it as a core exec call
  3. Gateway runs docker exec <container> sh -c "git status"
  4. The shell finds /tools/bin/git on $PATH — a generated launcher script that:
    • Connects to /beige/gateway.sock (Unix socket mounted into container)
    • Sends: {"tool": "git", "args": ["status"]}
    • Waits for response
  5. 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
  6. Launcher prints the result to stdout, exits
  7. docker exec captures output, returns to gateway
  8. 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 concurrency limit 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/stream endpoint 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 has fallbackModels configured, the gateway uses a multi-pass retry loop:
  1. Tries each model in order (primary first, then fallbacks)
  2. If a model fails, moves to the next one — but keeps it eligible for retry on the next pass
  3. Loops through the model list up to 3 passes before giving up
  4. A model is removed from the retry pool when it either:
    • Has a rate-limit cooldown active (retryAfter in the future), OR
    • Has accumulated 3 consecutive failures in this prompt run
  5. Returns the first successful response, or an error if all models are exhausted
This handles transient errors (e.g. Anthropic “overloaded”) gracefully: if model A fails once and model B also fails once, model A gets another shot on the next pass — the overload may have cleared by then. With 2 models, this means up to 6 total attempts before giving up. Rate limit vs. transient error handling:
  • Rate limits (HTTP 429, rate_limit / too many requests patterns): 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-after header, 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.json and survives restarts
  • A modelSwitched hook 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. The concurrency 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 both prompt() and promptStreaming() calls uniformly.
  • Concurrency is tracked per provider, not per model — all models on the same provider share the same pool.
This is complementary to the rate-limit fallback system: concurrency limits prevent overloading a provider in the first place, while fallback handles errors after they occur.

3. Core Tools (src/tools/core.ts)

Implemented as pi SDK ToolDefinition objects:
  • read: Read a file from the sandbox. Gateway runs docker exec <container> cat <path>.
  • write: Write content to a file in the sandbox. Gateway pipes content via docker 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 runs docker exec <container> <command>.
All four: log first, check policy, then execute.

4. Plugins (src/plugins/)

Each plugin is a directory:
plugin.json:
The gateway:
  1. Loads all plugins from config (calls createPlugin() and register())
  2. Plugins register tools, channels, hooks, and skills via PluginRegistrar
  3. Validates that all agent tool references resolve to registered tools
  4. Generates a launcher script per tool per agent
  5. Mounts launchers read-only into /tools/bin/ (which is prepended to $PATH)
  6. Mounts plugin packages read-only into /tools/packages/
At runtime, 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:
Where /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):
Response (gateway → sandbox):
Error response:

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 InteractiveMode locally for the full pi experience. LLM calls are proxied through the gateway’s /api/chat/stream endpoint (the TUI never needs API keys). Tool execution is proxied through the gateway’s /api/agents/:name/exec endpoint. 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):
  1. System default — hardcoded in the gateway
  2. Plugin/channel config default — set in plugin config
  3. 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:

Tech Decisions