@machine-core

Machine sources

The machine filesystem source and how agents exchange files with the sandbox.

The machine's container filesystem is not a special case — it is a regular source in the platform's URI-native filesystem, contributed by machine-core's webtop connector kind and served through brain-core's sources and connectors pipeline. Once a machine session is live, agents use the standard fs_* tools against the source's URI scheme:

fs_list   machine:///config/              # the user's home inside the machine
fs_read   machine:///config/Downloads/report.pdf
fs_write machine:///config/tmp/script.sh

The container root (/) is the URI root, so machine:///etc/hosts addresses the container's /etc/hosts.

Connector declaration

The manifest declares one source connector of kind webtop with the URL scheme machine. Its default scope is user, and the scope is selectable across user, project, and agent — a user-scoped source for Alice might persist as the slug machine-alice, producing URIs like machine-alice:///config/notes.md. All scoped slugs share the same connector implementation; the slug is the identity.

The connector advertises all thirteen capabilities of the port — list, read, write, delete, move, exec, scanDelta, walk, count, peekCount, scanContent, subscribeEvents and resolveOsUri — over the standard connector port. Its config schema is closed and empty: a machine source needs no connection settings, because the binding to the live container is resolved at runtime from the active session.

Path permissions

The connector declares a default permission matrix (neuralis.connectors[].defaultPermissions), from which the attach form takes the new source's initial read / write / exec triple. What is enforced afterwards is the persisted source config, not the declaration: brain-core resolves each call against that config — default, then the per-role / per-user / per-agent overrides, then any paths[] rules the config itself carries — and it is editable per source like any other URI policy.

Two consequences are worth knowing before reasoning about who can read a machine source:

  • A source created outside project scope — the machine default is user scope — is seeded default: read=false plus a read-only grant for the owner and admin roles. Which principal holds the working grant depends on the scope you picked: at user scope it is the person who attached the source (read/write/exec); at agent scope it is that agent (read and write, no exec) and no user grant is written at all, so the human who attached it reaches the desktop only through that agent. Access is therefore decided at the source level: any other principal gets nothing from that source, on any path, and an owner or admin can read someone's desktop but not write to it.
  • The declared matrix's per-path rows split in two. The allow rows are a baseline for the kind and are not copied into a new source config, so read a source's effective policy from the source itself rather than from the connector declaration. The restrict-only floor rows below are the exception: they are copied, and they cannot be removed afterwards.

Secret paths under /config are a floor

The desktop's /config volume holds the browser profile — cookies, session tokens, saved logins — plus the TLS key the container generates for itself and the usual dotfile credentials. Those paths carry an immutable policy floor: fs_read, fs_list and fs_search answer 403 uri_policy_denied for every caller, including the person who attached the source. A floor is applied after every other layer, so no role grant and no override rule re-opens it, and no in-app surface can remove it — the policy route answers 409 immutable_policy_floor and the next boot restores anything that went missing. Covered: the Chromium, Chrome and Firefox profiles and their HTTP caches, /config/ssl (the self-signed key nginx writes on first boot), the NSS certificate and private-key store — .local/share/pki, where a current Chromium on this image actually keeps key4.db, plus the legacy .pki spelling — .ssh, .gnupg, .vnc, the login keyrings, .aws, .azure, .kube, .docker, .netrc, .git-credentials, .npmrc, .pypirc, and the gcloud / gh / rclone config trees. The desktop session material is covered on the same grounds: the D-Bus session addresses (.dbus, .XDG/dbus-1), the accessibility bus (.XDG/at-spi), both dconf settings stores, the PulseAudio directory (its authentication cookie sits beside two dozen mode-600 siblings), Thunar's custom-action file, and both ICEauthority files. One of those — .config/dconf/user — is a world-readable file inside a private directory, which is exactly why the floor has to name it rather than trusting the file mode.

Each covered directory is declared twice — once for the folder itself and once for its contents — so listing the parent does not reveal the folder name. That pairing is deliberate: a glob written only as <dir>/** matches what is inside the directory and not the directory node, which would leave the name enumerable and turn a listing of the folder into an empty 200 instead of a 403.

Matching happens on the canonical URI. A policy pattern compiles to a whole-URI regular expression, so the evaluator canonicalizes both the pattern and the URI under test first — slash runs collapsed, . and .. segments resolved — and every way of spelling one path therefore reaches the same verdict.

This is permanent and it applies to your own desktop. If you deliberately want an agent to read a key file out of a sandbox you own, the filesystem tools are not the path. Webtop shell through execute is outside the filesystem URI-policy gate; credential-handling constraints still apply. The floor closes the route that reaches other users' listings and the vector index.

What gets indexed

Two independent inputs decide it, and they are different kinds of thing:

  • Sync excludes — a preference. The list the attach form pre-fills covers the system trees (/bin, /proc, /usr, …), the caches and /config/.logs/, which keeps the index to the files a desktop user actually works with. It is editable per source, and it was not applied retroactively to sources attached before a pattern shipped. It is not a security boundary.
  • The read floor above — security. What cannot be read cannot be embedded: every path the floor denies is also never newly embedded, on every machine source including ones attached long before the floor shipped, and no include rule re-opens it. There is no separate exclude list to keep in step, and no editable setting that can undo it.

Excludes govern what gets indexed and counted, not what a directory listing returns — an excluded file is still visible to fs_list and is simply never embedded. A floored file is neither.

Neither input deletes history: content embedded before a pattern or a floor arrived stays stored, though it is filtered out of every read and every search result for every caller. To drop it from storage, re-create the source.

No host path inside the sandbox

resolveOsUri on a machine source returns the container-internal path: machine-alice:///config/x.md resolves to /config/x.md. This is deliberate. The sandbox has no host-side path to expose — the connector neither fabricates a host path nor reports the location as unresolvable, because the container-internal path is the truthful coordinate. Host/container path translation only applies to connector kinds that legitimately straddle both environments (the local kind); a machine source never participates in it.

Live state and the auto toggle

A machine source is only readable while its Webtop session is running. Machine-core registers a liveness probe with brain-core at bootstrap, which powers two things:

  • enabled: 'auto' on the source config resolves against the live session state — a stopped machine reads as disabled (code: 'source_disabled' from fs_* tools) without anyone flipping a switch, and comes back automatically when the session starts.
  • The health route (GET /api/packages/@neuralis/machine-core/health) reports a perSource map of { isRunning, status? } per slug, which the sources panel renders as On / Off / Auto · Live / Auto · Stopped.

Exchanging files with the sandbox

The source is the file-exchange channel between agents and the desktop:

  • Into the machine — fs_write machine:///config/... writes a file the desktop user (and any desktop app) can open immediately; a typical pattern is writing a script there and running it with execute, using cwd: "<source>:///config" and a relative script path.
  • Out of the machine — anything the user downloads or saves in the desktop lands under /config/ and is readable with fs_read machine:///config/Downloads/....
  • Recordings — machine_use action=record always writes into a recordings directory that is bind-mounted to the project data zone, so the result is addressable as data://machine-core/recordings/<file> even after the machine session stops.

Prefer fs_* over shell file access

execute({ command: "cat foo.md", cwd: "<source>:///config" }) works, but fs_read machine:///config/foo.md is the right call: it is policy-gated per path and role, audit-logged as a filesystem access, and indexed. Reserve the shell for actual command execution.

On this page