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

SetNamesEffect
Reserved top-levels.xbin vendor data home homes xbin ingress runtimecan 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 segmentso, 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 dirnever 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.json or an index.html in it. Nothing else is required; a bare index.html is 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:

FieldHoldsChanged by
schemamanifest versionxbind
importMapworkspace-level import mapowner (by hand is fine here)
grants{from, target, role} rows — the capability tablegrant APIs / bx grant
resourcesworkspace-level resource declarationsowner
bindingsbindings[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
ifaceInstancesruntime-registered provider instancesthe provider itself (PUT /iface-instances)
ingressHostszone-bounded hostnames a tile self-registered (ING-2)the tile itself (PUT /ingress-hosts)
lifecycle, lifecycleAtnon-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)

PathContentsWritten by
data/users.jsonusers, orgs/teams, policy rows; Argon2id password hashes; mode 0600xbind (user/org APIs)
data/kv.dbbbolt store backing kv resourcesxbind (kv API)
data/resources/<scope~key>/sqlite files, filesystem/blob directories per scopebroker provisions; granted same-scope backends get direct rw paths
data/resources-enc/<scope~key>/<name>gocryptfs ciphertext for encrypted file-backed resourcesgocryptfs (via xbind)
data/vault/.barrier.json + data/vault/<comp~key>.jsonthe 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>.jsonper-user, per-component UI preferencesxbind (prefs API)

Keys are path-flattened (apps/calendarapps~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)

PathContents
.xbin/tokenthe owner token (0600; rotatable at runtime)
.xbin/secretHMAC key for frame tokens
.xbin/runsymlink to a tmpfs run dir holding gateway.sock and each backend's per-generation listen socket
.xbin/log/<comp~key>.logbackend stdout/stderr (bx logs, the terminal logs tab)
.xbin/build/<comp~key>/binGo 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 .gitignore adds 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 a template remote 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):

PathWritten byReadable from
<component>/ sourceterminals/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.jsonxbind (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/secretxbind onlynowhere else: masked in terminals and Landlock read-denied even if a mask were peeled
homes/<user>/that user's terminalsonly 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 accesslater 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.