@machine-core

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.

RouteMethodsFeature gatePurpose
sessions/*GET / POST / DELETEmachine.readProbe, 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 / DELETEmachine.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).
healthGETmachine.readDocker 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. resolveOsUri deliberately returns the container-internal path (e.g. machine-alice:///config/x.md → /config/x.md) instead of throwing OsUriUnresolvableError — the host-side translators only re-map host-anchored connectors, never the sandbox. This is the opposite of a local connector, which raises OsUriUnresolvableError when 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 projectId at construction, so two projects that share a slug name (e.g. the default machine-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) so fs_* 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 :3101 stream 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 — init receives the live ctx.config reader, 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.

On this page