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──► submit_plan (plan + nodes + edges) / 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/. The port is --port / CLEARBOX_PORT (default 8765); the
examples, the tests and this repo's .mcp.json follow CLEARBOX_PORT too.
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; use your port if you passed
--port).
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 |
|---|---|
submit_plan(title, working_directory=None, description="", test_instructions=None, nodes=[…], edges=[…], parent_plan_id=None, parent_node_id=None, max_parallel=None) |
The way to build a plan: the plan, all its nodes and all its edges in one call → {"plan_id": …, "nodes": {key: node_id}}. nodes are {key, title, description, files, results_from, kind, agent} objects, where key is your own label; edges are [from_key, to_key] pairs and results_from names keys too. All or nothing: every problem (duplicate or unknown keys, cycles, a results_from the node does not depend on, a path outside the working directory, an unknown agent or kind, the working-directory and subplan rules) is reported in one error, and then nothing has been created. Plan fields as for create_plan; with a parent it becomes the subplan of that node. |
create_plan(title, description="", parent_plan_id=None, parent_node_id=None, working_directory=None, max_parallel=None, test_instructions=None) |
New empty plan → integer id (1, 2, 3 …). With a parent it becomes the subplan of that node. A top-level plan must name its working_directory; test_instructions is free text saying how the project's tests are run, see Test levels. Subplans inherit both and may not set them. |
update_plan(plan_id, max_parallel=None, test_instructions=None) |
Change a top-level plan's settings; only what you pass changes. max_parallel=0 removes the limit, test_instructions="" removes the instructions. Same as PATCH /api/plans/{plan_id}. |
add_node(plan_id, title, description="", agent=None, files=None, results_from=None, kind=None) |
Extend or correct a plan one node at a time → [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 node ids 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. |
update_node(plan_id, node_id, title=None, description=None, files=None, results_from=None, kind=None, agent=None) |
Change only what you pass, all or nothing. files / results_from omitted keeps the current value, [] clears it; they and kind change only before the node has run. agent="" clears the node's own agent so it inherits again; a new agent applies the next time the node starts. |
get_plan(plan_id), list_plans() |
Structure, status, results, progress. get_plan also shows working_directory, test_instructions (the top-level plan's, for a subplan too), 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. Creating, editing, deleting and testing profiles is setup work, done in the web UI's Agents panel or over REST (/api/agents), not over MCP. |
set_plan_agent(plan_id, agent) |
Default for the plan and its subplans; null clears it. |
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 MCP tools, so an agent can decide mid-run that its node is too big
and expand it into a subplan (
submit_plan(parent_plan_id=…, parent_node_id=…, nodes=…)); 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_submit_plan, …). These are a restricted set on their own endpoint, see Worker agents' MCP tools. - 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" (on a submit_plan node, add_node, update_node, 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. submit_plan checks it
against the edges of the same call; add_node accepts the list before the edges exist (you add
those next), so then the check runs when the edges are known: 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:
submit_plan(title, working_directory, nodes=[
{"key": "store", "title": "Store: add the column", …, "files": ["clearbox/store.py"]},
{"key": "engine", "title": "Engine: use it", …, "files": ["clearbox/engine.py"]},
{"key": "docs", "title": "Docs", "description": "Document the change in README.md …",
"files": ["README.md"], "results_from": ["store", "engine"]},
], edges=[["store", "engine"], ["engine", "docs"]]) # 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 (with the plan'stest_instructions, if it has them), so reviews get capabilityrun(read, search, shell; no edits) whatever the profile allows for normal runs, see Capability. - 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, status, description, result and step review — 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. An automatic and an on-demand review are
the same session: same prompt, same tools, same transcript (marked as a review), same record; an
on-demand one replaces a queued or running review of the same node or plan. Every reviewer ends
its reply with
VERDICT: ok | concerns | error
SUMMARY: <one or two sentences>
FINDINGS:
<bullet list, or "none">
Anything before the last VERDICT: line is dropped; a reply with no VERDICT: line at all is
recorded as error with the whole reply as its summary. 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) or over REST (/api/agents). MCP only lists them (list_agents), so a
planning model can pick one; setting them up is not in its context.
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. | "" |
Worker agents' MCP tools
A built-in worker never sees the /mcp endpoint your own session uses. When the engine starts a
built-in node it mints a random token for that run and hands the agent
http://127.0.0.1:<port>/mcp-agent/<token> instead; the token names the node and is revoked the
moment the run ends (finished, failed or stopped), after which the endpoint answers 404, as it
does for a token it never minted. That endpoint serves only what expanding a node needs:
submit_plan, create_plan, add_node, add_edge, remove_edge, remove_node, update_node
and get_plan, plus read-only list_agents and report_progress for the agent's own node. Every
plan argument must be inside the node's own subtree: submit_plan / create_plan attach a subplan
to the agent's own node or to a node of a subplan below it, and the other tools work only on those
subplans. There is no start_plan / stop_plan, no agent-profile admin, no review, rework or
working-directory tool, no claim_next_node / complete_node / fail_node, and no way to pick an
agent: submit_plan, add_node and update_node refuse an agent argument with an error.
Instead a subplan a worker creates gets the creating node's resolved agent profile as its plan
default, so expanding a node never runs the sub-work on a more capable agent than the node itself
had (see Which agent runs a node). Reviewers get no clearbox tools at
all; external agents claiming over /mcp are trusted like you. The token never appears in a
transcript; only a --mock server hands it out, on a debug route the tests use.
This closes the tool layer, not the machine: an agent with capability run or full has a shell
and can reach /mcp on localhost directly. The scoping protects read and edit agents, and
stops a prompt injection from steering an agent through its own clearbox tools; it is no sandbox
for a shell-capable agent.
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 say how the
project's tests are run: the plan's test_instructions verbatim when it has them (set them on
submit_plan, create_plan, update_plan or the 🧪 pill in the UI's topbar), otherwise an
instruction to work it out from the repository itself (README, CLAUDE.md / AGENTS.md, CI config,
the test files) rather than invent a harness. Either way the agent is told that any server it starts for testing
gets a free port, never the port of the Clearbox server running the plan. 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. They get the same
how-to-test text.
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.
A subplan that a worker agent creates by expanding its node starts with that node's resolved profile as its plan default, so its nodes run on the same agent as the node that made them unless you change it (see Worker agents' MCP tools).
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 has no
tool that creates or edits a profile, so a key never passes through a model's context. 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 worker's scoped clearbox MCP tools (vc_submit_plan, vc_add_node, …, see Worker agents' MCP tools) 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
POST /api/agents/{id}/test) 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) over REST, then tests
cheap (POST /api/agents/{id}/test). It builds the plan node by node, the incremental way, and
assigns the agents the three ways: set_plan_agent makes cheap the plan default (the inventory
nodes inherit it), add_node(..., agent="strong") pins the assessment, and
update_node(..., agent="strong") 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.
- The topbar's 📁 pill shows a top-level plan's working directory (click to change it before the
plan starts); the 🧪 pill next to it holds its
test_instructions(click to edit, any time). - 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. Plans take working_directory, so point it at the repo path and 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 |
--allowed-host NAME[:PORT] (repeatable) |
CLEARBOX_ALLOWED_HOSTS (comma-separated) |
loopback and --host only |
--db |
CLEARBOX_DB |
<data dir>/clearbox.db |
--workdir |
CLEARBOX_WORKDIR |
<data dir>/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 PATCH /api/agents/{id}. 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.
Security. The API has no authentication: whoever can reach the port can run agents with
shell access on this machine. It listens on 127.0.0.1 by default, and every request must be
addressed to one of the server's own names (127.0.0.1, localhost, [::1], the --host it
binds unless that is a wildcard, and each --allowed-host) or it gets a 421; a request whose
Origin is not one of those gets a 403. That keeps other web pages in your browser away from
it, including through DNS rebinding. Serving it under a name or on the network (--host 0.0.0.0,
which prints a warning) needs that name in --allowed-host, e.g. --allowed-host mybox.lan; a
name without a port is allowed with the server's port and without one (behind a reverse proxy).
Only do that on a network you trust.
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
prompts.py what a worker agent is told: system prompt, test levels, the per-node context
diffs.py before-snapshots of a node's files and the diffs taken from them when it finishes
reviews.py step and plan reviews: reviewer prompts, reply parser, the review session
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, and the scoped subset worker agents get
app.py FastAPI: REST, SSE live stream, MCP mounted at /mcp and the workers' /mcp-agent/<token>
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 POST /api/agents/{id}/test
probe.py tiny completion round trip when there is no /models listing
static/ UI (plain HTML/CSS and ES modules, no build step): app.js (entry: routing, plan
view, live feed), core.js (state, helpers), graph.js, drawer.js, agents.js
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/unit/ in-process unit tests of the scheduler and store: `uv run pytest`
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_agent_scope.py the workers' scoped MCP endpoint: subtree rule, inherited agent, token lifetime
tests/test_host_check.py Host and Origin checks: foreign names and pages refused, own names and the stream pass
tests/test_openai_harness.py the agent loop against tests/fake_openai.py (no network, no DB)
Where Clearbox keeps its data
The database and the fallback workspace live in the platform's data directory:
| OS | path |
|---|---|
| Windows | %LOCALAPPDATA%\clearbox\ |
| macOS | ~/Library/Application Support/clearbox/ |
| Linux | $XDG_DATA_HOME/clearbox/, else ~/.local/share/clearbox/ |
--db / CLEARBOX_DB and --workdir / CLEARBOX_WORKDIR override both. Before 0.2.2 these
defaulted to clearbox.db and workspace/ relative to the current directory, so launching from
two folders gave you two unrelated databases; a clearbox.db already sitting in the directory you
start from is still used, and the banner says so.
Every top-level plan must name its own working_directory -- the directory its agents work in,
normally a repository. Subplans inherit it. Two plans with overlapping directories cannot run at
the same time, since file-conflict locking only orders nodes within one plan tree.
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 unit tests (uv run pytest) and 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.3.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.3.0.tar.gz | 200.7 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| clearbox-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 368.3 kB
Release files / clearbox-0.3.0.tar.gz
| Download URL | clearbox-0.3.0.tar.gz |
|---|---|
| Size | 200.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f278cfdbeae372cf8cb839cfa9dacb7d172c6588271b5981f0fa11438d523c9c
|
|
BLAKE2b-256 checksum How to use checksums |
f719de52516d3c57609b2cd70b9f5ba9b424840b949c392124216a6eacf38331
|
| 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 24, 2026.
Transparency logRelease files / clearbox-0.3.0-py3-none-any.whl
| Download URL | clearbox-0.3.0-py3-none-any.whl |
|---|---|
| Size | 167.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eba9f2d0add86faf49506c0c219d9a46d0ed95d8af66095ff877499729e8b96f
|
|
BLAKE2b-256 checksum How to use checksums |
fc86c31f1ae08fc7088d68606c7027725cfd3757702556cc93eb5d2764fecf3d
|
| 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 24, 2026.
Transparency logSecurity for clearbox 0.3.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.3.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 24, 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.