One runtime across app boundaries

September 19, 2026 ยท View on GitHub

createWaymode in @mossburgh/waymode/core owns the loop. It imports no DOM, React, Node, network client, or framework. Platform adapters supply an AppSurface: observe available actions and public state, invoke an existing handler, check that an observation is still current, and retire its handles. The browser entry point wraps this same runtime with the DOM adapter.

flowchart LR
    Request[User request] --> Runtime[waymode runtime]
    Web[Web controls and navigation] <--> Runtime
    Native[Native controls and navigation] <--> Runtime
    Backend[BFF or API operations] <--> Runtime
    Backend --> Existing[Existing auth, validation and handlers]
    Existing --> State[Saved state]
    State --> Runtime

This diagram is the architecture, not a support claim. The DOM adapter is tested. The bounded OpenAPI adapter has contract tests; live host outcomes need separate proof. Native controls, Next server actions and TanStack server functions still need adapters and live proof.

A feature has one definition

This is the target contract. Automatic discovery of arbitrary action schemas remains unsupported; adapters consume a contract supplied by the host. The normal app owns its operation's input contract, label, handler and authorization. An adapter derives agent choices from that existing definition. waymode owns no second feature registry. Adding or changing a definition changes the next observation; removing it invalidates old handles. A generated manifest is a build artifact, not a second set of handwritten tools.

OpenAPI is one input format for an adapter. It is not the public abstraction and it is not required of every app. A framework adapter can read its existing typed router instead. The OpenAPI specification provides a language-independent description of HTTP operations. A Rust or Python service can therefore share the same HTTP adapter; this repository has not yet run integration tests against either language.

App shapeAdapter boundaryProof still needed
React webLive DOM plus local handlersAdditional custom controls
Next or TanStack StartWeb UI plus the framework's normal server interfaceServer-action/server-function discovery
React plus Rust APIWeb UI plus the existing HTTP contractLive Rust service
Next BFF plus Python APIWeb UI plus BFF operations; BFF calls Python normallyLive BFF and Python service
React NativeNative views/navigation plus its authenticated backend interfaceNative adapter and iOS/Android device runs

A BFF continues to own orchestration and user permissions. waymode should call the boundary used by that client; it must not route around the BFF to a privileged internal service. A mobile client may expose different actions from the web client because its permissions, state and device capabilities differ.

React Native provides accessibility labels, roles and state. Those can describe native actions; they are not by themselves an invocation or portal implementation. The native adapter must bind the app's existing handlers, router and view lifecycle. A desktop browser at mobile width is not a native test.

Current code shape

import { createWaymode } from "@mossburgh/waymode/core";

const agent = createWaymode({
  surface: adapter,
  decide,
  settle: waitForAppEffects,
});
await agent.run("Enable compact layout");

adapter, decide and waitForAppEffects are host integration points. The browser entry point supplies createBrowserSurface; server code can use the experimental OpenAPI adapter. Both use the same runtime. combineSurfaces joins connected surfaces. createSurfaceSession and the browser createHttpSurface carry operations across a host-owned HTTP endpoint. The host must authenticate that endpoint and bind each surface to the current user; composition does not supply authorization.

A skill can install that integration once, and a build check can report features that have no discoverable interface. Those installer/build checks are planned. Neither the runtime nor a lint rule can infer the permissions or intent of arbitrary private functions. Apps without a usable UI or action contract need a one-time integration at their normal action boundary.

First backend adapter: bounded support

createOpenApiSurface reads the current OpenAPI 3.1 document for each observation. It supports JSON-body operations with finite boolean/enum/const inputs, nested objects, operations with no body, and explicit host/user-supplied inputs validated against the API schema. It preserves schema field descriptions in model choices.

It reports gaps for path/query/header parameters, unsupported schemas, unbounded inputs without a supplied value, more than 64 candidate calls, and oversized choice descriptions. Free-text generation, large catalogs, resource selection, streaming and file uploads are not proven. This is not arbitrary backend coverage.

Bind document, readState, authorize and dispatch once to the authenticated app session. authorize filters discovery and runs again before dispatch. It must handle a catalog check with no input as well as concrete inputs. dispatch invokes the normal router; that router must revalidate authorization, resource ownership, input and any version condition at the actual write. Snapshot checks cannot replace a database transaction or optimistic concurrency check.

Only expose model-safe operations, argument values and state. Explicit backend inputs are included in the decision context; do not supply secrets. The adapter does not infer data classification from arbitrary schemas. Model confidence grants no permission. A confirm policy returns confirmation-required without invocation; the app's existing consent flow must obtain permission.

Handles expire after 30 seconds and are consumed before dispatch. Schema or state changes invalidate pending actions. There is no automatic retry after an uncertain write. Invocation receipts mean the handler returned, not that the user's entire goal succeeded. The next observation reads fresh state, and evals query the saved record independently.

Outcome judgment is part of the loop

The model selects an action and judges whether fresh observations satisfy the complete user request. The app supplies facts: saved API state, rendered controls, view location, and errors. A successful response or an invoked handler alone is not completion. A combined request needs evidence for every part, such as the saved dark preference plus settings actually mounted in chat.

The current runtime asks for done or another action on the next observation. It rechecks freshness before accepting completion. This is a probabilistic semantic judgment, not an authorization decision or an independent correctness oracle. Evals compare it with exact saved-state and rendered-state assertions. False completion, partial completion and safe abstention need separate grades.

Current evidence

Use the eval contract to check each connected adapter. No current aggregate live score is asserted here.

Release gate

Freeze the SDK and integration. Add, rename, change inputs on, and remove an action in ordinary app code. Run the same requests from web and native clients. Verify exact saved state, unchanged unrelated fields, navigation, permissions, stale handles, cancellation and confirmation. Retain failures, timing, costs and source hashes. Record the public video only after those intended claims have live evidence.

External agents and computer use

createRemoteSurface adapts the same session protocol for an external process or another host-owned transport. It has no browser or model dependency. Supply an authenticated transport and preserve the host's error types and cancellation:

import { createRemoteSurface } from "@mossburgh/waymode/core";

const surface = createRemoteSurface((operation, data, signal) =>
  sessionClient.request({ operation, data }, { signal }),
);

The host must bind createSurfaceSession to the current authenticated owner. Discovery grants no permission. The server checks the snapshot, binds supported inputs, and invokes its existing handler. Retired or changed handles cannot be reused. Backend input binding remains the host's responsibility; this transport does not accept unvalidated caller arguments.

The browser's createHttpSurface uses this same adapter. MCP, WebMCP, and desktop screenshot/pointer controllers are not included. An external agent can also use its own browser controller for the normal UI.

An external transport must preserve cancellation, expiring handles, confirmations, and the current user's permissions. A denied operation must stay denied; switching to computer use is not permission to route around it.