Inspiration

We've tried AI chatbots with no way to see what it actually looked at, what it tried, or what it changed. A couple of us had already run into that with coding assistants specifically: it edits a file you didn't expect, or you can't tell if a failed run actually did nothing or did something halfway. When we got a Codex-based agent platform with zero identity, zero audit trail, and zero access control as the starting point, that was the gap we wanted to close.

What it does

Clanker-in-the-middle extends the CodeJam starter kit with the middleware needed to run it with more than one user.

Accounts, ownership, and sharing

Issue: With only one shared password protecting the platform, anyone holding it has full read, write, run, and delete access to every Agent on it, with no way to tell whose Agent is whose.

Feature: Real user accounts, per-Agent ownership, and revocable read-only or run-only sharing with specific users.

Impact: A user's Agent, and everything it's touched or been told, stays inaccessible to anyone else by default. Access is opt-in and scoped, and revoking it takes effect immediately, mid-conversation.

Per-Agent file access control

Issue: With one platform-wide setting controlling file access, every Agent has the same ability to overwrite or delete files as one explicitly built to run commands and write code.

Feature: File access (read-only or read-write) is set per Agent, enforced by Codex's own OS-level sandbox rather than an app-level setting.

Impact: An Agent locked to read-only is incapable of modifying anything, even under a prompt deliberately trying to talk it into writing. The block happens at the kernel, not at the model's judgment.

Run history and tracing

Issue: With a Run's activity living only in memory while it happens, a crash, a kill, or a server restart wipes that record entirely with no way to check what it actually did.

Feature: Every Run leaves a permanent, append-only trace of its lifecycle - when its thread started, every message the Agent produced, and how it ultimately ended (completed, cancelled, errored, or reconciled after a restart).

Impact: A Run's outcome is no longer a black box the moment the process exits. You can see what the Agent actually said and how the Run concluded, not just a final status with no explanation behind it.

Crash and disconnect recovery

Issue: With no reconciliation on restart, a dropped connection, a server restart, or clicking Stop at the wrong moment destroys it, silently discarding the conversation and any completed work with no way to get it back.

Feature: Interrupted Runs are reattached to a still-running container or checkpointed on restart, and the next message resumes exactly where the Agent left off.

Impact: The platform can be restarted, disconnected, or interrupted mid-task without it costing real work.

Multi-agent orchestration

Issue: A single Agent working alone has to handle an entire task itself, unable to hand off a sub-task to another Agent better suited for it without the user manually creating one and copying context between separate conversations by hand.

Feature: An Agent can hand off part of a task to another existing Agent, or create a new specialised Agent if none of the roster fits, and resume with that Agent's real output. Chains and Agent creation are both capped so a bad loop can't run forever, and every hop scoped to whichever user actually triggered the task.

Impact: A task can be decomposed and delegated across a tree of Agents instead of one Agent faking the others' responses, and that orchestration can only ever reach Agents its triggering user was already permitted to access, no matter how many hops deep the delegation goes.

Per-Agent token budgets

Issue: With no cost ceiling, an Agent can run indefinitely at whatever cost it racks up.

Feature: A configurable per-Agent token budget that pauses the Agent cleanly once reached, and un-pauses automatically the moment the budget is raised.

Impact: Runaway spend on one Agent is capped without anyone needing to babysit it, and hitting that cap is a clean, recoverable pause rather than a dead Agent that has to be rebuilt.

Retrieval-Augmented Generation (RAG) private and shared documents

Issue: Retrieval had no boundary at all between what belonged to one Agent's own workspace and what was meant to be visible platform-wide. it could pull in anything sitting in scope indiscriminately, including the platform's own internal scaffolding files.

Feature: Documents can be uploaded privately to a single Agent's own workspace, or published to a shared library any Agent can draw from, kept in physically separate storage, with a confidence signal on every retrieved answer.

Impact: What's private to one Agent's workspace stays there, what's published to the shared library is deliberately shared and nothing else, and an Agent's answers no longer leak details about the platform's own internals.

