What it is
CodePinion is a cloud development platform — repositories, sprint planning, workspaces, databases, deployments — where the assistant is not a sidebar that suggests things. It operates the platform. It plans a sprint, assigns the work, edits the code, runs the tests, provisions the database and opens the merge request, through the same service layer a human clicking the dashboard goes through.
The interesting engineering is not "can an agent call a tool." It is: once an agent can genuinely do everything a user can, what stops it from doing the wrong thing?
What inspired it
Two frustrations, both from building software on a slow connection in Nairobi.
The first: every AI dev tool we used stopped at the boundary where work actually happens. It would write the migration but not run it, describe the sprint but not create it, explain the fix but leave you to apply it across nine files. The last mile — the part that is tedious precisely because it is mechanical — stayed manual.
The second: the tools that did act were untrustworthy in a specific way. They asked for blanket permission once, then did things you could not see and could not undo. "Allow file edits" is not consent to a force-push.
So the goal became a platform where the agent has real reach, and where the friction is proportional to consequence rather than to how alarming a verb sounds.
How we built it
Backend — Django with Django REST Framework for the API and Channels for the chat socket, Celery over RabbitMQ for background work, PostgreSQL as the source of truth, Redis for the channel layer and short-lived caches. Kubernetes runs all of it.
The agent loop lives in the WebSocket consumer. Each turn the model may call tools, read results, and call again. Crucially, tools reach the same domain services the REST API uses — so an agent action cannot bypass a validation the dashboard enforces. Adding a capability means writing a tool over existing services, never a second write path into the database.
The tool registry currently holds 195 tools, each declaring a permission tier:
| Tier | Count | Gate |
|---|---|---|
| Read | 74 | Runs immediately — nothing observable changes |
| Write | 119 | One approval showing exactly what will happen |
| Irreversible | 2 | Typed resource name plus a one-time code emailed to the account holder |
Shell execution is deliberately Write tier, not exempt. A shell command can trivially do what any irreversible tool does — DROP DATABASE, rm -rf, a force-push — so giving it its own escape hatch would make every other gate decorative. The human sees the exact command text and approves once.
Model layer — 26 models across 6 providers (Google, Anthropic, OpenAI, NVIDIA, xAI, DeepSeek), including six Gemini models with Gemini 3.5 Flash as the free default. One resolution path: a user's own API key wins when present, otherwise the platform key is used and the call is charged against a rolling budget. Cost is recomputed from stored token counts rather than trusted at write time:
$$\text{cost}{\text{USD}} = \frac{t{\text{in}} \cdot p_{\text{in}} + t_{\text{out}} \cdot p_{\text{out}}}{10^{6}}$$
Provider rates refresh on a schedule behind an anomaly guard — a sudden price move must survive several consecutive fetches before it is applied, so one garbled upstream response cannot reprice the platform.
Workspaces are pods, not folders. A separate runtime service owns every container: it provisions the pod, mounts the working copy, starts language servers per detected toolchain, streams shell and file operations, and reclaims idle workspaces. Django holds no cluster credentials.
Audit — every Write and Irreversible call is recorded with its arguments, tier and outcome, so an organisation can reconstruct what the agent did and on whose authority.
What we learned
More tools made the agent worse, not better. Sending all 195 schemas every turn was expensive, but the real damage was accuracy: choosing among 195 options measurably degraded selection, and the agent once tried to create a workspace while the user was sitting inside one. We split the registry into seven scoped bundles chosen from the surface the user is on — 73 for planning, 26 for coding, 19 read-only — with everything else one find_tools call away. A narrow default works because escape is cheap.
Confirmation should be a negotiation, not a yes/no. Asked to delete duplicate tasks, the agent proposed thirty. Twenty-seven were right. With only approve/reject, the user rejects a call that was 90% correct and starts over. So a confirmation response can now narrow a call — the dialog lists every item with a checkbox, and the reply carries the surviving subset. The invariant that makes it safe is that a response may only ever narrow, never widen: an id the user was never shown is refused outright rather than filtered out. The typed confirmation carries the count, and the server re-derives it from the final selection — so unticking a row changes what you must type.
One bug shape kept recurring: the read surface emitting identifiers the write surface refused. Tools that list people returned user_id=7; the tool that assigns work matched only names and emails, so the model passed back the id it had just been given and was told to "use find_assignable_people to search" — the very tool that supplied it. Worse, a numeric value fell through to a substring email match, so "4" could match dev4@example.com and assign someone nobody named. The same shape appeared for branch names, epic bodies and team-owned repositories. Read and write halves of one concept drift apart quietly, because each half looks correct alone.
An availability listing is not proof of availability. Google's model-listing endpoint still returns the Gemini 2.5 family; a real generateContent call answers 404, "no longer available to new users." We had shipped a dead model on the strength of that listing, and the test meant to catch it passed because it compared the frontend against a hand-copied list containing the same wrong id. Every model id in the catalogue is now verified with a live call.
Stop the confused agent, not the slow one. An iteration cap punishes long legitimate work and repetitive nonsense identically. Our stall monitor watches for repeated tool-call signatures instead, so an agent making progress is left alone and one going in circles is ended.
Challenges we faced
A free tier that silently billed. We added five free Gemini models — priced at zero in the catalogue — and CI failed with "gemini-3.5-flash is free but was charged." Cost resolution prefers a live pricing table over the catalogue, and an old migration had seeded that model at \$1.50/\$9.00 when it was still paid. Worse, we had added the free models to the table that refreshes rates from an upstream marketplace, which quotes a real nonzero price for them — so within hours of deploy the free tier would have started billing against user budgets, silently, because nothing fails when a price merely exists. The fix was to delete the stale rows so free models resolve like every other free model, and to make the refresh job skip any model the catalogue marks free — enforcing the rule rather than trusting a hand-curated list to stay curated.
Guards that hold for one event and not two. A workspace's bootstrap effect depended on nine values, protected by a single one-shot "skip the next run" flag. Under load, more than one dependency settled after the flag was consumed, and the page re-bootstrapped. It only failed on a slow machine, which is the worst kind of bug: green locally, red in CI, green again on re-run.
Measuring resource numbers before shipping them. We deployed a job to pre-pull container images onto every node so workspaces would start faster. The images totalled 36 GB. Nodes hit disk pressure, the kubelet began evicting workloads, and because the job had been given broad tolerations and critical priority it kept pulling while evictions ran. It took production down. The lesson was narrow and permanent: never ship a change whose main risk is a number you have not measured.
Tests that agree with the bug. Migrating a description field to rich text, three call sites were missed — and the full suite passed, because every fixture wrote the old field. The tests encoded the same assumption the code did. Grep found what the suite could not, and we added invariant tests that compare read and write surfaces against each other rather than against fixtures.
What's next
Cross-repository agents that can reason over an organisation's whole codebase, an agent-discovery surface so teams can find and audit each other's agents, and taking the confirmation model further — narrowing works for deletes now, and the same protocol generalises to any bulk write.
Log in or sign up for Devpost to join the conversation.