The workspace filesystem
Sudo does not drive a catalogue of per-surface tools. He is handed the workspace as a directory tree and works on it with ordinary commands. That is why he needs no new tool when the product grows a new kind of surface: a new export folder is a new folder.
The mounts
/
├── workspace/ the configuration, exactly as the definitions export writes it [writable]
├── reference/ a generated example workspace, one well-formed file per surface [read-only]
├── docs/ one page per code-execution scope, plus the schema helper [read-only]
├── sdk/ the C# surface your workspace code is written against [read-only]
├── proc/ live runtime state, regenerated on every read [read-only]
├── session/ the assistant's own to-do list [read-only]
├── stage/ the files you uploaded to the Stage area, as text [read-only]
├── home/uploads/ the files you attached to this conversation [read-only]
└── tmp/ scratch, discarded with the session [writable]
Only two of them accept writes, and only a write under /workspace is a change to your workspace.
That is what makes diff and commit answerable by comparing one directory against the live
configuration.
Directories are synthesised from the file names rather than stored. The export is a flat list of
paths, and a tree kept beside it could disagree with it, so one list is the source of truth for
ls, find, grep -r and the diff alike.
/workspace — the configuration
The same layout as the workspace-definitions export zip, the WebDAV mount and the git-tracked configuration repository. Everything you configure by hand in the admin pages has a file here:
| Path | Surface | Format |
|---|---|---|
code/endpoints/ |
Custom endpoints | .cs |
code/chat-ai-tools/ |
AI tools | .cs |
code/agents/ |
Agents | .cs |
code/skills/ |
Skills | .md |
code/ai-assistants/ |
AI assistants | .cs |
code/ai-prompt-templates/ |
Prompt templates | .cs |
code/ai-providers/ |
Custom-code AI providers | .cs |
code/scheduled-tasks/ |
Custom-code scheduled tasks | .cs |
code/data-connector-tasks/ |
In-workspace data connectors | .cs |
code/migration-tasks/ |
Migrations | .cs |
code/indexes/code/ |
Code indexes | .cs |
code/indexes/search/ |
Search code indexes | .cs |
code/entity-post-processing/ |
Entity post-processing | .cs |
config/schemas/nodes/ |
Node schemas, with their access control | .cs |
config/schemas/edges.json |
Edge schemas | .json |
config/schemas/nodes/styles/ |
Node styles | .json |
config/schemas/nodes/rendererers/ |
Node renderers | .json |
config/indexes/others/ |
Other index settings | .json |
config/chat-policies/ |
Chat policies | .json |
config/secrets/ |
Secret declarations (names only, never values) | .json |
config/access-groups/ |
Access-group declarations (names only, never membership) | .json |
config/packages/ |
Installed NuGet package declarations | .json |
config/search.json |
Search settings | .json |
config/interface.json |
Interface settings | .json |
config/file-indexing.json |
File-indexing settings | .json |
config/chat-policy-enforcement.json |
Chat-policy enforcement settings | .json |
config/general.json |
General workspace settings | .json |
nlp/spotters/, nlp/patterns/, nlp/pipelines/, nlp/linking/ |
NLP configuration | .json |
A file whose path the classifier cannot name is a file commit would silently drop, so build
refuses it rather than passing it through.
Editing a file changes nothing until you approve it
Writes under /workspace land in the session's own overlay on top of the live configuration.
The workspace is untouched until you approve a commit.
A node schema is a C# class
config/schemas/nodes/<Type>.cs declares one node type. The class name is the node type, its
properties are the fields spelled the way the graph generates them — List<T> for a list field,
Dictionary<string, T> for a dictionary, List<List<T>> for a table — and the access control sits
on the thing it restricts:
[schema: Curiosity.Schemas.Key("InvoiceNumber")]
[schema: Curiosity.Schemas.AccessGroup("Fn8kQ2mVdRt4wYbLp9sXhC")]
//Access groups: Finance (Fn8kQ2mVdRt4wYbLp9sXhC)
public class Invoice
{
public string InvoiceNumber { get; set; }
[Curiosity.Schemas.AccessGroup("Au7dKm3vRp6nWxYbLt2sQe")] //Audit
public double Total { get; set; }
public List<string> Tags { get; set; }
}
| Attribute | Where it goes | What it does |
|---|---|---|
[schema: Curiosity.Schemas.Key("…")] |
the header | names the schema's key field |
[schema: Curiosity.Schemas.AccessGroup("…")] |
the header | restricts the whole node type to that access group |
[Curiosity.Schemas.AccessGroup("…")] |
a property | restricts that one field |
[schema: Curiosity.Schemas.InternalNodeType] |
the header | the file carries access control only |
A node type the workspace itself owns is not an administrator's to declare, so its file carries the internal marker, declares no schema, and lists only the fields that carry an access group of their own.
A group is named by UID, because that is what survives a rename. The //Access groups: line and the
comment beside a field's attribute are regenerated on every export and read by nobody — they are
there so a diff naming a group is readable. A node type nobody restricted carries no access
attributes at all.
Schemas were JSON, and their access control lived in the general settings
Both halves of one decision are now in the same file. The two dictionaries that used to hold
node-type and field access control in config/general.json are kept read-only, so a bundle
exported before the change still imports.
Access-group declarations
config/access-groups/<name>.<uid>.json is one access group's declaration: its UID, name,
description, whether it is hidden from the pickers users see, and the scope its UID was derived
from. Membership never travels. The people in a group belong to the workspace that holds them,
and a file rewriting them would hand one deployment's roster to another.
They exist because everything access-controlled — a node type, a single field, a code endpoint, an AI tool, an AI assistant — names its group by UID. Without the declaration, an import installs a restriction pointing at a group the workspace does not have, which fails closed: the restricted thing becomes unreachable for everyone. So they import before anything that can be restricted to one, and:
- a restriction naming a group the workspace lacks is still applied, and reported as a warning saying which group is missing and where to add it;
- a group that arrives new is reported too, because an empty group grants nothing until somebody is put in it;
- deleting an access group by importing a file is refused, because it would revoke access for everyone in it. Remove it under Manage / Access instead.
An import replaces a definition's access groups rather than adding to them, so a file that drops a group from an endpoint, an AI tool or an AI assistant's public access has that applied.
/reference — a worked example of every surface
A generated example workspace, built from the same content classes the real exporter uses, with one well-formed file per kind of surface. It is what Sudo reads when he is unsure what a file of a given kind looks like, instead of guessing from memory, and it is regenerated with the product, so it cannot drift out of date the way a hand-written template would.
/docs — the code-execution scopes
One page per code-execution scope:
what an endpoint's scope exposes, what an AI tool's does, what a data connector's does, plus the
generated graph-schema helper for your workspace's own node and edge types. The type command
serves the same pages for a single type.
/sdk — the C# surface your code is written against
The types and methods that custom endpoints, AI tools, connectors, indexes, scheduled tasks and
custom front ends are written against, as files grouped by area (Query/, Endpoints/,
Connector/, Search/, AITools/, FrontEndComponents/, …) with an index.tsv to grep.
It is a partial extract by design: signatures and documentation, with method bodies rendered as
{ ... } and non-surface members left out. The point is to let Sudo get a call right, not to ship
the product's source into a chat.
/proc — what the workspace is doing now
Every file here is generated when it is read, in the shape /proc has on Linux:
| File | Contents |
|---|---|
nodetypes, edgetypes |
node and edge types with counts, key field and field count (TSV) |
exceptions |
the 100 most recent recorded exceptions (JSON) |
metrics |
endpoint and AI-tool call metrics (JSON) |
audit |
recent configuration commits, who changed what and when (JSON) |
connectors |
data connectors and their last run (JSON) |
indexes |
indexes and their status (JSON) |
embeddings |
embedding models and settings (JSON) |
scheduled-tasks |
schedules and last outcome (JSON) |
migrations |
migration tasks, status and progress (JSON) |
access-groups |
access groups (JSON) |
logs |
the tail of the server's own log, newest last (text) |
runs/ |
one folder per thing this session ran: status, log, output, error |
Because it regenerates on every read, the same cat run a minute later shows how far a still-running
task has got, and jq works on it directly:
jq '.[] | select(.type == "System.NullReferenceException") | .message' /proc/exceptions
awk -F'\t' '$2 > 1000' /proc/nodetypes
Nothing under /proc is configuration and nothing here can be edited or committed.
/stage — the files you handed him
Files uploaded to the private Stage
area, served as their text rather than their bytes: a PDF's bytes are no use to a shell, and the
indexed text is what answering a question about the upload actually needs. A file nothing has
indexed yet reads as a note pointing at the extract command, rather than as an empty file that
looks like an empty document.
That is what makes "here is a CSV, build me a connector and a schema for it" a request he can start from.
/home/uploads — the files you attached to this conversation
A file attached to the chat with the composer's attach button is here under its own name, so "have a
look at the CSV I just sent you" is cat /home/uploads/customers.csv rather than a hunt for a node.
- Text and code read as the bytes you uploaded.
- A document — a PDF, a spreadsheet, a Word file — reads as the text the extractor gets out of it, produced on demand, so a file nothing has indexed yet is still readable.
- A
.zipis a directory. Its structure is read out of the archive itself, so it can be listed, walked and grepped without being unpacked into/tmpfirst.
The mount is read-only, like /stage: these are your files, not the workspace's configuration, and
nothing here is committable. They are stored as ordinary uploads of yours and are deleted with the
conversation, so an attachment does not outlive the chat it was meant for.
/session — his own plan
todo.md, rendered from the list the tasks command maintains. It is generated and never parsed,
and the mount refuses writes, so tasks is the only way to change it. A plan that could also be
half-edited with sed would have two sources of truth and no way to tell which one is current.
The list outlives the conversation, which is what makes a multi-step change survive a session that is picked up again the next day.
/tmp — scratch
Writable, and thrown away with the session. Nothing here is ever part of a commit.
Read next
- Commands — what the sandbox adds on top of bash.
- Reviewing and approving — what happens to a
/workspaceedit aftercommit. - Workspace definitions — the same file layout, as an export and a git repository.