Clearbox
Observable, reviewable agent work. Big changes, broken into a graph you can watch and audit.
A small service that lets a model post a plan as a DAG of work items over MCP, executes the plan node by node (roots first, then their dependents, in parallel where possible) with agent sessions attached to the nodes, and shows everything live in the browser: which nodes are being worked on right now, and — on click — the agent's session in that node as it happens.
model ──MCP──► create_plan / add_node / add_edge / start_plan
│
┌───────▼────────┐ SQLite (plans, nodes, edges, agent profiles, transcripts)
│ Clearbox │──► built-in agent per ready node, chosen by its agent profile:
│ scheduler │ · Claude Agent SDK
│ │ · OpenAI-compatible endpoints (OpenAI, vLLM, Ollama, …)
└───────┬────────┘◄── or external agents: claim_next_node / complete_node
│ SSE
browser UI (DAG view, running markers, live transcripts)
Install
Needs Python ≥ 3.10.
uv tool install clearbox # or: pipx install clearbox, or: pip install clearbox
clearbox --mock # fake agents: try the UI without spending tokens
Quick start
From a clone, with uv:
cd Clearbox
uv run clearbox --mock # fake agents: try the UI without spending tokens
uv run python examples/demo_plan.py --start
Open http://127.0.0.1:8765/.
For real agents drop --mock. Built-in agents run through the Claude Agent SDK, which ships its
own Claude Code CLI and uses your existing Claude login (or ANTHROPIC_API_KEY).
uv run clearbox # real Claude sessions, 3 in parallel
uv run clearbox --max-concurrency 5
Agents may edit files and run commands in their plan's working directory by default; what each agent profile may do is its capability.
Nodes can also run on any OpenAI-compatible endpoint instead — a local Ollama or vLLM model, or OpenAI itself — and one plan can mix them per node, see Agents and harnesses:
uv run clearbox --openai-base-url http://localhost:11434/v1 --openai-model qwen2.5-coder:14b
Without uv: pip install -e . then clearbox ... (or python -m clearbox ...).
Connect a model
The MCP endpoint is http://127.0.0.1:8765/mcp (streamable HTTP).
claude mcp add --transport http clearbox http://127.0.0.1:8765/mcp
Then ask, e.g. "Use clearbox to make a plan for … and start it."
MCP tools
| Tool | What it does |
|---|---|
create_plan(title, description="", parent_plan_id=None, parent_node_id=None) |
New plan → integer id (1, 2, 3 …). With a parent it becomes the subplan of that node. |
add_node(plan_id, title, description="", agent=None, files=None, results_from=None, kind=None) |
New node → [plan_id, node_id]. Node ids count up per plan. agent (profile name or id) pins the node to an agent profile; omitted = inherit. files declares the paths it will create or modify, see File scoping. results_from lists earlier nodes whose result text it receives, see Inherited results. kind="user" makes it the user's own job, see User-job nodes; default "agent". |
add_edge(plan_id, from_node_id, to_node_id) |
from must finish before to starts. Cycles are rejected. Passes no result; that is results_from. |
remove_edge, remove_node |
Corrections while building. |
update_node(plan_id, node_id, title=None, description=None, files=None, results_from=None) |
Change only what you pass; files / results_from omitted keeps the current value, [] clears it. |
set_node_files(plan_id, node_id, files) |
Replace a node's declared files on their own, without touching title/description; [] or null clears them. Only before the node has run. |
set_node_results_from(plan_id, node_id, node_ids) |
Replace the earlier nodes whose result text the node receives; [] or null clears them. Each must be a node it already depends on through edges. Only before the node has run. |
set_node_kind(plan_id, node_id, kind) |
Make a node the user's job ("user") or an agent's ("agent"), see User-job nodes. Only before the node has run. |
get_plan(plan_id), list_plans() |
Structure, status, results, progress. get_plan also shows default_agent and, per node, the resolved agent and its agent_override, plus files and conflicts (see File scoping), results_from, kind and waiting_for_user (a user node that is ready and waiting to be marked done). |
list_agents() |
Agent profiles (secrets redacted to has_api_key; default: true marks the server default), including each one's capability and test_level. |
create_agent(name, harness, model=None, base_url=None, api_key_env=None, params=None, tools=None, permission_mode=None, system_append=None, max_parallel=None, description="", capability=None, test_level=None) |
New agent profile. capability (read | run | edit | full) and test_level (none | light | standard | thorough) are explained under Capability and Test levels; permission_mode and tools are raw escape hatches. Deliberately takes no literal api_key: secrets should not pass through a model's context. |
update_agent(agent, …same fields…) |
Change only the fields passed; "" clears a text field (for capability / test_level: back to the default), max_parallel=0 removes the limit, params/tools replace the whole object. |
delete_agent(agent) |
Delete a profile; nodes and plans that used it inherit again. |
set_node_agent(plan_id, node_id, agent) |
Per-node override; null clears it. Applies the next time the node starts. |
set_plan_agent(plan_id, agent) |
Default for the plan and its subplans; null clears it. |
test_agent(agent) |
Liveness check: for openai lists the endpoint's models (falls back to a tiny completion) → {ok, detail, reachable, model_listed, models_sample, credential}. |
start_plan(plan_id, executor="builtin" | "external"), stop_plan(plan_id) |
Run / pause a top-level plan. |
claim_next_node(agent_name, plan_id=None) |
External agents: take the next ready node, with the same prompt a built-in agent would get. |
report_progress(plan_id, node_id, message, kind="text") |
Append to the node's live transcript. |
complete_node(plan_id, node_id, result), fail_node(plan_id, node_id, error) |
Finish a claimed node. |
set_plan_review_agent(plan_id, agent) |
Name (or null to clear) the agent profile that automatically reviews this plan's steps and, once it finishes, the plan as a whole; see Reviewing. |
review_node(plan_id, node_id, agent=None), review_plan(plan_id, agent=None) |
Review a done node's diff, or a finished plan's steps as a whole, right now; agent is a one-off override, used for this run only. |
get_reviews(plan_id) |
Every review recorded for this plan: the plan-level one plus each node's verdict and findings. |
rework_node(plan_id, node_id, instructions=None) |
Re-run one finished (done or failed) step with its review findings, or your instructions, in its prompt; dependants are left alone. It is reviewed again by itself afterwards, see Rework. |
How execution works
- Node states:
pending → ready → running → done | failed. - A node becomes ready when all its prerequisites are done. Root nodes are ready immediately.
- builtin executor: the service starts one agent session per ready node (up to
--max-concurrencyin total, and up to each agent profile'smax_parallel), on the harness the node's agent profile names (see Agents and harnesses). The prompt contains the overall goal, the chain of parent nodes for subplans, a one-line list of the other nodes of the plan, the node's own task, its file scope, and only the earlier results it asked for (see Inherited results). The agent's final message becomes the node's result; it reaches a later node only if that node requests it. - external executor: ready nodes wait until something calls
claim_next_node. - User-job nodes (
kind="user"): no agent runs them; the plan stops there until you mark the node done, see User-job nodes. - Subplans: a node with a subplan skips the agent; when it is reached its subplan runs, and the
node is done once the whole subplan is done (its result summarises the subplan's final nodes).
Built-in agents also get the MCP tools, so an agent can decide mid-run that its node is too big
and expand it into a subplan (
create_plan(parent_plan_id=…, parent_node_id=…)); the node then finishes after that subplan (disable with--no-agent-mcp).openaiagents get the same tools through a built-in MCP client, prefixedvc_(vc_create_plan, …). - All agents of one plan tree share a working directory:
<workdir>/plan-<root id>(default./workspace/plan-N). - A failed node blocks its dependents. Use Retry in the UI. Stop cancels running built-in agents and re-queues their nodes; Reset clears all results and transcripts.
- Everything is stored in
clearbox.db. Built-in sessions that were running when the service stopped are re-queued on the next start.
User-job nodes: a step a person does
Some steps of a plan are not an agent's to do: push a branch, open a merge request, deploy, approve
something. Mark such a node with kind="user" (add_node(..., kind="user"), set_node_kind, or the
Done by select in the node drawer; changeable while the node is pending or ready). Nothing else
about the node changes: it has edges, it can declare files and request results_from, and it
blocks its dependents like any other node.
When its prerequisites finish the node becomes ready and the plan simply stops there: the
built-in scheduler never starts a session for it, claim_next_node never hands it out, and it
holds no file lock while it waits (a parked user node never delays an agent node that shares a
file). It stays ready until you mark it done — the ✓ Mark done button in its drawer, MCP's
complete_node, or POST /api/plans/{id}/nodes/{node_id}/complete — with an optional result (the
MR link, say) that later nodes can request through results_from. Then the nodes after it start as
usual. A plan with a waiting user node is not finished: its plan review does not fire, and its
progress rollup counts it under waiting.
Write the description for a person, not for an agent: it is shown verbatim in the drawer as the instruction. No agent, prompt, transcript, diff, step review or token cost is involved; Stop on a waiting user node fails it ("stopped by user") and Retry puts it back to ready.
Node views carry kind ("agent" | "user") and waiting_for_user (true for a ready user node).
GET /api/plans/{id} also lists every waiting user node of the tree under waiting as
{plan_id, node_id, title}, which is what the topbar's "Waiting for you" chips show. REST sets the
kind with PUT /api/plans/{plan_id}/nodes/{node_id}/kind (body {"kind": "user"}); switching a
ready node back to "agent" starts it.
File scoping: avoiding clobbered edits
Independent nodes run in parallel, and nothing stops two of them from editing the same file at the
same time — in practice they overwrite each other's work. files on a node lists the paths it will
create or modify; reading a file never needs declaring, since reads cannot clobber anything.
Declare nothing and a node behaves exactly as before — no conflicts, no waiting; this is also how
every plan made before this feature keeps working unchanged.
Declaring files buys three things: conflicting nodes are serialized (below), the agent is told its
scope, and the node gets a complete diff for review. Before a node starts, Clearbox snapshots
every declared path and compares it again when the node finishes, so a change shows up however it
was made, from the shell as well as from the file tools. A node that declares nothing only gets the
writes made through its file tools (see Reviewing).
Two nodes that could otherwise run at the same time and declare a shared file never actually run
together: they run one after the other, lowest node number first. This spans the whole plan
tree — a node in a subplan can conflict with one in the root plan or a sibling subplan — because all
agents of one plan tree share a single working directory (<workdir>/plan-<root id>, see above).
Nodes already ordered by a dependency edge are never held back for this: if A must finish before
B starts, they cannot overlap, so sharing a file is fine and costs nothing extra. A node takes all
of its declared files at once, never one at a time, so two nodes that each declare the other's file
cannot deadlock — the lower-numbered one always gets to start.
Paths are relative to the plan tree's working directory, exactly like the ones a node's agent reads
and writes; they may name a file that does not exist yet (the node is about to create it) or a
directory, which claims everything under it (e.g. files=["clearbox/"] for a node that
restructures a whole package). Matching is case-insensitive and treats \ and / alike on every
platform, not just Windows — App.js and app.js are treated as the same file even on a
case-sensitive filesystem. That occasionally serializes two genuinely different files; the
alternative is the opposite mistake, two agents writing what they each think is a different file and
one edit getting silently lost, which is worse.
Example: node 3 declares files=["src/parser.py"], node 5 (no dependency between them) declares
files=["src/"]. Both become ready at the same time. Node 3 starts; node 5 is held back with
blocked_by reason "running" until node 3 finishes, then starts. Had node 5 instead been ready
first purely by luck of scheduling, it would still wait — the rule is node number, not arrival order.
REST exposes the same thing as PUT /api/plans/{plan_id}/nodes/{node_id}/files (body {"files": [...]}). Every node view, from REST and from MCP's get_plan alike, carries files (its own
declared paths) and conflicts (the other nodes in the tree that share one and could run at the
same time — these are the ones that get serialized); REST's node view adds blocked_by while a node
is waiting: which node holds the file, and whether that node is "running" or merely has an earlier
"turn".
In the UI, nodes that share a file are drawn one step lower than the last within their layer and
joined by a dashed connector, so the stagger is visible at a glance even though it is not a
dependency edge; a node waiting on one says so in its drawer ("waiting for 3 "Parse config" to
release src/parser.py"), and the drawer's Files row lets you view and edit a node's declared
files directly (disabled once it has started).
Inherited results: edges order, results_from passes
An edge means "runs after" and nothing more. A node starts with no earlier results in its prompt
unless it asks for them with results_from=[…], so every description must be self-contained: what to
change, where, and any decision an earlier node made that this one has to follow. This is deliberate.
Predecessor summaries run to hundreds of words each; a node handed three of them spends its budget
reading them, and then re-verifying them in the code, instead of doing its own task. Most nodes need
nothing beyond their description.
What a node's prompt contains, in order: the overall goal (plan title and description, plus the parent chain for subplans), the other nodes of the plan as one-liners (so nodes don't overlap), the requested results, if any, then its own task and its file scope. The requested results are introduced as finished work to build on, not to redo.
results_from only passes; it never orders. A node may only request nodes of its own plan that it
already depends on, directly or through other nodes — anything else might not have a result yet when
it runs, and is rejected with a message saying which edge is missing. add_node accepts the list
before the edges exist (you add those next), so the check runs when the edges are known:
set_node_results_from, update_node and start_plan reject it. Like files, it can only change
while the node is pending or ready.
The useful pattern: implementation nodes request nothing and start with a short prompt; a final docs or summary node depends on all of them and requests every result, writing from them instead of rediscovering the work:
store = add_node(p, "Store: add the column", …, files=["clearbox/store.py"])
engine = add_node(p, "Engine: use it", …, files=["clearbox/engine.py"])
docs = add_node(p, "Docs", "Document the change in README.md …", files=["README.md"],
results_from=[store[1], engine[1]])
add_edge(p, store[1], engine[1]); add_edge(p, engine[1], docs[1]) # docs depends on store via engine
REST: PUT /api/plans/{plan_id}/nodes/{node_id}/results-from (body {"node_ids": [...]}), and
GET /api/plans/{plan_id}/nodes/{node_id}/context → {"context": "…"}, the exact prompt the node gets,
built from the plan as it is now (for a node that already ran: what it got, unless the plan changed
since). In the UI, the node drawer's Inherits results from row shows and edits the list (checkboxes
offer only the node's dependencies; editable while pending or ready), the Depends on row is marked
"ordering only", and the collapsed Prompt block shows that same context.
Reviewing
Reviewing is a second, separate layer on top of execution: it never edits anything, never fails a
node and never blocks a dependent — it only records a verdict (ok | concerns | error) and
findings for a human to act on. Reviewing is off for a plan until it names a review agent with
set_plan_review_agent (MCP) / PUT /api/plans/{id}/review-agent; a plan that never sets one
behaves exactly as it did before reviewing existed, and nobody pays for review tokens by accident.
There are two levels, deliberately kept apart so each stays inside a context it can actually judge:
- Step review (
review_node/POST /api/plans/{id}/nodes/{node_id}/review): once a node isdone, a reviewer looks at that node's captured diff against that node's task description — one diff, one task, nothing else. It runs in the background after the node has already finished, so it never delays whatever depends on it. The diff has two sources: a snapshot of the node's declaredfilestaken before it starts, compared again when it finishes (directories are walked, so added and deleted files show up too), plus every write the node makes through the file tools (write_file,edit_file, the Claude harness's own edit tools). Within the declared scope the diff is therefore complete, including changes made throughbash,sed -ior a heredoc; outside it, and for a node that declares nothing, it is best-effort and covers file-tool writes only. The reviewer is told which case applies, so it can treat a missing change in the declared scope as not made and stays cautious elsewhere. Agents are still asked to edit through the file tools: those edits show up in the transcript as reviewable steps, and they are the only ones captured outside the declared scope. The reviewer is asked to run the test suite before its verdict, so the automatic reviews get capabilityrun(read, search, shell; no edits) whatever the profile allows for normal runs, see Capability. On-demand reviews triggered withreview_node/review_plancurrently still run with read-only tools. - Plan review (
review_plan/POST /api/plans/{id}/review): once every node in the tree is done, one reviewer looks at the plan as a whole — every node's title, description, result and step-review verdict — and asks what was missed, contradicted or left out across steps. This is explicitly not a code review: it never reads diffs or source, only reads what the step reviews and results already say.
Both levels run automatically once set_plan_review_agent names an agent (the plan review fires
once, right after the last node completes), and both can also be triggered on demand with an
optional one-off agent= override, whether or not the plan has one set — so a plan finished before
reviewing existed can still be reviewed after the fact. get_reviews(plan_id) /
GET /api/plans/{id} read every verdict and finding back. The intended loop: read the plan-level
review's oversights, then turn each one into a follow-up node (or plan) rather than editing by
hand — reviews describe gaps, they don't fill them.
Rework: closing the review loop
For a single step, the loop closes in place: step runs → step review → rework with the findings →
automatic re-review. rework_node(plan_id, node_id, instructions=None) (MCP) /
POST /api/plans/{id}/nodes/{node_id}/rework (body {"instructions": "…"}, optional) re-runs that
one node's agent. Its prompt is the node's normal context plus a final REWORK section: the
previous attempt is already in the working tree, so do not redo the step from scratch, address
exactly what is listed and change only what that needs. What is listed is your instructions
or, if you leave them out, the review's verdict, summary and findings. "Re-run this step and fix X"
works without any review too. With neither instructions nor a review the call is refused; use
retry or a new node instead.
- Always explicit. Nothing in Clearbox re-runs a node because a review disliked it. Reviews stay advisory, and a rework is always something a person or a planning model asked for.
- One node only. Only a
doneorfailednode can be reworked. A running one has to be stopped first, and a node that wraps a subplan is refused: rework a step inside it instead. Nodes that depend on it are not reset or re-run, even though they may have built on the old output. That limit is deliberate: cascading resets would discard finished work that may be fine, and deciding what needs redoing is a judgement for whoever asked for the rework. The node view lists itsstarted_dependants(direct dependants that already ran or are running), and the UI warns when some are done, so you know what you are choosing. If they need redoing, rework them too or add follow-up nodes. - Re-review by itself. A rework is just another run of the node: its transcript, token counters,
diff and file locks reset as on any start, and it finishes through the normal path. So if the plan
has a review agent and the run captured a diff, a new step review is queued with no extra trigger.
Read it with
get_reviewsand repeat if needed. Without a review agent, trigger one withreview_node.
The brief stays on the node as rework_brief after the run (so a retry of a failed rework still
carries it); the next rework replaces it, and Reset clears it.
Agents and harnesses
A harness is the runtime that executes a node; an agent profile is a named, persisted configuration of one (harness + model + connection + limits). Nodes and plans refer to a profile by name or id and never hold connection details themselves, so one plan can run its mechanical nodes on a cheap or local model and its hard nodes on a strong one. Every harness emits the same transcript events, so the UI, the database and the live stream look identical whichever ran.
| Harness | What runs the node | Tools |
|---|---|---|
claude-sdk |
The Claude Agent SDK (its bundled Claude Code CLI, your Claude login or ANTHROPIC_API_KEY). |
Its own (Bash, Read, Edit, …), gated by the profile's capability. |
openai |
Any endpoint speaking the OpenAI chat-completions API, driven by Clearbox's own agent loop (ask → run the tool calls → feed results back → repeat until a reply without tool calls). | Clearbox's built-in toolset (below) + the MCP tools, gated by the same capability. |
mock |
A fake agent that prints a plausible transcript. No cost. | Pretend ones. |
On every start the server makes sure these profiles exist (created only when absent, never
overwritten): claude, mock, and — only if --openai-base-url, OPENAI_BASE_URL or
OPENAI_API_KEY is set — openai. The claude profile stores no model / permission mode / max
turns, so --model, --permission-mode and --max-turns keep applying to it; a value set on the
profile overrides the flag. Manage profiles in the UI (Agents in the sidebar, with a Test
connection button), over MCP (create_agent …) or over REST (/api/agents).
uv run clearbox --openai-base-url http://localhost:11434/v1 --openai-model qwen2.5-coder:14b
$env:OPENAI_API_KEY = "sk-…"; uv run clearbox --agent-profile openai # every node on gpt-4o-mini
Profile fields
| Field | Meaning | Default |
|---|---|---|
name |
Unique, not purely numeric; what nodes, plans, MCP tools and the UI refer to. | required |
harness |
claude-sdk, openai or mock. |
required |
model |
Model name as the endpoint knows it (gpt-4o-mini, qwen2.5-coder:14b, vendor/model on OpenRouter, claude-opus-5-5). |
claude-sdk: --model, else the SDK default. openai: required. |
base_url |
openai only: endpoint root, the part before /chat/completions — usually ends in /v1. |
https://api.openai.com/v1 |
api_key_env |
openai only: name of the server environment variable holding the key, read when a node starts. |
none (OPENAI_API_KEY for the default OpenAI url) |
api_key |
Literal key, settable from the UI / REST only. Write-only: responses only say has_api_key. |
none |
params |
JSON object. openai: temperature, top_p, max_tokens, max_completion_tokens, reasoning_effort, seed, parallel_tool_calls, max_turns (50), stream (true; false for endpoints with broken streaming), timeout (600 s), max_retries (2), extra_body (passed through verbatim), input_price_per_mtok / output_price_per_mtok (USD, enables the cost shown in the transcript). claude-sdk: max_turns. mock: speed. |
{} |
capability |
What the agent may do, the same for every harness: read | run | edit | full, see Capability. |
null = full as a worker, run as a reviewer |
test_level |
How much a worker agent is told to test: none | light | standard | thorough, see Test levels. |
null = standard |
tools |
JSON object, openai only, raw escape hatch (the capability picks this for you): {"preset": "all" | "readonly" | "none", "allow": […], "deny": […], "mcp": true | false}. allow adds to the preset, deny removes (deny wins); mcp overrides --no-agent-mcp's on/off for this profile (it cannot turn the tools on when the server has --no-agent-mcp) and is the one key the capability keeps. |
from the capability; mcp as the server |
permission_mode |
claude-sdk only, raw escape hatch: same values as --permission-mode, overrides the mode the capability chose. |
from the capability |
system_append |
Extra system-prompt text for every node on this profile. | none |
max_parallel |
Max nodes running on this profile at once, across all plans (on top of --max-concurrency). Useful for a local GPU. |
no limit |
description |
Free text for humans. | "" |
Capability: what an agent may do
A profile's capability is one setting that means the same thing on every harness; the registry
(clearbox/harness/registry.py) maps it to each harness's own gating.
capability |
Allows | claude-sdk |
openai |
|---|---|---|---|
read |
read and search only; no edits, no commands | Read, Grep, Glob | preset readonly |
run |
read, search and run commands; no edits | + Bash | readonly + allow: ["bash"] |
edit |
read, search and edit files; no commands | + Write, Edit, MultiEdit | readonly + allow: ["write_file", "edit_file"] |
full |
everything | all of the above, in the server's --permission-mode |
preset all |
Unset (null) means full for a worker agent and run for a reviewer — a reviewer has to run
the test suite but must never edit. full lets an agent run arbitrary commands in the plan's
working directory, with the rights of the Clearbox process; that is the operator's choice, made
when picking the profile (see the warning under Tools of the openai harness
and use a sandbox for anything you do not trust).
For claude-sdk the gating is done with the SDK's allowed_tools: everything listed is
pre-approved and runs without a prompt. Below full the session runs in permission mode dontAsk,
which denies any tool that is not pre-approved instead of prompting — an SDK session has nobody to
answer a prompt, so any prompting mode would silently block the tool. full pre-approves every file
and shell tool and keeps --permission-mode for whatever else the model reaches for. A profile's
raw permission_mode still overrides the chosen mode, and openai's raw tools config is still
honoured for a profile that sets no capability, so profiles made before this setting existed
behave as they did. Reviewers ignore the profile's normal-run capability and tools /
permission_mode entirely and get run; params.review_capability changes that for one profile.
An unknown value is rejected when the profile is saved.
Test levels: how much a worker agent tests
Every worker prompt ends with a testing paragraph chosen by the profile's test_level; the UI's
Test level select uses the same wording:
test_level |
Meaning |
|---|---|
none |
do not test; the node says what to verify, or nothing needs verifying |
light |
a quick sanity check that the change works — one script or a couple of cases |
standard |
run the existing tests covering what you touched, and fix what you broke |
thorough |
run the full suite, and add tests for behaviour you added |
standard is the default, for new profiles and for every existing one (null), and for
external agents claiming through claim_next_node. standard and thorough also tell the agent
where the run commands live (each test file's docstring) and never to use port 8765 or the project's
own database. The level is for worker agents only: reviewers keep their own prompt and always run
the suite before giving a verdict, whatever the profile's level — which is why they need
capability run.
Which agent runs a node
First hit wins: the node's own profile → its plan's default → each ancestor plan's default, up to
the root plan → the server default (--agent-profile). With no profile anywhere the node runs on
the CLI-configured harness (claude, or mock with --mock) — exactly what a plan without any
agent configuration did before profiles existed. The choice is made each time a node starts, so
editing a profile or an assignment affects the next node to start, not running ones.
In the UI the node drawer shows the resolved agent and where it came from (set on this node, plan default, inherited from plan N, server default) and can override it; the topbar sets the plan's default for the plan and its subplans. Graph nodes get a coloured chip only when their agent differs from the plan's default.
Secrets
Put api keys in environment variables of the Clearbox process and reference them by name with
api_key_env; the value is read when a node starts and lives only in the HTTP client. A literal
key can be typed into the Agents panel (or sent as api_key to POST/PATCH /api/agents) for
convenience: it is stored in the database but never returned by REST, the stream, MCP or the
logs — they only show has_api_key. "" clears it; leaving the field out keeps it. MCP's
create_agent/update_agent do not accept a key at all. Error texts from endpoints are scrubbed of
the key before they reach a transcript. Endpoints on localhost or a private network get a
placeholder key when none is configured, since they rarely check it.
Tools of the openai harness
Warning: these tools run arbitrary code in the plan's working directory, with the rights of the Clearbox process. The file tools are jailed to that directory (
..and symlinks are resolved before the check), butbashis not jailed — a shell can alwayscdout. Use presetreadonly(ordeny: ["bash"]) for models you would not hand a shell, and a sandbox for anything you do not trust.
| Tool | What it does | readonly |
|---|---|---|
read_file(path, offset=0, limit=2000) |
Read a text file with line numbers. | ✓ |
write_file(path, content) |
Create or overwrite a file (creates parent directories). | |
edit_file(path, old_string, new_string, replace_all=false) |
Exact string replacement; old_string must be unique unless replace_all. |
|
list_dir(path, depth) |
Directory listing, optionally recursive. | ✓ |
glob(pattern) |
Find files, e.g. **/*.py, newest first. |
✓ |
grep(pattern, path, glob, max_results=100) |
Regex search over file contents → path:line: text. |
✓ |
bash(command, timeout_ms=120000) |
Run a shell command (cmd.exe on Windows, /bin/sh elsewhere), max 600 000 ms; kills the process tree on timeout. |
|
vc_* |
The clearbox MCP tools (vc_create_plan, vc_add_node, …) so the agent can expand its node into a subplan; off with tools.mcp=false or --no-agent-mcp. |
Walks skip .git, node_modules, __pycache__ and virtualenvs; results are truncated like the
Claude harness's.
Which endpoint works
| Server | base_url |
Notes |
|---|---|---|
| OpenAI | https://api.openai.com/v1 (leave empty) |
api_key_env=OPENAI_API_KEY |
| OpenRouter | https://openrouter.ai/api/v1 |
model is the vendor/model slug; api_key_env=OPENROUTER_API_KEY |
| vLLM | http://<host>:8000/v1 |
start it with --enable-auto-tool-choice --tool-call-parser … or tool calls are rejected |
| LM Studio | http://localhost:1234/v1 |
load a model with tool-use support first |
| Ollama | http://localhost:11434/v1 |
model is the tag from ollama list, e.g. qwen2.5-coder:14b |
Common pitfalls (the transcript error says which one hit):
base_urlmust include/v1(the path before/chat/completions); without it the endpoint answers 404.- The model must support tool / function calling. One that doesn't either gets a 400 or simply
answers in prose and never touches a file. For pure-text nodes use preset
none. - Small local models often loop — re-reading the same file, repeating a call. Cap them with
params.max_turns(e.g. 15–25), keep their nodes mechanical, and setmax_parallel: 1if they share one GPU. - Unsure? Test connection in the Agents panel (or
test_agent) checks reachability, the key and whether the model name is listed, without spending tokens.
Walkthrough: a mixed-harness plan
examples/mixed_plan.py builds a plan that reviews a repository (default: this one) without
changing it. Two cheap-model nodes do the mechanical work (inventory the files, inventory the
tests) in parallel, a strong-model node assesses the architecture from their results, and a
strong-model node reviews that assessment:
uv run clearbox --openai-base-url http://localhost:11434/v1 --openai-model qwen2.5-coder:14b
uv run python examples/mixed_plan.py --start
It creates the agents it needs if missing and prints them: cheap (an openai copy of the first
configured openai profile with preset readonly, max_turns 20, max_parallel 1) and strong
(claude-sdk with raw permission mode default — note that with no capability set this is
full, every tool pre-approved; set capability="read" on it for a genuinely read-only
reviewer), then runs test_agent on
cheap. It assigns them the three ways: set_plan_agent makes cheap the plan default (the
inventory nodes inherit it), add_node(..., agent="strong") pins the assessment, and
set_node_agent moves the review node. --cheap NAME / --strong NAME use profiles you already
have; --repo PATH reviews another directory. With no openai profile configured (e.g. against
uv run clearbox --mock) or with --mock it creates mock-cheap / mock-strong instead, so
the whole flow can be watched for free.
UI
- Left: plans with progress bars. Top: breadcrumb (for subplans), status, Start / Stop / Reset.
- "Working now" lists every node in the plan tree that has an agent on it right now — click to jump.
- Graph: top-down layered DAG. Running nodes glow and spin, edges into them animate; subplan nodes are drawn as a stack with a progress bar (double-click to open). Nodes within a layer that share a declared file are nudged down a step each and joined by a dashed connector — the file stagger, see File scoping.
- Click a node: task, prerequisites, result/error, the agent that runs it (with where that came from, and a select to override it), Inherits results from and the Prompt block (the exact prompt it gets, see Inherited results), the Files row (its declared paths, which other nodes share one, and — while waiting — which node to wait for) and the live agent session (messages, thinking, tool calls with their results, final result with turns / time / cost).
- Nodes whose agent differs from the plan's default carry a coloured chip naming it; a dot marks an override set on the node itself. The topbar sets the plan's default agent.
- A user-job node is drawn as a pill with a double border, a person glyph and a "your job" chip; while it waits for you it lights up and pulses. "Waiting for you:" in the topbar lists every such node in the plan tree — click to jump. Its drawer shows the instruction in a box with a ✓ Mark done button under it, and the Done by row (an agent / you) switches a pending or ready node's kind, see User-job nodes.
- Agents in the sidebar manages the profiles: add one, pick its harness, model, endpoint and key, choose What may this agent do (the capability) and its Test level, cap its parallelism, and Test connection before trusting it. The raw Permission mode and Tools preset selects sit under Advanced as escape hatches.
- A reviewed node carries a small verdict marker (✓ / ! / ?) in its top-right corner; its full-view Review tab shows the verdict, summary and findings and a button to (re-)review it on demand. A review chip next to the plan status does the same for the plan-level review, see Reviewing.
- ⟲ Rework this step (in the Review tab and the drawer of a finished step) opens a box pre-filled with the review's findings. Edit it or clear it to fall back on the findings, then Re-run with these instructions. A line warns when later steps that depend on it are already done and will not be re-run, see Rework.
- URLs are deep-linkable:
/#/plan/2/node/3,/#/agents,/#/agents/4.
Developing Clearbox with Clearbox
Run a frozen copy as the service so agents can edit the source without breaking the running instance, and keep the database outside the repo:
uv tool install --force . # snapshot of the current code -> `clearbox` on PATH
clearbox --db $HOME\.clearbox\vc.db --workdir $HOME\.clearbox\workspace
The repo contains a .mcp.json, so Claude Code started in this folder offers the clearbox
server automatically. Create plans with working_directory set to the repo path so built-in agents
work on the code. To pick up the agents' changes, stop the service and run
uv tool install --force . again.
Agent profiles live in that database, so they survive the reinstall — configure an endpoint once and every later plan can use it. Two things worth knowing when a plan builds Clearbox itself:
- Built-in
claude-sdkagents use the login of the service process, not of the Claude Code session that planned the work. Afterclaude /login, or when a session limit hits agents but not your own terminal, restart the service so it picks the credentials up. - Nodes of one plan can run on different accounts or vendors by giving them different profiles — useful for spreading a long plan over a rate limit, or for keeping mechanical nodes off a metered model.
Options
| Flag | Env | Default |
|---|---|---|
--host, --port |
CLEARBOX_HOST, CLEARBOX_PORT |
127.0.0.1, 8765 |
--db |
CLEARBOX_DB |
clearbox.db |
--workdir |
CLEARBOX_WORKDIR |
workspace |
--max-concurrency |
CLEARBOX_MAX_CONCURRENCY |
3 |
--permission-mode |
CLEARBOX_PERMISSION_MODE |
acceptEdits |
--model |
CLEARBOX_MODEL |
SDK default |
--max-turns |
unlimited | |
--mock, --mock-speed |
CLEARBOX_AGENT=mock |
off |
--no-agent-mcp |
agents get the MCP tools | |
--agent-profile NAME |
CLEARBOX_AGENT_PROFILE |
mock with --mock, else claude |
--openai-base-url URL |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
--openai-model NAME |
CLEARBOX_OPENAI_MODEL |
gpt-4o-mini for the OpenAI endpoint, else none |
--openai-api-key-env NAME |
OPENAI_API_KEY |
--model, --permission-mode and --max-turns configure the claude profile's empty fields (and
any claude-sdk profile that leaves them empty). The three --openai-* flags only bootstrap
the openai profile the first time: once it exists they are ignored with a warning, because the
profile is the source of truth — edit it in the Agents panel or with update_agent. An
--agent-profile naming a profile that does not exist stops the server with a message listing the
ones that do.
What an agent may do is set per profile by its capability, not
by --permission-mode. The flag is the fallback: it is the mode a claude-sdk session runs in at
capability full, after every file and shell tool has already been pre-approved, so it only
governs what the model reaches for beyond those. Below full the session runs in dontAsk
regardless. On its own acceptEdits is not a usable default for unattended agents — it
prompts for Bash, and an SDK session has nobody to answer, so agents could not run anything; that
is what capability replaced. bypassPermissions lifts every check (only in a sandbox). For
openai agents the raw equivalent is the profile's tools preset.
Layout of the code
clearbox/
store.py SQLite schema + DAG operations (ids, cycle check) + agent profiles
engine.py scheduler, node lifecycle, subplans, external-agent protocol
bootstrap.py built-in profiles, server default, startup banner and validation
mcp_bridge.py MCP client that hands the clearbox tools to non-Claude harnesses (vc_*)
mcp_tools.py MCP tool definitions
app.py FastAPI: REST, SSE live stream, MCP mounted at /mcp
agents.py backwards-compatible aliases; the implementations moved to harness/
harness/
base.py the event contract every harness implements (AgentJob, Harness, HarnessError)
claude_sdk.py Claude Agent SDK harness
openai_compat.py OpenAI-compatible harness: the agent loop (stream, tool calls, usage)
mock.py fake harness
tools.py sandboxed toolset for harnesses that bring none (read_file … bash)
registry.py picks the harness for a node; per-profile caches and limits
openai_check.py /models check behind test_agent
probe.py tiny completion round trip when there is no /models listing
static/ UI (plain HTML/CSS/JS, no build step)
examples/demo_plan.py builds a demo plan through MCP
examples/mixed_plan.py builds a plan that mixes a cheap and a strong agent
tests/test_flow.py end-to-end test (run against a server started with --mock)
tests/test_agents_api.py REST agent lifecycle, incl. the secret-handling rules
tests/test_mixed_run.py per-node agents and max_parallel in a real run
tests/test_openai_harness.py the agent loop against tests/fake_openai.py (no network, no DB)
Releasing
The git tag is the trigger and the single source of truth. Bump the version, tag it, push:
uv version --bump patch # or minor / major; edits pyproject.toml
git commit -am "release v0.1.1"
git tag v0.1.1
git push origin main --tags
.github/workflows/release.yml then builds the sdist and wheel, refuses to continue if the tag
disagrees with pyproject.toml, checks the wheel really contains clearbox/static/, publishes to
PyPI, and only then creates the GitHub release with the artifacts attached. PyPI goes first
because an upload there is permanent, while a GitHub release can be deleted and recreated.
Publishing uses PyPI Trusted Publishing, so there is no API token in the repository. Configure
it once on PyPI under Publishing with: owner KaiHabermann, repository Clearbox, workflow
release.yml, environment pypi. Before the first release the project does not exist on PyPI
yet — add it as a pending publisher on your account instead, with the same four values.
.github/workflows/ci.yml runs the same build plus the mock-agent test suite on every push and
pull request, and installs the built wheel into a clean environment to catch a packaging mistake
that an editable install would hide. Action versions are pinned exactly and kept current by
Dependabot.
Metadata
Release files for clearbox 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| clearbox-0.2.0.tar.gz | 179.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| clearbox-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 334.2 kB
Release files / clearbox-0.2.0.tar.gz
| Download URL | clearbox-0.2.0.tar.gz |
|---|---|
| Size | 179.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
df7a626118eaf10c293ab61f5e2361042d4171909b12efeb49ff634bc6daa66f
|
|
BLAKE2b-256 checksum How to use checksums |
da06195fd1820fce93df1016fa7720736de4489cfdc69abdc077e1f2f249cb39
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / clearbox-0.2.0-py3-none-any.whl
| Download URL | clearbox-0.2.0-py3-none-any.whl |
|---|---|
| Size | 154.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
96bf7e6b08cdc5d7d8fbfaba5d0dcb209101e2b0df12e1c65590340e6e9f49a1
|
|
BLAKE2b-256 checksum How to use checksums |
0959de76546154349a3ceee2896ccf8ecb4da1991597a99e165699c4fd6a80aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for clearbox 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for clearbox-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.