Input and output sanitisation

Issue: Without validation, a crafted file name can escape its storage directory or silently collide with a reserved Windows device path, a single NUL byte in a chat message can crash a running task outright, and an unthrottled login endpoint means every account is brute-forceable.

Feature: Resource names, chat content, and login attempts are all validated and rejected before they reach anything that trusts them.

Impact: A crafted filename can no longer escape its intended storage location, a malformed message can no longer take down a live Run, and no account can be cracked open by unlimited password attempts.

How we built it

We started by reading the starter kit itself, its architecture doc, the AgentRunner/AgentService code, and its own "Intentional limitations" section. After identifying areas to focus on, we ran its baseline acceptance test to confirm those gaps first-hand, then split the work into three tracks matching the brief's recommended middleware directions: access control, observability/reliability, and RAG.

  • Observability wraps AgentRunner, the interface AgentService calls to actually run a turn. We wrote a class that implements the same interface and wraps whichever concrete runner is underneath it, composed once in a factory function. AgentService just gets handed whatever that factory returns; it has no way to tell if it's a bare runner or a wrapped one. If a trace write fails, that failure is caught inside the wrapper itself, so it can't turn a real Run's success into a reported failure.

  • Access control lives inside AgentService, not the routes or the UI. Every method that touches an Agent checks ownership and Grants before doing anything, so a new route gets the same protection automatically as long as it goes through the service layer instead of the store directly. Grants aren't cached, they're checked on every request, so revoking one takes effect on the next call, not the next login.

  • RAG hooks into sendMessage, before the prompt reaches the runner. It chunks and embeds workspace files, shared resources, and older messages, then merges the top matches into the prompt. The stored user message stays as-is. What gets sent to the runner and what gets saved to history aren't the same string.

For testing, automated tests cover the core logic. Access-control tests run against a real AgentService and Fastify instance via app.inject(), so granting "viewer" access and sending a write request exercises the actual assertAccess() check. sandbox-enforcement.test.ts spins up a real Docker container running Codex's Landlock sandbox and has it attempt a real write, checking that it's blocked at the OS level, not just that the right flag was passed.

We also did manual testing against the live running container. RAG's internal-file leak was found by asking a live Agent about its own workspace and seeing it name AGENTS.md directly. A separate bug (uploads disappearing on every docker compose down && up) was only visible by docker exec-ing into the container and finding the files there but nowhere on the host. Crash recovery was verified by killing a running container mid-task, restarting the server, and watching the task resume and finish on its own. There's also a scripted manual walkthrough (scripts/demo-access-control.sh), two accounts, curled through deny → grant → revoke for demonstrating access control live against the real server.

Challenges we ran into

  • Multi-agent orchestration and the access control system were built on separate branches by different people. When the branches were merged, any user could get their own Agent to delegate to another user's Agent and get real output back, with zero authorisation involved. The fix scoped every delegated Run to whoever triggered it, and routed delegation through the same ownership and Grant checks as a direct message.
  • One reliability fix was verified only against a mocked runner at first, with the test suite passing. A passing test suite wasn't enough to actually trust it, so it still needed someone to kill a real container mid-task and confirm the reattach logic worked against a live process before it could be called done. These changed how we treated "passes the test suite" as evidence, resulting in more time being spent to conduct manual verification.

What's next for Clanker-in-the-middle

A few gaps are known and documented: Grants currently apply to a whole Agent, not individual messages, so a viewer sees the entire conversation history rather than a scoped slice. Self-signup is fully open with no invite or approval step. And on the reliability side, a local-process Agent still can't be reattached after a hard crash of the server itself, only checkpointed and resumed as a fresh continuation, not truly re-attached mid-execution.

Another direction is pushing multi-agent orchestration further. Currently a delegation tree is strictly hierarchical and always resolves back to whichever user triggered the root task. A shared, multi-user workspace where several people's Agents coordinate directly, with its own access rules for what one user's Agent can see of another's, is the next step.

Built With

Share this project:

Updates