API & usage
machine_use, URI-routed execute, the HTTP route surface and its feature gates, the webtop source connector, and the typed cross-package API.
This page is the code-is-truth reference for everything machine-core exposes:
the drive tool and connector shell, the HTTP routes the widget and the brain use,
the webtop source connector, and the typed cross-package API. For the
tool argument deep-dive and the action catalogue see
the machine_use tool and the
tools overview; this page focuses on the
reach surfaces an integrator or agent author touches.
Drive and shell entry points
machine_use is machine-core's embedded tool. Its uri argument selects a source;
omitted, it keeps its existing first-accessible-source selection (user, then agent,
then project scope), with the bare machine fallback when none is configured.
An explicit URI is checked against the caller's accessible sources.
Shell uses agent-core's execute, with an exact registered source URI in cwd.
There is no machine-specific action or target field and no source-free fallback for shell.
machine_use — unified action × target
Purpose. One tool drives the whole sandbox. It dispatches on an action
discriminator across a target of chromium (the CDP-attached browser, the
default) or desktop (the full XFCE session). The action enum covers
navigation (goto), input (click, type, key, scroll, select, drag,
hover), capture (screenshot, record), reads (read_text, read_dom,
read_a11y, read_combined), desktop ops (windows, activate_window,
launch_app, cursor_position), waits (wait, wait_for_selector,
wait_for_text), and a batch composite that runs atomic ops in a
single call — 25 by default, admin-raisable to 50.
Feature gate. The declared baseline is machine.read for read-class
actions; everything that mutates escalates to machine.drive inside the
handler. The schema's x-neuralis.requires records this as
{ features: ["machine.read"], escalations: ["machine.drive", "filesystem.observeScoped"] } — the required feature depends on the action
argument, so it cannot be a single static declarative gate, and starting a
machine that is not yours (a kept container or an existing profile) needs
filesystem.observeScoped when the existing lifecycle ownership/scope predicate
requires it. Starting a new or own profile does not unconditionally need that grant. The full per-action gating split lives on
the machine_use page.
{ "uri": "machine-user://", "action": "goto", "target": "chromium", "url": "https://example.com" }Webtop shell through execute
Call execute with command and cwd: "<source>:///config". The source slug is
project-specific; machine is a default suggestion, not a special scheme. Source root
<source>:// means /. The platform checks core.execute, the declared entry exec.machine and the source's own exec policy on the cwd; the provider then checks current source scope and
machine lifecycle authority, then uses the existing in-container shell. Stop ends the wait and,
on a machine running the current desktop image, the command's whole process group with it. The
caller's own env and background: true are served when that image supports them and announced
with the remedy when it does not; no platform ticket or active-skill credential environment is
ever projected to Webtop.
Authenticated external MCP clients can use this connector path without an agent stream; app/host shell and skill activation still require one. See shell parameters and the execute contract.
The result contains bounded output, exit state, source and cwd with plane: 'connector'.
Caller identity, command and session key remain in the provider's audit trail.
Route surface
machine-core's src/routes/ declares HTTP handlers discovered by the package
loader and dispatched through the host's generic /api/packages/machine-core/…
catch-all. Each route declares a feature that the host gates against the
caller's real role and grants before the handler runs — deny-by-default.
| Route | Methods | Feature gate | Purpose |
|---|---|---|---|
sessions/* | GET / POST / DELETE | machine.read | Probe, start, stop or delete the caller's own machine for a source, and list every machine the caller can see (GET sessions/overview). POST starts the kept container, or creates one on first use. DELETE sessions STOPS and keeps everything; DELETE sessions/container removes the container (the profile volume stays) and ?purge=1 there destroys the profile too — on a stopped machine only (a running one answers 409; stop it first). A purge flag on the plain stop is refused with 400, never ignored. |
session/:key/stream/* | GET / POST / PUT / PATCH / DELETE | machine.read (+machine.drive for stream control) | Direct-API proxy of the desktop stream — re-parses the session key, re-runs the shared authorization ladder, classifies the path, and injects the stream credential server-side so the caller never sees it. The workspace widget reaches the stream on the companion surface instead (below). |
health | GET | machine.read | Docker availability + the caller's project active-session count and per-source isRunning map (used by the widget and the brain's 'auto' probe). |
Owner and project isolation are enforced inside the handlers, not just by the
feature gate: sessions resolves through an owner-scoped probe so a member can
only see, stop, delete, or purge their own machine (a non-owner DELETE
returns 404, not 403, so the machine's existence is never confirmed, and the
overview reports someone else's machine as none rather than as occupied); health
builds its per-source map only from the caller's project; and the stream proxy
runs every request through the shared authorization predicate described below.
A caller-supplied uri is validated against the caller's
scope-filtered accessible-source set — an out-of-scope slug is rejected, so a
member cannot spin up an arbitrary-named container. When the host has no Docker
socket the session routes answer 503 with a docker-unavailable phase rather
than failing opaquely.
Surfaces that are not package routes
Two of machine-core's HTTP surfaces sit outside the package-route
dispatcher, on the platform's companion HTTP server (the MCP HTTP port, default
3101): the stream's HTTP proxy mounted at /machine/<sessionKey>/stream/…,
and the WebSocket bridge that takes over the relative upgrade at that same path.
The workspace widget points its iframe there, because the stream client loads its assets
and opens its socket with same-origin relative URLs that cannot carry a query
string. The session/:key/stream/* package route above proxies the same stream
for direct API callers.
All three surfaces run the same project → machine.read → owner → ready →
path authorization predicate, in that order and deny-by-default. Owner is checked
before readiness on purpose, so a non-owner never learns whether the session
exists. The last step is an allow-list over the stream's endpoints: viewing (the
dashboard, status, screenshots, downloads from the desktop folder) needs
machine.read; control (upload, recording, print, session management,
signaling) needs machine.drive; any other route under the stream's API answers
403 — or closes the socket 4403 — while a read outside it only reaches the
dashboard's static files. The one data socket carries both pictures and input, so without
machine.drive it is opened in the stream server's own view-only role, and the
client can never choose that role itself: the proxy forwards only the path and
query the predicate returns. The companion mounts are implemented inside machine-core rather than
in the host precisely so that gate travels with them — a generic host-side
wrapper would have dropped the feature check. Port and network layout:
deployment.
The webtop source connector
machine-core contributes a webtop-kind source connector
(WebtopConnector) that backs the machine-*:// URIs. It implements the
ConnectorPort contract — list,
read, write, delete, move, exec, scanDelta, walk, count,
peekCount, scanContent, subscribeEvents, and resolveOsUri (all
thirteen) — routing filesystem calls through the live MachineFsClient for the
project's slug. peekCount is the one method that never makes the trip: it
always answers "no cheap estimate", so callers fall back to a real count.
Two properties matter for integrators:
- No host
os_uri. Webtop sources live inside the sandbox container, so there is no host-side path to surface.resolveOsUrideliberately returns the container-internal path (e.g.machine-alice:///config/x.md→/config/x.md) instead of throwingOsUriUnresolvableError— the host-side translators only re-map host-anchored connectors, never the sandbox. This is the opposite of alocalconnector, which raisesOsUriUnresolvableErrorwhen a host path cannot be derived. - Project-scoped, fail-closed. The connector is constructed once per project
(brain-core builds one filesystem infra per project) and captures
projectIdat construction, so two projects that share a slug name (e.g. the defaultmachine-user) can never cross-resolve each other's container filesystems. When no live session is registered for the slug, filesystem methods throw a typed "no running machine session" error (or returns empty for list-class calls) sofs_*tools surface the missing prerequisite cleanly instead of silently succeeding against a placeholder.ConnectorPort.exec(request, context)is separate: it requires the real session and may start the machine after scope, feature and lifecycle checks; filesystem reads acquire no startup authority.
Typed cross-package API
machine-core does not expose model-facing tools through MCP for internal
callers — cross-package calls use the typed getPackageApi() adapters per the
platform's direct-internal-API rule. machine-core's
own wiring runs in both directions.
Connector-owned runtime state.
The webtop connector reads ctx.packageState() and answers isRuntimeActive() for its own (projectId, source) binding. The brain uses that exact connector for enabled: "auto" resolution, so a same-named source in another project cannot flip its enabled state. This read has no startup authority; authenticated connector execution retains the existing scope, feature and lifecycle gates. Runtime-gated liveness remains separate from URI policy and its display tone.
Inbound — the platform calls machine-core. The package publishes its own
typed API, resolved through the same getPackageApi() mechanism, and it is
consumed rather than decorative:
- Companion mounts — the two entry points that install the
:3101stream proxy and WebSocket bridge described above. The platform runs a generic mount loop and injects only ports; the gating stays in the package. - Platform configuration —
initreceives the livectx.configreader, so an edit to its configurable values reaches the running package without a process restart. - Managed containers and shared-network resolution — the generic container substrate machine-core builds for its own Webtops is reused by agent-core to run stdio MCP servers in a managed sidecar container on the same Docker network, instead of a second copy of the same plumbing.
- Session, stream, driver and audit accessors plus the Docker-availability reason, so a caller can surface an honest degraded state rather than a generic failure.
No method on this surface accepts a SessionContext — it is infrastructure
wiring, not a second way in. The one place it touches identity is the companion
mount, which resolves the caller from the request itself and then applies the
very same stream predicate the package route uses.
Audit trail
Every machine_use call is logged with its action, target, session key, and
caller identity; every Webtop shell call is logged with the command line.