Install
openclaw skills install @psyb0t/piboxpi-coding-agent (earendil-works) running on the network inside an aicodebox container. Exposes seven programmatic surfaces on one image — interactive shell, one-shot exec (-p "..."), an HTTP REST API (run/async/cancel, workspace file ops), an OpenAI-compatible /openai/v1/chat/completions endpoint (streaming, client-executed tool calling, response_format/JSON-schema), an MCP server at /mcp (mounted in API mode or as a sidecar), a Telegram bot, and a cron scheduler that fires pi on a schedule. Foreground modes (API/Telegram/Cron) are mutually exclusive except Telegram+Cron; MCP coexists with any of them. Bearer-token auth per surface (PIBOX_API_MODE_TOKEN, PIBOX_MCP_MODE_TOKEN), empty = no auth. Use when the user wants to drive pi-coding-agent programmatically over HTTP/MCP/Telegram/cron instead of a local terminal session, or needs to reason about which pibox mode/endpoint fits a given integration.
openclaw skills install @psyb0t/piboxpi-coding-agent inside an aicodebox container. One image, seven ways in: interactive shell, one-shot exec, HTTP REST API, OpenAI-compatible endpoint, MCP server, Telegram bot, cron scheduler.
You talk to pibox, pibox talks to pi, pi talks to whatever Anthropic-compatible LLM you point it at (ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN).
For installation and configuration, see references/setup.md.
PIBOX_API_MODE_TOKEN is unset. With it empty the REST/OpenAI-compatible API surface is UNAUTHENTICATED — anyone who can reach it gets full agent-execution and workspace file-read/write/delete access. NEVER expose such an instance on a network or to untrusted agents; set the token and bind to loopback / behind an authenticating proxy.PIBOX_MCP_MODE_TOKEN is unset. Same story for the MCP surface (/mcp or the sidecar) — empty token means unauthenticated run_prompt/file-tool access, and it does not fall back to PIBOX_API_MODE_TOKEN. Set it explicitly.DELETE /run/{id}, DELETE /files/{path}, and the MCP delete_file tool remove state with no undo (canceled runs can't be resumed; deleted files are gone). An agent must NEVER call these unless the user explicitly asked for that exact action; confirm the specific target first, scope it to the current task, and never enumerate-then-bulk-delete. On a shared/multi-tenant instance a deletion can destroy another caller's in-flight run or workspace file — treat these routes as admin-only./run, /openai/v1/chat/completions, or MCP tools)./openai/v1/* surface./openai/v1/chat/completions to stream token-by-token when tools or a JSON schema is in play — those modes compute the full answer first and replay it as a single-shot SSE stream (still a valid stream, just not incremental). Plain chat streams incrementally.PIBOX_MCP_MODE_TOKEN falling back to PIBOX_API_MODE_TOKEN — MCP has its own bearer, no fallback.No mode env var set, no args passed to docker run. Falls through to pi's own CLI, invoked directly — a normal interactive pi session inside the container.
docker run -it --rm \
-e ANTHROPIC_AUTH_TOKEN=your-token \
-e ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic \
-e ANTHROPIC_MODEL=glm-4.6 \
-v "$PWD/workspace:/workspace" \
psyb0t/pibox:latest
Auth: none at the container boundary — you're inside it. pi itself uses the ANTHROPIC_* env vars.
No mode env var set, args passed after the image name are forwarded verbatim to the pi binary (passthrough). -p "<prompt>" runs pi non-interactively and prints the result to stdout, then exits.
docker run --rm \
-e ANTHROPIC_AUTH_TOKEN=your-token \
-e ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic \
-e ANTHROPIC_MODEL=glm-4.6 \
-v "$PWD/workspace:/workspace" \
psyb0t/pibox:latest \
-p "list the files in /workspace"
Any pi CLI flag works here (--model, --thinking, --session, etc.) — the entrypoint just execs pi "$@". Auth: none at the container boundary; pi uses ANTHROPIC_*.
PIBOX_API_MODE=1. FastAPI server on :8080 (override PIBOX_API_MODE_PORT). Requires PIBOX_AVAILABLE_MODELS=<csv> — the server refuses to boot without it (no sensible default; pi can drive any provider's models).
docker run -d --network host \
-e PIBOX_API_MODE=1 \
-e PIBOX_API_MODE_TOKEN=your-secret \
-e PIBOX_AVAILABLE_MODELS=glm-4.6,glm-4.5-air \
-e ANTHROPIC_AUTH_TOKEN=your-token \
-e ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic \
-e ANTHROPIC_MODEL=glm-4.6 \
-v "$PWD/workspace:/workspace" \
psyb0t/pibox:latest
| Method | Path | What it does |
|---|---|---|
GET | /healthz | liveness — {ok, adapter}, unauthenticated |
GET | /status | in-flight runs + busy workspaces |
POST | /run | run the agent — sync by default; body async or fireAndForget makes it fire-and-poll |
GET | /run/result?runId=<id> | poll a run started with async/fireAndForget |
DELETE | /run/{id} | cancel an in-flight run (kills the subprocess) |
GET | /files | list the workspace root — {entries: [{name, type, size?}, ...]} |
GET | /files/{path} | list a sub-directory, or stream a file's bytes |
PUT | /files/{path} | upload — raw request body becomes the file contents; parent dirs auto-created |
DELETE | /files/{path} | delete a file (refuses directories — 400) |
POST | /openai/v1/chat/completions | OpenAI-compatible chat (see below) |
GET | /openai/v1/models | model list, from PIBOX_AVAILABLE_MODELS |
POST | /mcp | MCP server (streamable HTTP) — mounted only when PIBOX_MCP_MODE=1 |
All /files/* paths resolve against the workspace root with traversal checking — .. segments that escape the root return 400. Every route except /healthz is gated by Authorization: Bearer <PIBOX_API_MODE_TOKEN> when that var is set; empty/unset token = no auth.
No auth when PIBOX_API_MODE_TOKEN is unset. With it empty the API is UNAUTHENTICATED — anyone who can reach it gets run-the-agent and workspace file-read/write/delete access. NEVER expose such an instance on a network or to untrusted agents; set the token and bind to loopback / behind an authenticating proxy.
Destructive & irreversible. DELETE /run/{id} kills the in-flight subprocess with no undo, and DELETE /files/{path} deletes a workspace file with no undo. An agent must NEVER call either unless the user explicitly asked for that exact action; confirm the specific target first; scope it to the current task; never enumerate-then-bulk-delete. On a shared/multi-tenant instance this can disrupt or destroy another caller's run or data — treat these routes as admin-only.
# sync run
curl -s http://localhost:8080/run \
-H "Authorization: Bearer your-secret" \
-H "Content-Type: application/json" \
-d '{"prompt": "say HELLO", "workspace": "/workspace"}'
# async run, then poll
RUN_ID=$(curl -s http://localhost:8080/run \
-H "Authorization: Bearer your-secret" -H "Content-Type: application/json" \
-d '{"prompt": "long task", "async": true}' | jq -r .runId)
curl -s "http://localhost:8080/run/result?runId=$RUN_ID" \
-H "Authorization: Bearer your-secret"
# cancel it
curl -s -X DELETE "http://localhost:8080/run/$RUN_ID" \
-H "Authorization: Bearer your-secret"
# upload / download / list / delete a workspace file
curl -sS -X PUT -H "Authorization: Bearer your-secret" \
--data-binary @local.txt http://localhost:8080/files/notes/hello.txt
curl -sS -H "Authorization: Bearer your-secret" \
http://localhost:8080/files/notes/hello.txt
curl -sS -H "Authorization: Bearer your-secret" \
http://localhost:8080/files/notes | jq
curl -sS -X DELETE -H "Authorization: Bearer your-secret" \
http://localhost:8080/files/notes/hello.txt
POST /run body fields: prompt (required), workspace, model, systemPrompt, appendSystemPrompt, jsonSchema, noContinue, resume, timeoutSeconds, thinking, noTools, toolsAllowlist, extraArgs, async, fireAndForget, includeRaw. With jsonSchema set the response is verbose: text, json (schema-validated), events, sessionId, usage, attempts (per-retry breakdown, up to 3 self-correction retries on parse/validation failure). Without jsonSchema the response is lean: {runId, workspace, exitCode, text}.
Same PIBOX_API_MODE=1 server, POST /openai/v1/chat/completions and GET /openai/v1/models. Drop-in for any OpenAI-SDK client — point the base URL at http://host:8080/openai/v1 and set the model to one of PIBOX_AVAILABLE_MODELS.
curl -s http://localhost:8080/openai/v1/chat/completions \
-H "Authorization: Bearer your-secret" \
-H "Content-Type: application/json" \
-d '{
"model": "glm-4.6",
"messages": [{"role": "user", "content": "say HELLO"}]
}'
Streaming ("stream": true): plain chat streams incrementally, one SSE chunk per token delta. Requests carrying tools or a JSON-schema constraint can't stream token-by-token (the answer only exists once fully computed) — they're buffered: the whole response is computed, then replayed as a single-shot SSE stream (role chunk → one content/tool_calls delta → finish chunk → [DONE]). The client's streaming parser is satisfied either way.
Tools / tool_choice (OpenAI client-executed tool calling): send tools (OpenAI function-schema array) and optionally tool_choice (auto/none/required/{"type":"function","function":{"name":...}}). pibox's own internal tools (bash, file edits) default OFF while in tool mode so the model behaves as a pure function-calling LLM — override with header x-aicodebox-no-tools: 0 to re-enable the hybrid. The response comes back as tool_calls + finish_reason: "tool_calls", exactly like OpenAI; you execute the tool client-side and send the result back in the next message round.
response_format / JSON-schema: standard OpenAI response_format body field —{"type": "text"} (default), {"type": "json_object"} (force parseable JSON, no shape constraint), or {"type": "json_schema", "json_schema": {"name": ..., "schema": {...}}} (schema-validated, with up to 3 self-correction retries on failure → 422 if still invalid). tools and response_format compose in one request: a tool-call turn returns tool_calls (not schema-checked); the model's final answer (no more tool calls) is what gets schema-validated.
Extra x-aicodebox-* headers (workspace pinning, session continuation, extra args, timeout, tools allowlist) are documented in references/setup.md. Upstream provider errors (auth failure, rate limit, content-safety rejection) surface as HTTP 400, not a silent empty response.
PIBOX_MCP_MODE=1. Exposes an MCP (streamable HTTP) surface with 5 tools: run_prompt, list_files, read_file, write_file, delete_file. Coexists with any foreground mode:
| Foreground | MCP placement |
|---|---|
API mode (PIBOX_API_MODE=1) | mounted at /mcp on the API port — no extra process |
| Telegram / Cron / passthrough / none | sidecar uvicorn on its own port, PIBOX_MCP_MODE_PORT (default 8081), mounted at the port root |
claude mcp add --transport http pibox http://localhost:8080/mcp \
--header "Authorization: Bearer your-mcp-token"
Raw JSON-RPC (debugging, non-MCP-aware callers):
curl -s http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer your-mcp-token" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
run_prompt(prompt, workspace?, model?, system_prompt?, append_system_prompt?, no_continue=true, resume?, thinking?, json_schema?) invokes the agent and returns its text. list_files/read_file/write_file/delete_file operate on workspace-relative paths with the same traversal guard as the REST /files endpoints.
Destructive & irreversible. delete_file removes a workspace file with no undo. An agent must NEVER call it unless the user explicitly asked for that exact action; confirm the specific target first; scope it to the current task; never enumerate-then-bulk-delete. On a shared/multi-tenant instance this can destroy another caller's workspace data — treat it as admin-only.
Auth: PIBOX_MCP_MODE_TOKEN=<token> — bearer via Authorization: Bearer …, or ?apiToken=… query param for clients that can't set headers. Empty = no auth. No fallback to PIBOX_API_MODE_TOKEN.
No auth when PIBOX_MCP_MODE_TOKEN is unset. With it empty the MCP surface is UNAUTHENTICATED — anyone who can reach it gets run_prompt (arbitrary agent execution) and workspace file read/write/delete access. NEVER expose such an instance on a network or to untrusted agents; set the token and bind to loopback / behind an authenticating proxy.
PIBOX_TELEGRAM_MODE=1 + PIBOX_TELEGRAM_MODE_TOKEN=<token from @BotFather>.
[SEND_FILE: path] in pi's output delivers workspace files as Telegram attachments./model, /effort (maps to pi's --thinking levels), /system_prompt, /append_system_prompt. Persisted across restarts./cancel kills the in-flight run. /reload re-reads config. /config dumps merged settings. /status shows in-flight state. /fetch <path> downloads a file. /start, /help for basics.Config at $HOME/.aicodebox/telegram.yml (override via PIBOX_TELEGRAM_MODE_CONFIG):
allowed_chats: [-100123, 42]
default:
model: glm-4.6
workspace: shared
chats:
-100123:
workspace: alpha
allowed_users: [10, 20]
Auth: chat/user allowlisting via the config yaml (allowed_chats, per-chat allowed_users) — no separate bearer token, the bot token itself gates who can even message it.
PIBOX_CRON_MODE=1 + PIBOX_CRON_MODE_FILE=/path/to/cron.yaml. 6-field cron schedules via croniter. Each job fires pi with the given instruction on schedule.
jobs:
- name: morning-standup
schedule: "0 0 9 * * 1-5"
instruction: |
Summarize what changed in /workspace since yesterday.
Be brief. One paragraph max.
workspace: myproject
telegram_chat_id: -100123
model: glm-4.6
thinking: low
Each run gets a history dir at $HOME/.aicodebox/cron/history/<workspace>/<timestamp>-<job>/ (override root via PIBOX_CRON_MODE_HISTORY_DIR) with meta.json, stdout.log, stderr.log, result.txt. If telegram is also configured, telegram.json lands there too and the next run's prompt gets a "prior run" hint so pi can reference its own history without you wiring it up. Running alongside Telegram mode (PIBOX_TELEGRAM_MODE=1 + PIBOX_CRON_MODE=1) runs cron in-thread inside the telegram process — the only foreground-mode pairing allowed.
Auth: none — this is a scheduled background job, not a request-driven surface. telegram_chat_id on a job routes its result through the (already-authenticated) Telegram bot if set.
pi speaks the Anthropic wire protocol. Point it at any Anthropic-compatible endpoint via env vars, forwarded into every mode:
| Var | Purpose |
|---|---|
ANTHROPIC_AUTH_TOKEN | Bearer token (Z.AI, direct Anthropic, etc.) |
ANTHROPIC_API_KEY | Same thing — pi reads both |
ANTHROPIC_BASE_URL | Endpoint override (default https://api.anthropic.com) |
ANTHROPIC_MODEL | Default model when the caller doesn't specify one |
Z.AI's GLM models are a fast/cheap default: ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic + ANTHROPIC_MODEL=glm-4.6.
pi's thinking levels (--thinking): off, minimal, low, medium, high, xhigh. Exposed as /effort in Telegram mode and thinking in API/OAI requests.
Surface-level auth (separate from the LLM upstream) is per-mode: PIBOX_API_MODE_TOKEN gates the REST + OAI routes, PIBOX_MCP_MODE_TOKEN gates MCP (no fallback between the two), Telegram gates by bot-token possession + chat/user allowlist, cron and interactive/exec modes have no surface auth (container-boundary trust). Both PIBOX_API_MODE_TOKEN and PIBOX_MCP_MODE_TOKEN default to empty, which means no auth — see Security & safety above before exposing either surface beyond localhost.
One-off task from a script, no server:
docker run --rm -e ANTHROPIC_AUTH_TOKEN=$TOKEN -e ANTHROPIC_BASE_URL=$BASE_URL \
-e ANTHROPIC_MODEL=glm-4.6 -v "$PWD:/workspace" \
psyb0t/pibox:latest -p "summarize the diff in /workspace"
Long-running server, drive it with curl:
docker run -d --network host -e PIBOX_API_MODE=1 -e PIBOX_API_MODE_TOKEN=$SECRET \
-e PIBOX_AVAILABLE_MODELS=glm-4.6 -e ANTHROPIC_AUTH_TOKEN=$TOKEN \
-e ANTHROPIC_BASE_URL=$BASE_URL -v "$PWD/workspace:/workspace" psyb0t/pibox:latest
curl -s http://localhost:8080/run -H "Authorization: Bearer $SECRET" \
-H "Content-Type: application/json" -d '{"prompt": "run the tests and report failures"}'
Structured extraction via JSON schema:
curl -s http://localhost:8080/run -H "Authorization: Bearer $SECRET" \
-H "Content-Type: application/json" \
-d '{"prompt": "extract TODOs from /workspace", "jsonSchema": {"type":"object","properties":{"todos":{"type":"array","items":{"type":"string"}}},"required":["todos"]}}'
Wire an MCP-aware agent (Claude Code, OpenClaw) to a running pibox:
claude mcp add --transport http pibox http://localhost:8080/mcp \
--header "Authorization: Bearer $MCP_TOKEN"
Chat + Telegram + scheduled digest, all on one box:
docker run -d --network host \
-e PIBOX_TELEGRAM_MODE=1 -e PIBOX_TELEGRAM_MODE_TOKEN=$BOT_TOKEN \
-e PIBOX_CRON_MODE=1 -e PIBOX_CRON_MODE_FILE=/config/cron.yaml \
-e ANTHROPIC_AUTH_TOKEN=$TOKEN -e ANTHROPIC_BASE_URL=$BASE_URL \
-v "$PWD/workspace:/workspace" -v "$PWD/cron.yaml:/config/cron.yaml:ro" \
psyb0t/pibox:latest