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.shThe 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=falseplus aread-only grant for theownerandadminroles. 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
includerule 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'fromfs_*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 aperSourcemap of{ isRunning, status? }per slug, which the sources panel renders asOn / 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 withexecute, usingcwd: "<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 withfs_read machine:///config/Downloads/.... - Recordings —
machine_use action=recordalways writes into a recordings directory that is bind-mounted to the project data zone, so the result is addressable asdata://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.