Sudo: seventy-four tools, replaced by one shell
Rafael
Computerwelt, part 3 of 4. Part 1 was the port, part 2 the library. This post is the thing we built it for. Part 4 is how a workspace builds one of its own.
Sudo is the admin-facing assistant in Curiosity Workspace: an always-on chat beside the admin pages, for reading, designing, changing and operating every configurable surface of a workspace. He is also a near-black pixel-art cat with electric-blue ear tips, and he is walking along the bottom of this page, keeping pace with how far you have read. Click him for a jump, twice for a startle. He sits down at a laptop for the sections where the machinery is actually running.
This post is about the tool he has, which is one tool.
What he used to have
Sudo carried a catalogue: introspect.<surface> to read one, propose.<surface> to draft
one, test.<surface> to check one, apply.* to commit one — for endpoints, AI tools,
agents, prompt templates, four kinds of scheduled task, two kinds of index, node schemas,
edge schemas, node styles, node renderers, four NLP families, six search sub-settings,
access groups, access grants, UI settings, embeddings settings and skills.
Seventy-four tools. Plus six sub-agents (Planner, Coder, Reviewer, Operator, Verifier,
Knowledge) whose actual job was routing between the seventy-four. Plus a ChangeManifest,
a ProposedChange type, an HMAC approval-token manager and a set of static reviewer checks.
It worked, in the sense that it did what it was designed to do. Two things were wrong with it.
It grew when the product grew. Every new configurable surface meant four new tools, a new manifest arm, and a new reviewer check. The catalogue was a second copy of the product's shape, maintained by hand, always slightly behind.
It only answered questions somebody had already thought of. "Which endpoints reference
an agent that no longer exists?" is not introspect.endpoints. It is a grep across two
folders and a set difference: three lines of shell, and no line of the catalogue.
None of it exists any more.
The observation that replaced it
The workspace configuration already round-trips through the definitions export as plain files. That is what the export zip is, what the WebDAV mount is, and what the git tracking is: one flat list of paths, one per definition, each holding that definition's source.
So it is handed to Sudo as a filesystem, and he changes it the way anyone changes files:
reads them, edits them, compiles them, and asks for the change to be applied. One tool
(shell, taking one script parameter), one sandbox, one way into the workspace.
A new export folder is a new folder. Nothing has to learn about it.
What is mounted
The sandbox is a Computerwelt session (bash
and Python in-process, over one virtual filesystem), and the filesystem is an IFileSystem
implementation backed by the graph rather than by a disk:
| Mount | What | Writable |
|---|---|---|
/workspace |
the configuration, exactly as the definitions export writes it | yes — this is where edits go |
/frontend |
the workspace's own front end, as source, one branch at a time | yes — applied as a git commit, never as a definitions import |
/reference |
a generated example workspace, one well-formed file per surface | no |
/docs |
one page per code scope, plus the generated graph schema helper | no |
/sdk |
workspace/ the product's own C# surface, tesserae/ the UI library's component pages |
no |
/proc |
live runtime state, regenerated on every read | no |
/session |
his own plan — todo.md, and the JSON it is generated from |
no — one command is the only writer |
/stage |
files an admin uploaded to the Stage area, each reading as its text | no |
/home/uploads |
files attached to this conversation; a .zip is a directory |
no |
/artifacts |
documents he has written for this admin to open | yes |
/tmp |
scratch, discarded with the session | yes |
Three of those are worth stopping on.
Directories are synthesised from the file names rather than stored. The export is a flat
list of paths; a tree built beside it could disagree with it. One list is the source of truth
for ls, find, grep -r and the diff.
/workspace is an overlay. The base map is re-read from the live exporter before every
command (so an endpoint somebody edited in the admin UI two minutes ago is the one Sudo
sees), with the session's own writes and deletions layered on top. Only the overlay is
persisted.
/proc is the observability tools, retired. Each file delegates to the same method the
matching tool used to call, and the map is rebuilt per listing so every read is fresh. That
is why those tools could be deleted rather than kept alongside: cat /proc/nodetypes is the
tool, and it composes with grep.
And /sdk/tesserae is not baked into the product at all. It reads the Tesserae version out
of the workspace's own front-end repository, fetches that .nupkg, and serves the skills/
folder inside it, which is the mechanism from
an earlier post, arriving here as a mount. The pages
describe the version that workspace actually compiles against.
The loop
sudo@workspace:~$ grep -rl "Triage" /workspace/code
/workspace/code/chat-ai-tools/AI9kR2mQ....cs
/workspace/code/agents/AGtriage11....cs
sudo@workspace:~$ sed -i 's/Agents.Triage/Agents.Intake/' /workspace/code/chat-ai-tools/AI9kR2mQ....cs
sudo@workspace:~$ build
1 file changed, compiled clean.
sudo@workspace:~$ commit "Point the ticket tool at the renamed agent"
commit does not commit. It stages a diff and stops. What reaches the workspace is what an
admin read and approved, in a Monaco diff, deliberately read-only. Editing the draft there
would put the admin's change somewhere Sudo cannot see it, and leave the file still in his
shell silently disagreeing with what was applied.
Six invariants hold that loop together.
1. No silent writes. Edits live in the session's copy of /workspace until commit
stages a diff. Only approval applies anything, and what is approved is the diff. A commit
carries an id, so a card left open while a newer commit was staged refuses rather than
applying something nobody read. The staged commit is stored with the session, so it survives
a restart, a failover and the session cache being dropped.
That last clause is there because it once was not true. The proposal lived only on the in-memory session while the edits behind it came back from the node, so a 30-minute idle sweep could take the proposal and leave the changes. It is now stored as a snapshot, the exact bytes, rather than as a pointer into the changed files, because Sudo carries on editing after staging, and what is applied has to be what was reviewed.
2. Build before commit. commit parses every changed file back through the importer's
own reader and compiles its body against the scope it will run in, and refuses the whole set
if anything fails.
The compile sees the workspace as the whole change set leaves it, not as the graph currently holds it: the changed files are read into a pending-definitions overlay that the code generators lay over the real ones. So an AI tool may reference an agent added beside it in the same commit, and code still naming an agent that the same commit deletes stops compiling. It is the same question asked in both directions. It is also diagnostics only: nothing that runs is generated from the overlay, and an overlaid generation is never cached.
3. One way in. An approved commit is an ordinary definitions import of the files that differ, routed by the same routing that reads an uploaded bundle. One git commit per approval. There is no second path that applies configuration.
4. The data is read-only. The graph commands read as the admin who is chatting, drop
node types marked never-returnable, and run through three guards: transactions blocked,
never-returnable types removed, and emitted nodes capped at 50, with the cap adding a
<name>-Truncated entry rather than letting a partial answer read as a whole one. The query
command interprets a fluent query chain against a derived list of methods whose arguments
are literals; it never compiles C#. Sudo changes how a workspace is configured. An admin
assistant that can rewrite its customers' records is a different and much worse product.
5. No ambient anything. No host disk, no process, no network: the properties from part 2, unchanged. The sandbox ships no network commands, and the session withholds the names anyway:
"curl", "wget", "nc", "netcat", "ssh", "scp", "sftp", "telnet", "ftp",
"ping", "host", "dig", "nslookup", "traceroute", "rsync", "git", "watch",
None of those exist in the library today. Withholding them is for the day one does, so it cannot be acquired silently by a package upgrade.
6. Sessions are per chat. Keyed by the conversation, so two admins in two conversations never share a working directory or an uncommitted edit.
Resuming a session is not restoring a snapshot
The session node stores only what the session changed: the written files, the deleted paths, the working directory, the command history, the to-do list, and the proposal waiting for an admin. Everything else is re-exported from the live workspace every time the session is opened.
That is a deliberate difference. A snapshot would show Sudo the workspace as it was when he last looked; re-exporting shows him the workspace as it is, with his own uncommitted edits on top. An endpoint somebody else changed in the admin UI meanwhile is the one he sees.
Two clocks, and neither of them is the proposal's:
| Limit | Value | What it decides |
|---|---|---|
| idle timeout | 30 min | when the in-memory session is dropped and re-read from its node — a cache only |
| retention | 3 months | how long the node itself is kept |
A staged commit has no clock. It waits until the admin answers it.
What the limits are set to
The sandbox is built with the ordinary builder from
part 2, and the profile is Default with three caps
moved:
ExecutionLimits.Default with
{
Timeout = TimeSpan.FromSeconds(120),
MaxCommands = 100_000,
MaxOutputBytes = 4_000_000,
}
A command here can legitimately walk the whole configuration, so it gets more room than the
profile meant for untrusted input. Every cap is still a cap, and the wall clock is what
actually bounds a runaway loop. MaxOutputBytes is doing a second job as well: what a script
prints is what the model reads, so it is a context budget as much as a resource limit.
What it cost
Less code, and a smaller thing to keep true. Seventy-four tools, six sub-agents, a manifest type, a proposed-change type, an approval-token manager and a set of reviewer checks are gone. What replaced them is one tool, one filesystem, and a skill per surface telling him how to write that surface's file.
The per-chat "allow configuration changes" switch is gone too, and that one is the clearest sign the shape is right. The switch could not strip any tool (taking the shell away would leave Sudo unable to read the workspace either), so all it ever did was refuse a commit the admin meant to make, in a session they had forgotten the setting of. The sandbox is what makes "nothing reaches the workspace without approval" true, rather than a promise a setting has to enforce.
The last post in this series is the part that generalises: the same mounts, the same interpreter, the same session model, declared by a developer in a workspace for their own agents. Part 4.
Read next
Articles on context graphs, enterprise search and industrial AI