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

The loop a configuration change takes: the graph is exported as files into the session, Sudo edits and builds them, commit stages a diff and stops, the admin approves it, and an ordinary definitions import applies it back to the graph.
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

Connected knowledge for AI systems