Workspace anatomy
July 12, 2026 · View on GitHub
A workspace is one directory tree: every component, every approval, every piece of brokered state, and all derived runtime state live under a single root that xbind serves, watches, and supervises. This page maps that tree — what each path holds, who writes it, and from where it can be read — the on-disk half of the trust model.
Related: 01-model.md · 03-components.md · 07-users-orgs.md · 09-terminals.md · 10-resources.md · 14-lifecycle.md · /docs/elements.md · /docs/auth.md · /docs/resources.md · /docs/isolation.md · plans/DECISIONS.md · plans/orgs.md
The tree
<workspace>/
xbin.json # workspace manifest — machine-managed (grants, bindings, …)
AGENTS.md # builder guidance for agents; CLAUDE.md is a symlink to it
.gitignore # seeded by init (D2): .xbin/ data/ home/ homes/
go.work # generated by xbind — every Go component + the SDK
root/ # the entry page (GET / → /c/root/) — an ordinary component
shell/ # the workspace chrome (bx-shell + per-tile admin popovers)
tiles/ # workspace tooling tiles: admin console, tile manager, apidocs
apps/ # your applications (scopes + components)
welcome/ # shipped starter tile
o/<org>/… # org-owned subtree (positional binding, plans/orgs.md D19)
lib/ # shared library components
o/<org>/… # org subtrees may also sit at top level
homes/<user>/ # per-user terminal $HOME (seeded dotfiles; masked from others)
home/ # skeleton dotfiles seeded by init; legacy shared home (migrated)
data/ # brokered state — xbind-owned, never in git
.xbin/ # derived state + daemon credentials — never in git
vendor/ is a reserved name but not a directory you create: core web
elements and vendored frontend libraries (lit, xterm, marked) are embedded in
the xbind binary and served at /vendor/ (from the source tree under
--dev). A workspace is self-contained without fetching anything at runtime.
xbind init <dir> scaffolds all of this from the embedded template and never
overwrites an existing file; an empty bind mount auto-initializes on first
boot. Upgrades backfill only AGENTS.md (agents depend on it); everything
else in the template is yours after first seed.
Reserved and ignored names
| Set | Names | Effect |
|---|---|---|
| Reserved top-levels | .xbin vendor data home homes xbin ingress runtime | can never be components; not scanned or watched. ingress and runtime are reserved so no component path can impersonate the public-caller identity or the builtin ingress source (ING-5) |
| Reserved path segments | o, u (any depth, outside sanctioned positions) | o/<org> is the positional org marker (D19); u/ is held for future per-user tiles. Enforced for new tiles; existing dirs keep working and bx doctor warns |
| Ignored everywhere | .git node_modules deps __pycache__ and any dot-prefixed dir | never scanned, watched, or served as component internals (deps/ is xbind-materialized symlinks — 03-components.md) |
What makes a component, a scope, a tile
- A directory is a component when the scan finds an
xbin.jsonor anindex.htmlin it. Nothing else is required; a bareindex.htmlis a valid static component. - A directory is a scope when it contains
scope.json. Every component belongs to its nearest-ancestor scope ("" = the workspace scope). Scopes own resource namespaces and are the trust unit for same-scope auto-grants (06-authorization.md). - A tile is a component the shell surfaces; there is no separate tile object on disk.
Manifest errors are non-fatal: a component whose xbin.json fails to
parse (or declares an invalid exposes section) keeps serving statically,
and the error surfaces in bx ls, bx doctor, and the status API — a broken
manifest never takes the workspace down.
Two manifests, two authors
Component xbin.json — agent-authored
jsonc (comments welcome), written by whoever edits the component. It contains
only declarations: runtime/entry, deps, uses (grant requests),
expose.roles (its callable surface), interfaces/provides/exposes
(typed slots), setup (env layer script), template metadata. Declarations
are inert until the owner approves the matching grant or binding
(01-model.md §Security philosophy). Full field reference:
/docs/elements.md.
Workspace xbin.json — machine-managed
Rewritten whole (marshal → temp file → atomic rename) by xbind on every change; comments do not survive, and hand-edits, while honored on the next scan, are still capped by policy ceilings at evaluation (D20). Its fields are the approval record:
| Field | Holds | Changed by |
|---|---|---|
schema | manifest version | xbind |
importMap | workspace-level import map | owner (by hand is fine here) |
grants | {from, target, role} rows — the capability table | grant APIs / bx grant |
resources | workspace-level resource declarations | owner |
bindings | bindings[component][slot] → provider ref(s); a bare ref is a plain string, an ingress binding carries route config {ref, host|zone|listen} (ING-1) | binding APIs / bx bind / bx expose |
ifaceInstances | runtime-registered provider instances | the provider itself (PUT /iface-instances) |
ingressHosts | zone-bounded hostnames a tile self-registered (ING-2) | the tile itself (PUT /ingress-hosts) |
lifecycle, lifecycleAt | non-default component states + timestamps (LC-1) | lifecycle API |
Because this file is plain JSON in the tree, the workspace's entire capability surface is greppable and git-diffable.
data/ — brokered state (xbind-owned, never in git)
| Path | Contents | Written by |
|---|---|---|
data/users.json | users, orgs/teams, policy rows; Argon2id password hashes; mode 0600 | xbind (user/org APIs) |
data/kv.db | bbolt store backing kv resources | xbind (kv API) |
data/resources/<scope~key>/ | sqlite files, filesystem/blob directories per scope | broker provisions; granted same-scope backends get direct rw paths |
data/resources-enc/<scope~key>/<name> | gocryptfs ciphertext for encrypted file-backed resources | gocryptfs (via xbind) |
data/vault/.barrier.json + data/vault/<comp~key>.json | the encryption barrier + per-element secrets (encrypted at rest when the barrier is unsealed; plaintext only under --insecure-vault) | xbind (vault API) |
data/prefs/<user~key>/<comp~key>.json | per-user, per-component UI preferences | xbind (prefs API) |
Keys are path-flattened (apps/calendar → apps~calendar, plus a short hash
for component keys). data/ sits outside every terminal's view (masked and
Landlock read-denied, 09-terminals.md) — no shell, however
privileged inside its sandbox, can read the user table or another element's
secrets. See 10-resources.md for the resource and vault
model itself.
.xbin/ — derived state and daemon credentials (never in git)
| Path | Contents |
|---|---|
.xbin/token | the owner token (0600; rotatable at runtime) |
.xbin/secret | HMAC key for frame tokens |
.xbin/run → | symlink to a tmpfs run dir holding gateway.sock and each backend's per-generation listen socket |
.xbin/log/<comp~key>.log | backend stdout/stderr (bx logs, the terminal logs tab) |
.xbin/build/<comp~key>/bin | Go build outputs |
.xbin/cache/ | shared build caches (go-build) |
.xbin/env/<comp~key>/<hash>/ | built setup environment layers, keyed by script+rootfs hash |
.xbin/term/<key>/ | persistent per-tile terminal dev layers (overlay upper + base-image pin). Keyed by tile, not by user — shared across every user's terminals on that tile (09-terminals.md §How tiles and terminals share the filesystem) |
.xbin/resenc/<scope~key>/<name> | decrypted gocryptfs mountpoints for encrypted resources |
.xbin/docs/ | the builder docs extracted to disk so terminals read them as files ($XBIN_DOCS) |
.xbin/builtins.json, .xbin/builtins/<id>/ | builtin-update provenance + pristine base snapshots (14-lifecycle.md) |
Why the run dir must be tmpfs: it is bind-mounted read-write into every
backend sandbox (the listen socket lands there), and a sandbox must never
receive a writable mount backed by host disk (plans/isolation.md) — only
tmpfs, gocryptfs, or read-only. xbind picks systemd's RuntimeDirectory
(/run/xbin), then $XDG_RUNTIME_DIR, then $TMPDIR, and leaves the
symlink at .xbin/run for discoverability; the short path also keeps unix
socket paths under the kernel's 108-byte limit.
Most of .xbin/ is safely deletable derived state — the exceptions are
token/secret (identity: deleting them mints new ones and logs everyone
out) and term/ (users' installed dev environments).
homes/<user>/ — per-user terminal homes
Every human gets their own $HOME for terminals at homes/<user> (D6, as
amended): seeded lazily on first terminal with skeleton dotfiles
(.zshrc/.bashrc/.bash_profile from the embedded template), never
overwritten afterwards. In a terminal sandbox, homes/ is masked and only
the session user's own home is mounted back in — agent credentials in one
user's home are invisible to every other user's shells. A legacy shared
home/ from older workspaces is migrated to homes/<user> on boot when the
target user is unambiguous; the migration refuses to guess if both forms hold
real data. The home/ directory that init still seeds holds the pristine
skeleton copies.
The git model
Two layers of git, per D2 ("auto git init, never auto-commit"):
- The workspace repo — created by
xbind init, ignoring.xbin/,data/,home/,homes/(the seeded.gitignoreadds the home dirs to D2's original.xbin/+data/). Versions workspace-level files; commits are always yours. - Per-component repos — every component gets its own repo (
git init -b main+ an initial commit; idempotent, applied at boot and after any structural change like imports or template instantiation). This is what makes a component a self-contained unit: terminals commit inside the tile they're scoped to, builtin updates diff against recorded base snapshots, template instances carry atemplateremote pointing at their (materialized, read-only) upstream blueprint so fixes can be pulled, and backups/clones travel with history. Git treats the nested repos as boundaries, so workspace-repo history and component-repo history don't interleave.
Who writes what, who can read it
The tree at a glance, as a trust table (enforcement details: 08-sandbox.md, 09-terminals.md):
| Path | Written by | Readable from |
|---|---|---|
<component>/ source | terminals/agents on the editing plane (needs the tile access level, D16) | own backend: read-only bind of its own dir; other components: only via a code:/code:<comp> grant; terminals: tiles below your read level are mount-masked (D17a) |
workspace xbin.json | xbind (grant/binding APIs) | readable everywhere the workspace is — the capability table is deliberately visible |
data/resources/<scope>/ | broker + granted same-scope backends (rw bind) | not from terminals (masked); cross-scope only through service APIs |
data/users.json, data/vault/, .xbin/token, .xbin/secret | xbind only | nowhere else: masked in terminals and Landlock read-denied even if a mask were peeled |
homes/<user>/ | that user's terminals | only that user's terminals (on any tile); masked for everyone else |
.xbin/term/<tile>/ (dev layer: apt, /etc, stray writes) | terminals on that tile — any user with terminal access | later terminals on the same tile, including other users; never a backend (09-terminals.md) |
.xbin/log/, .xbin/build/, .xbin/env/ | xbind (runner) | logs via the gated logs API/tab; the rest is daemon-internal |
.xbin/run (tmpfs) | xbind + backends (sockets) | bind-mounted into sandboxes; sockets only |
The pattern to remember: source is the editing plane's, state is the broker's, credentials are xbind's — and every read across those lines goes through an API that asks the policy first.