The agent-agnostic foundation for putting any terminal-shaped AI coding agent on the network. Pick your poison — claude-code, pi, opencode, hermes, whatever vibes — bolt on a 20-line adapter, and out the other end you get an HTTP API, an OpenAI-compatible chat completions endpoint, an MCP server, a Telegram bot, and a cron scheduler that fires the agent on whatever schedule you can dream up. One container. Same surfaces. Swap the brain.
You don't fork this. You FROM it.
FROM psyb0t/aicodebox
RUN npm install -g @earendil-works/pi-coding-agent@0.74.0
COPY mypkg /opt/mypkg
RUN pip3 install --break-system-packages /opt/mypkg
ENV AICODEBOX_ADAPTER=mypkg.adapter:MyAdapter \
AICODEBOX_AGENT_BINARY=piThat's it. The base owns the surfaces. Your adapter translates "run this prompt" into whatever your agent's CLI expects. New agent lands in an afternoon.
| Layer | The goods |
|---|---|
| OS | Ubuntu 24.04. aicode user (UID 1000), passwordless sudo, docker group. |
| Runtimes | Node.js 22 LTS (for agents that ship as npm), Python 3.12, Docker CE + buildx + compose (in case your agent needs to spawn containers). |
| Package | aicodebox — the adapter contract + four mode dispatchers (api / telegram / cron / mcp). Pure Python, zero side effects until you boot a mode. |
| Modes | All optional, all opt-in via env vars. Run none or one per container. Exception: telegram + cron can share a container — cron runs in-thread inside the telegram process. |
| Auth | AICODEBOX_API_MODE_TOKEN gates API mode; AICODEBOX_MCP_MODE_TOKEN gates MCP. Single bearer per surface, no fallback between them. Empty = no auth. Telegram has its own allowlist. |
| State | Per-chat overrides + cron history go under $HOME/.aicodebox/. Bind-mount that path if you want it to outlive the container. The package itself stores nothing. |
Everything routes through one interface. You implement it once per agent.
# mypkg/adapter.py
from aicodebox.adapters.base import AgentAdapter
from aicodebox.shared.runner import RunRequest, RunResult
class MyAdapter(AgentAdapter):
name = "my-agent"
available_models = ["fast", "smart"]
available_thinking_levels = ["off", "low", "high"]
def build_argv(self, req: RunRequest) -> list[str]:
argv = ["my-agent", "-p", req.prompt]
if req.model: argv += ["--model", req.model]
if req.workspace: argv += ["--cwd", req.workspace]
return argv
def parse_result(self, stdout: str, stderr: str, code: int) -> RunResult:
return RunResult(text=stdout, raw_stdout=stdout, raw_stderr=stderr, exit_code=code)ENV AICODEBOX_ADAPTER=mypkg.adapter:MyAdapterThe package gets resolved at first call, cached for the process lifetime. Every mode pulls the same adapter — what gets exposed over HTTP / MCP / Telegram / cron is exactly what your build_argv knows how to drive.
Modes are controlled by env vars. Set the flag, the entrypoint starts that mode. No flag, no mode. Foreground modes (API / Telegram / Cron) are mutually exclusive — except telegram + cron, which share a process (cron runs in-thread inside telegram). API wins if set alongside anything else. MCP mode is independent — it coexists with any foreground mode, served on its own port (or mounted at /mcp inside API).
AICODEBOX_API_MODE=1. Boots a FastAPI server on :8080 (override with AICODEBOX_API_MODE_PORT) with:
Required:
AICODEBOX_AVAILABLE_MODELS=<csv>—/v1/modelsneeds a real list, and there's no safe fallback (the adapter name isn't a model name). API mode refuses to boot without it. Pick the model ids your configured provider actually serves.
POST /run— sync agent run; returns{text, raw_stdout, raw_stderr, exit_code}POST /run/async— fire-and-forget; returns a job idGET /run/{id}— poll an async jobPOST /run/{id}/cancel— kill an in-flight runGET|PUT|DELETE /files/{path}— workspace file CRUDPOST /v1/chat/completions— OpenAI-compatible (streaming + non-streaming). Plug it into anything that speaks OpenAI.GET /v1/models— model list from the adapterPOST /mcp— MCP server (mounted only whenAICODEBOX_MCP_MODE=1; auth viaAICODEBOX_MCP_MODE_TOKEN, separate from the API bearer)
Bearer auth for the API surface: AICODEBOX_API_MODE_TOKEN=<one-token>. Single token, no rotation list. Empty = no auth.
AICODEBOX_TELEGRAM_MODE=1 + AICODEBOX_TELEGRAM_MODE_TOKEN=<bot:token>. Drop the bot into a chat, talk to it, get answers. Features:
- Text in → agent run → response chunked + Markdown→HTML rendered for Telegram.
- File uploads (document / photo / video / voice) land in the chat's workspace.
[SEND_FILE: relative/path]in agent output delivers workspace files back as Telegram attachments.- Per-chat overrides:
/model,/effort,/system_prompt,/append_system_prompt. Persisted to disk. /cancelkills the in-flight run for the chat./reloadre-reads the yaml./configdumps merged chat config./fetch <path>downloads a workspace file./statuslists busy chats.- Replies to cron-fired messages inject the job's instruction + result so follow-ups make sense.
Config lives at $HOME/.aicodebox/telegram.yml:
allowed_chats: [-100123, 42]
default:
model: glm-4.5-air
workspace: shared
chats:
-100123:
workspace: alpha
model: claude-sonnet
allowed_users: [10, 20]AICODEBOX_CRON_MODE=1 + AICODEBOX_CRON_MODE_FILE=/path/to/cron.yaml. 6-field croniter schedules, per-job workspace, optional telegram notification.
jobs:
- name: morning-report
schedule: "0 0 9 * * *"
instruction: |
Summarize yesterday's git activity in {workspace}.
workspace: shared
telegram_chat_id: -100123
model: claude-sonnetEach run gets its own history dir under $HOME/.aicodebox/cron/history/<workspace-slug>/<YYYYmmdd-HHMMSS>-<job>/ with meta.json, stdout.log, stderr.log, result.txt, and (if telegram-notified) telegram.json. The next run's prompt gets a "prior runs" hint pointing at that directory — your agent can read its own past output without you wiring it up.
AICODEBOX_MCP_MODE=1. Exposes the MCP (Model Context Protocol) surface. Coexists with any foreground mode:
| Foreground | MCP placement |
|---|---|
API mode (AICODEBOX_API_MODE=1) |
mounted at /mcp on the API port — no extra process |
| Telegram / Cron / passthrough | runs as a sidecar uvicorn on AICODEBOX_MCP_MODE_PORT (default 8081) |
Auth: AICODEBOX_MCP_MODE_TOKEN=<one-token> — bearer token in the Authorization: Bearer … header, or ?apiToken=… for clients that can't set headers. Empty = no auth. No fallback to API_MODE_TOKEN — MCP is its own surface with its own bearer.
Point Claude Desktop / Cursor / whatever at the MCP endpoint and the agent shows up as a set of tools (run_prompt, list_files, read_file, write_file, delete_file).
Everything's an env var. The base sets sane defaults, your child image overrides.
Env var convention: <MODE>_MODE is the on/off flag for that mode; <MODE>_MODE_<KNOB> is its config. Vars that aren't mode-scoped (workspace, adapter, container) are bare.
| Var | Default | What it does |
|---|---|---|
AICODEBOX_ADAPTER |
required | pkg.module:Class reference to your AgentAdapter subclass |
AICODEBOX_AGENT_BINARY |
required | Name of the agent's CLI binary (for which checks, version reports) |
AICODEBOX_WORKSPACE |
/workspace |
Root dir for all per-chat / per-job workspaces |
AICODEBOX_CONTAINER_NAME |
aicodebox |
Display name in /status, logs, and per-container state files |
AICODEBOX_AVAILABLE_MODELS |
— | Required for API mode. CSV list returned by /v1/models and shown in the telegram /model picker. API mode refuses to boot without it; telegram /model picker degrades to a "set this env var" reply. |
AICODEBOX_AVAILABLE_EFFORTS |
adapter list | Override the effort/--thinking list exposed via /effort (comma-separated) |
| Var | Default | What it does |
|---|---|---|
AICODEBOX_API_MODE |
0 |
Boot the HTTP API server (foreground) |
AICODEBOX_TELEGRAM_MODE |
0 |
Boot the Telegram bot (foreground) |
AICODEBOX_CRON_MODE |
0 |
Boot the cron scheduler (foreground; runs in-thread if telegram is also on) |
AICODEBOX_MCP_MODE |
0 |
Expose the MCP server — mounted at /mcp in API mode, or as a sidecar elsewhere |
| Var | Default | What it does |
|---|---|---|
AICODEBOX_API_MODE_PORT |
8080 |
Port the API server binds to |
AICODEBOX_API_MODE_TOKEN |
empty | Bearer token for the API surface. Empty = no auth |
| Var | Default | What it does |
|---|---|---|
AICODEBOX_TELEGRAM_MODE_TOKEN |
— | Bot token from @BotFather |
AICODEBOX_TELEGRAM_MODE_CONFIG |
$HOME/.aicodebox/telegram.yml |
Path to the telegram config yaml |
AICODEBOX_TELEGRAM_MODE_OVERRIDES |
$HOME/.aicodebox/telegram_overrides.json |
Per-chat override store (model/effort/system prompts) |
| Var | Default | What it does |
|---|---|---|
AICODEBOX_CRON_MODE_FILE |
— | Path to the cron yaml |
AICODEBOX_CRON_MODE_HISTORY_DIR |
$HOME/.aicodebox/cron/history |
Where each run writes meta.json, stdout.log, stderr.log, result.txt |
| Var | Default | What it does |
|---|---|---|
AICODEBOX_MCP_MODE_PORT |
8081 |
Port the sidecar MCP server binds to (ignored when MCP is mounted inside API) |
AICODEBOX_MCP_MODE_TOKEN |
empty | Bearer token for MCP. Empty = no auth. No fallback to API_MODE_TOKEN |
Minimal adapter that wires up an npm-shipped agent:
FROM psyb0t/aicodebox:latest
# Your agent — pin the version.
ARG AGENT_VERSION=0.74.0
RUN npm install -g @your-org/your-agent@${AGENT_VERSION}
# Your adapter package — implements aicodebox.adapters.base.AgentAdapter.
COPY your_adapter /opt/your_adapter
RUN pip3 install --no-cache-dir --break-system-packages /opt/your_adapter
ENV AICODEBOX_ADAPTER=your_adapter.adapter:YourAdapter \
AICODEBOX_AGENT_BINARY=your-agentBoot it:
docker run --rm -p 8080:8080 \
-e AICODEBOX_API_MODE=1 \
-e AICODEBOX_API_MODE_TOKEN=$(openssl rand -hex 16) \
-v "$PWD/workspace:/workspace" \
your/child-image:latestA reference child image lives at psyb0t/pibox — wraps pi-coding-agent and uses this base verbatim.
make help # list targets
make build # docker build .
make test # python unit tests (94 cases — adapter contract, modes, helpers)
make test-unit # same as test
make lint # flake8 + pyright
make format # isort + black
make clean # nuke caches + the built imageTests run in-process — no docker required. The suite stubs out the adapter via AICODEBOX_ADAPTER=aicodebox.tests.conftest:_StubAdapter so the modes can be exercised without a real agent on disk.
For integration testing with a real agent + real Telegram chat, see the e2e harness in the pibox repo — it uses psyb0t/telethon-plus as a userbot driver.
WTFPL — see LICENSE. Do what the fuck you want.