# Rust page provider contracts
Owner: [ADR 0077](../../../../docs/adr/0077-rust-page-provider-ownership.md).
State: provider Windows fixture verified; consumer Rust handoff and positive-grant acceptance pending.
## Dependency and API ownership
```text
consumer page commands/schema/lifecycle
-> moirai_pal::wasm::{WebViewBridge, WebEventListener, WebPermissions, WebNumber}
-> actual browser WebIDL and event loop
consumer native host/profile owner
-> moirai_pal::windows::webview::{WebViewConfig, WebViewHost}
-> actual WebView2 controller/environment and owner-thread message pump
consumer build orchestration
-> moirai_transport::process::{ProcessSpec, ProcessSupervisor}
-> child-only OS working directory and contained process lifecycle
```
No provider imports Metis messages, commands, backend calculations or page
policy. Public types export through their existing crate/module facade; private
leaves own their respective operation domains. `webview_limits.rs` owns the
message byte/unit/event and operation wait constants shared by native/WASM.
## Message and callback law
`WebViewBridge::current` checks the actual native EventTarget/method. Absence is
Unsupported; invalid shape and thrown native operations retain context. Posting
validates supplied JSON before parsing and native serialization, then calls
postMessage with its object value. Receiving accepts actual object/string data
and validates its complete serialization. Admission requires UTF8 bytes and
UTF16 units each at most the shared 64 KiB limit. These bounds do not claim to
cover JSON parser or serializer private allocations.
One `WebViewMessageListener` owns the document's native-message callback.
Successful explicit close detaches before releasing the Rust closure. A failed
close leaves the handle active; a failed Drop moves its existing ownership to
one counted realm guard. New attachment returns WouldBlock while an owner or
guard exists. Browser destruction releases the remaining realm.
`WebEventListener` applies the same detach-before-release rule to DOM events.
On the first failed Drop, let A be the finite number of already-admitted live
owners. Further admission stops; retained owners R obey 0 <= R <= A. Ownership
transfer uses each existing node, and realm destruction drains the list without
recursive stack growth. Counts report actual active and retained ownership;
failed removal never claims a callback was freed.
`WebNumber::decimal` calls actual Number.toString with radix ten. WebIDL Number
is binary64; no precision conversion or application formatting algorithm is
introduced. Browser decimal/exponent thresholds, signed-zero and nonfinite
spelling remain the browser's computed result.
## Permission law
`WebPermissions::current` acquires one current Document's shared registry.
`request(WebPermissionKind, Duration)` returns an owned future; zero duration
rejects, while a positive deadline becomes min(caller duration, 30 seconds).
The timer starts before the actual API request. A provider bound from a prior
document rejects new requests. There are exactly four admission slots:
Geolocation, Camera, Microphone and Notifications.
For each slot, the transition law is:
```text
Idle -> ActivePending -> ActiveSettled -> Idle (consumer observes result)
\-> DetachedPending -> Idle (actual late settlement)
ActiveSettled -> Idle (consumer drops an unobserved result)
```
ActivePending cancellation first releases the actual watch, then retains its
settlement guard. ActiveSettled cancellation drops any acquired lease.
Per-document counts obey active_requests <= 4, geolocation_watches <= 1 and
unsettled_guards <= 4. Monotone checked generations never wrap; stale or duplicate
callbacks cannot settle a later request. The returned snapshot also reports
media leases/tracks and actual realm geolocation dispatchers.
The actual watchPosition first result/error clears its native watch before
settlement. [Geolocation sections 6.4–6.7](https://www.w3.org/TR/geolocation/)
remove watch IDs independently from already queued callbacks and permission
waiting. Therefore two realm-owned Rust dispatchers remain after first use,
until document destruction. Native bind attaches a never-reused nonzero u64
key encoded as a canonical decimal string, preserving every bit through JS.
The dispatcher removes its pending record before clearing/settling. Late or
duplicate bound callbacks find no newer owner under that key.
A cleared watch may not callback again. Its one DetachedPending guard then
keeps Geolocation busy until document destruction. This is reported ownership;
it is not a completed cancellation claim. Pending records retain only native
registration/registry state and a weak request consumer, with no page elements
or location data. The provider publishes no coordinates.
Camera/microphone use one getUserMedia algorithm whose kind selects video-only
or audio-only constraints. An actual stream becomes `WebMediaLease`; its drop
stops every native track. A late successful acquisition after cancellation
creates then drops that lease without publication. Unsettled getUserMedia and
requestPermission Promises have no abort parameter; each kind's guard remains
busy until genuine settlement or document destruction.
[Media Capture](https://www.w3.org/TR/mediacapture-streams/) owns track lifecycle.
[Notifications](https://notifications.spec.whatwg.org/) returns exactly granted,
denied or default. Unavailable APIs, actual rejection with native code/name/
message, grant, denied and default remain distinct outcomes. No grant or denial
is inferred from a timeout, a completed count or a missing device.
## Native directory, profile and exit law
`ProcessSpec::working_directory` stores an owned absolute child directory.
Spawn validates native representation and an existing directory before opening
process resources. Unix Command.current_dir and Windows CreateProcessW apply
that directory to the child; the parent cwd does not change. OS failure retains
the operation and native code. Relative executable lookup is platform-specific,
so deterministic consumers select an absolute executable.
`WebViewConfig::with_user_data_folder` requires an existing empty local drive
directory, bounded native Unicode representation, no relative/link/reparse
component and no overlap with mapped package resources. The caller creates and
exclusively owns both directories; the provider revalidates before COM creation
and checks the runtime's effective profile. Runtime overrides fail admission.
Permission callbacks set actual DENY, request nonsaved profile decisions and
mark the request handled. The fresh profile supplies independent per-request
native evidence; an old shared profile does not establish that evidence. Native
policy does not grant permission for positive browser fixtures.
The SDK [permission Uri](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2permissionrequestedeventargs#get_uri)
reports the requesting origin, while WebMessageReceived.Source reports the
document URL. Actual PR 619 CI observes `https://metis.invalid/` for denial and
`https://metis.invalid/index.html` for a page message. Two same-document API
calls produce one native denial and two browser errors in that run; call counts
do not imply native callback counts. The repaired fixture navigates to a second
packaged document after the first actual native/browser completion under the
same profile, origin and controller. It still requires two actual denials and
two actual errors.
The repaired Windows job [115537873626](https://github.com/ryancinsight/Moirai/actions/runs/38467480001/job/115537873626)
passes at [reviewed source](https://github.com/ryancinsight/Moirai/commit/0d42510b50cec901852124ddbdcd0727632ebcb9), observing the real two-document
denials/errors, browser exit and profile cleanup. This provider evidence does
not establish the Metis Rust module handoff or four-kind native page acceptance.
The run passes 25 native policy/destruction tests and four actual capture
fixtures, including two 100-host cases, with WebView2 `153.0.4234.48` and SDK
bindings `0.39.1`. The repeated two-document fixture completes in 7.410 seconds.
Fresh-profile close registers BrowserProcessExited for the actual nonzero
browser PID before closing callbacks/controller. The owner-thread pump observes
the matching normal exit within the existing operation deadline, then releases
the environment/window. Cleanup attempts continue after errors; the first
failure remains observable on repeated close. The provider does not remove the
caller-owned directory. Default profiles retain controller-only close behavior.
## Executable acceptance
- Portable state/key tests inspect exact slot counts, busy errors, media sums,
cancellation/settlement, stale generations, decimal identities around 2^53
and u64::MAX, overflow and deadline boundaries.
- Shared message/cwd tests inspect actual computed length/admission and native
child directory/output without global-directory mutation. Rust browser tests
inspect JSON values/bounds, real DOM registration/removal and Number results.
- Installed Windows geolocation requests across two packaged documents under
the same profile/origin/controller must produce two actual
native DENY events and two browser errors, then actual browser exit and caller
profile cleanup. Source [reviewed source](https://github.com/ryancinsight/Moirai/commit/0d42510b50cec901852124ddbdcd0727632ebcb9) passes the actual
Windows job above; this macOS host supplies no local Windows execution claim.
- Consumer acceptance must use the actual Rust-ready native page and four
native permission kinds, then stop/remount with exact reported resources.
Unexpected grants, unavailable required APIs and deadlines fail acceptance.
- With explicit human consent in an ordinary browser and real devices, observe
grants and every native track stopping on lease drop and late acquisition;
check real watch clearing and notification granted/denied/default. Ignored
requests retain their reported guards and block replacement until settlement.
Native/WASM/Windows warning-denying diagnostics and minor compatibility checks
are separate gates. No source/property test substitutes for the actual native
or consented positive fixtures; no provider-private allocation total is claimed.