Paperbark 0.2.0-alpha.16.1.c9da1a1

This is a prerelease version of Paperbark.
dotnet add package Paperbark --version 0.2.0-alpha.16.1.c9da1a1
                    
NuGet\Install-Package Paperbark -Version 0.2.0-alpha.16.1.c9da1a1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Paperbark" Version="0.2.0-alpha.16.1.c9da1a1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Paperbark" Version="0.2.0-alpha.16.1.c9da1a1" />
                    
Directory.Packages.props
<PackageReference Include="Paperbark" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Paperbark --version 0.2.0-alpha.16.1.c9da1a1
                    
#r "nuget: Paperbark, 0.2.0-alpha.16.1.c9da1a1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Paperbark@0.2.0-alpha.16.1.c9da1a1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Paperbark&version=0.2.0-alpha.16.1.c9da1a1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Paperbark&version=0.2.0-alpha.16.1.c9da1a1&prerelease
                    
Install as a Cake Tool

<p align="center"> <img src="site/public/brand/paperbark-lockup.png" alt="Paperbark — lightweight WebView wrapper for .NET" width="760"> </p>

Paperbark

Paperbark is a small package for turning an ASP.NET Core application into a native desktop application. Add the package, choose the first route, and keep using the hosting, routing, dependency injection, and web stack you already know. Its deliberately minimal API covers the essentials: windows, dialogs, titles, and closing a surface with a result.

This repository is an architecture spike, not a production-ready framework. The native host supports macOS arm64 and x64. A Windows arm64/x64 WebView2 host is available as an architecture spike; Linux remains follow-up work.

Developer experience

Register Paperbark, configure an application-relative startup route, and run the application exactly like any other ASP.NET Core application:

using Paperbark;

var builder = WebApplication.CreateBuilder(args);
builder.AddPaperbark(options =>
{
    options.Route = "/desktop";
    options.Title = "My application";
    options.Width = 1280;
    options.Height = 800;
});

var app = builder.Build();
app.MapGet(
    "/desktop",
    () => Results.Content("<h1>Hello from ASP.NET Core</h1>", "text/html"));

await app.RunAsync();

AddPaperbark binds Kestrel to an ephemeral IPv4 loopback port and installs the Paperbark HTTP session boundary. A hosted lifecycle service waits until Kestrel has its actual address, extracts or resolves the native host for the current runtime, and launches it with a private control channel. Paperbark therefore does not replace ASP.NET's normal startup or shutdown model.

Native quit confirmation

Confirmation is opt-in and does not depend on browser focus or DOM events:

builder.AddPaperbark(options =>
{
    options.CloseConfirmation = new PaperbarkCloseConfirmationOptions
    {
        Title = "Quit application?",
        Message = "Quitting ends the current session and may interrupt running commands."
    };
});

The native dialog has Cancel (initial focus, Enter, and Escape) and an explicit Quit action. On macOS the application menu provides Cmd+Q; application quit and the primary window's close button share the policy. On Windows Alt+F4 and the primary close button are intercepted by FormClosing, before WebView disposal. Only confirmed quit ends the host and stops ASP.NET Core. Repeated quit requests cannot stack confirmations; cancel leaves the session alive and restores focus.

Closing a secondary window or modal dialog only dismisses that surface. With an application modal open, macOS Cmd+Q presents confirmation above that sheet; Windows Alt+F4 on the modal dismisses the modal, not the application. A primary close request on Windows can present confirmation owned by the active modal. Cancel preserves the modal and its pending result. New window/dialog requests are rejected while quit confirmation is pending.

CloseConfirmation defaults to null, preserving existing close behavior. Title and message must be nonblank. The configuration travels in the authenticated initialize.closeConfirmation object (title and message), not on the command line. Use a matching native host build; an older NativeHostPath override cannot enforce this option. Explicit managed shutdown, control-channel loss, parent loss, startup cleanup, and native smoke-test completion bypass interactive confirmation. Windows system shutdown also bypasses confirmation. MainWindow.CloseAsync is a user-facing close request and observes the policy; its completion acknowledges the request, not the user's eventual decision. Linux native hosting is not implemented.

Await application work before native close

PaperbarkOptions.CloseHandler is an opt-in asynchronous decision policy on macOS and Windows. It runs after the configured native confirmation, off the control reader. Cancel never calls it. Returning approval authorizes closure; it is not an observation of closure. Keep CloseConfirmation configured when work needs an explicit Cancel/Quit decision:

builder.AddPaperbark(options =>
{
    options.CloseConfirmation = new()
    {
        Title = "Quit application?",
        Message = "Save the session state and quit?"
    };
    options.CloseDecisionTimeout = TimeSpan.FromSeconds(30);
    options.CloseHandler = async (request, cancellationToken) =>
    {
        // sessionStore is an application-owned service. Key durable work by request.Id.
        var result = await sessionStore.CommitCloseAsync(
            request.Id, request.WindowId, cancellationToken);
        if (!result.Succeeded)
        {
            await notifications.ShowCloseFailureAsync(result.Error);
            return PaperbarkCloseDecision.Veto;
        }
        return PaperbarkCloseDecision.Approve;
    };
});

sessionStore and notifications above stand for application services, not Paperbark APIs. The handler may use DI via services.AddOptions<PaperbarkOptions>() .Configure<YourService>((options, service) => ...). Await all required durable work before returning. A veto, exception, or cancellation does not undo work already performed; an ended session cannot be resurrected by keeping its window open. Use application UI to report partial outcomes.

The immutable PaperbarkCloseRequest contains Id, WindowId, Origin (TitleBar, ApplicationQuit, Managed, Browser) and Scope (Window, Application). With policy enabled, primary close is application quit, not an independent primary-window lifetime. Secondary close is window-scoped and uses the handler without the application Quit prompt. Modal dialogs retain their existing result/dismissal behavior and do not invoke application close policy; a handler can open and dismiss an actionable dialog without deadlocking itself. Application Quit while a non-dialog close is pending is rejected, not silently upgraded into a different scope.

Observe all origins, including native close-button failures, through the desktop:

var desktop = app.Services.GetRequiredService<IPaperbarkDesktop>();
desktop.CloseStarted += (_, operation) => closeOperations.Track(operation);
desktop.CloseCompleted += (_, result) => notifications.Publish(result);

var operation = await desktop.MainWindow.RequestCloseAsync();
var result = await operation.Completion; // Not the command acknowledgement.

closeOperations and notifications are application-owned state/UI services. Subscribe before running the application. Event subscribers run serially off the control reader and must return promptly; throwing subscribers are logged without suppressing other subscribers or operation completion. Async-void subscribers are not awaited. Never await an operation's Completion from its own handler: native closure follows the handler's returned approval. The decision context deliberately has no completion task.

Ordering is: CloseStarted is queued, native confirmation, handler (if enabled), decision, actual native window closure, Completion/CloseCompleted. Cancellation instead completes Cancelled before handler delivery. CloseStarted observers are notifications, not a blocking policy phase. IPaperbarkWindow.Closed separately observes actual window closure; veto/cancel does not finish it. Final native frames and synchronous observer invocations are drained before process-exit-triggered managed shutdown (two-second bounds). A slow subscriber cannot hold shutdown indefinitely; use Completion for the authoritative result and do durable work in the decision handler, not observers.

Outcome Meaning
Closed Native window(s) actually closed; never inferred from approval or exit code.
Cancelled User cancelled native confirmation; application handler was not called.
Vetoed Handler declined; window remains open. Effects are not rolled back.
Failed Handler exception/cancellation/deadline or native decision deadline; window stays open and close is quarantined.
Busy Another non-dialog operation or unreconciled failure blocks this new intent; no policy effects for it.
Bypassed Managed/system teardown bypassed policy; not deliberate consent or proof of rollback.
Unavailable Stale window, transport loss, native crash, or missing definitive result; closure is unknown.

There is one active non-dialog transaction. Concurrent/reentrant requests are rejected as Busy, and repeated native gestures cannot stack prompts or execute policy twice. Cancelling RequestCloseAsync's token after delivery only abandons that caller's wait: it does not withdraw native intent. Subscribe to CloseStarted to retain the operation independently. A fresh intent after Cancel/veto gets a fresh ID. Late or mismatched decisions cannot approve a different request.

Handler deadlines are 100 milliseconds to five minutes (default 30 seconds). After failure, the operation retains its ID and final Completion. HandlerCompletion independently reports when application code really finishes; it remains pending if code ignores cancellation. Late approval/veto is recorded there but never closes the window. After reconciling partial durable effects, the application can explicitly release quarantine:

var failure = await operation.Completion;
if (failure.Outcome == PaperbarkCloseOutcome.Failed)
{
    var eventualHandlerResult = await operation.HandlerCompletion;
    await sessionStore.ReconcileAsync(operation.Request.Id, eventualHandlerResult);
    await operation.ReconcileFailureAsync();
    // Still open. Only a new explicit user intent can start another close.
}

Recovery rejects while the handler is pending and requires the exact quarantined operation. It acknowledges the application's reconciliation; Paperbark cannot verify external durable state. It does not retry effects, reopen a session, or request a close. If the handler never returns, keep the actionable UI available; explicit managed shutdown remains an independent bypass, not successful recovery.

No-handler consumers keep native-only policy and existing confirmation defaults. The legacy macOS unconfirmed title-bar primary close retains its window-only behavior while secondary windows exist; programmatic primary close and explicit Quit remain application-scoped. Opting into CloseHandler makes primary close application-scoped consistently on both platforms. Existing CloseAsync and browser close() remain acknowledgement APIs but enter the same transaction. Custom implementations of the public interfaces must implement the additive members. A matching native build is required: initialization rejects an opt-in handler if the host does not advertise close protocol version 1.

Managed shutdown, system termination, parent loss, and bridge/control loss do not invoke user policy. Pending operations resolve as bypassed/unavailable rather than fabricating consent from a PID, process exit, socket count, DOM unload, or log. No framework can promise delivery after abrupt termination of the managed process.

Managed services can control native windows through DI:

app.MapPost(
    "/window",
    async (IPaperbarkDesktop desktop, CancellationToken cancellationToken) =>
    {
        await desktop.OpenWindowAsync(
            new PaperbarkWindowOptions
            {
                Route = "/dialog",
                Title = "Application dialog",
                Width = 680,
                Height = 440
            },
            cancellationToken);

        return Results.NoContent();
    });

Client-side desktop services

Paperbark carries a small JavaScript library as an embedded resource. The managed process transfers it to the authenticated native host over the control channel. The host injects it into the main frame at document start with WKUserScript; application files do not need to reference a script URL.

const windowHandle = await window.paperbark.openWindow({
  route: "/dialog",
  title: "Opened from the SPA",
  width: 680,
  height: 440
});

await windowHandle.setTitle("Updated by the SPA");

Modal dialogs are async AppKit sheets attached to the primary window:

const result = await window.paperbark.showDialog({
  route: "/confirm",
  title: "Confirm operation",
  width: 640,
  height: 420
});

console.log(result);

The backend-served dialog content completes that promise through its own injected window API:

await window.paperbark.window.close("accepted");

The additive browser requestClose(value) uses the same native policy and returns { id, waitForCompletion(), waitForHandler(), reconcileFailure() }. The wait methods return structured objects with string enum names matching the managed API. Completed operation lookup retains the latest 128 ordinary operations; active/failed operations remain available until recovery or host teardown. Browser calls cannot promise to resolve after their own WebView is destroyed: use managed CloseCompleted/Completion for application-quit observation. A surviving page may observe another window's close; never rely on beforeunload for durable work.

The injected facade sends a private WebView postMessage envelope to the native shell. The shell supplies the trusted source window ID and forwards a framed bridge-request to ASP.NET, where a fixed DI-backed dispatcher validates and handles the method. A handler can issue another native command over the same connection before returning a bridge-response; both read loops remain re-entrant so modal operations do not deadlock.

The public API is deliberately constrained to paperbark.window, paperbark.openWindow, and paperbark.showDialog. There is no public generic invoke, custom handler registration, or shell extension surface. Ordinary windows are independent NSWindow instances. Modal dialogs use an AppKit sheet: the primary window is blocked and the JavaScript promise remains pending until dialog content closes the sheet. All WebViews share the same nonpersistent HTTP session, injected facade, and restricted navigation origin.

Samples

Four samples exercise different web application styles:

Sample UI stack Desktop bridge
samples/minimal Minimal ASP.NET Core HTML Calls the injected API directly
samples/asteroids High-DPI Canvas game Native-window game loop and keyboard input
samples/react React 19 + TypeScript + Vite Calls window.paperbark directly
samples/blazor Interactive Server Blazor Calls window.paperbark through IJSRuntime

The Blazor modal route is itself an interactive server component. Its button event runs on the Blazor circuit and calls the injected paperbark.window.close(value) API through IJSRuntime, returning the value to the component that opened the native sheet.

Run them with:

dotnet run --project samples/minimal
dotnet run --project samples/asteroids
dotnet run --project samples/react
dotnet run --project samples/blazor

The React project runs npm ci when its lockfile changes and produces its Vite bundle during dotnet build. Node.js and npm are therefore additional prerequisites for building that sample.

Documentation site

The GitHub Pages site under site/ contains the three-step quickstart and copyable recipes for window management, dialogs, Blazor, publishing, and app bundle metadata. Run it locally with:

cd site
npm ci
npm run dev

npm run build produces a repository-name-independent static site under site/dist. The Pages workflow builds pull requests and deploys main through GitHub's Pages artifact and deployment actions.

Reusable brand assets live under site/public/brand: the original concept artwork, horizontal lockup, square application mark, and favicon.

Architecture

+---------------- managed ASP.NET Core process ------------------+
| Kestrel on 127.0.0.1:ephemeral                                |
| capability-cookie middleware                                  |
| routes + SPA/Blazor + application DI                           |
| fixed DI bridge dispatcher + IPaperbarkDesktop                  |
| re-entrant framed JSON control server                          |
+-----------------------------+----------------------------------+
                              | private UDS or current-user named pipe
                              | 4-byte big-endian length + JSON
+-----------------------------v----------------------------------+
| native paperbark-host process                                   |
| authenticated bootstrap + WebView message bridge                |
| nonpersistent cookie store + injected window.paperbark          |
| AppKit/WKWebView or WinForms/WebView2 command dispatcher        |
+----------------------------------------------------------------+

The control protocol uses UTF-8 JSON framed by a four-byte network-order payload length with a 1 MiB limit. The host authenticates its first hello frame with a random per-launch 256-bit capability. Events currently include ready, navigated, bridge-tested, host-closing, close-started, close-requested, close-completed, and window-closed; request/response commands include initialize, set-title, open-window, show-dialog, close-window, request-close, recover-close, and shutdown. close-decision replies carry the exact close ID, target, and scope. Close IDs are distinct from command acknowledgement IDs. The shell wraps browser calls as bridge-request messages and ASP.NET returns bridge-response messages with a result or structured error. Concurrent requests are correlated by a random request ID. A modal show-dialog request deliberately remains outstanding until its window is closed with a result.

Initial URL, HTTP capability, cookie name, JavaScript source, and title are sent only after control-channel authentication. They are not passed on the host command line.

The managed process owns the native child. Managed shutdown requests native shutdown and forcibly terminates an unresponsive child after a timeout. Closing the last native window exits the host and stops ASP.NET Core; with confirmation enabled, confirmed primary close exits the entire application, including secondary windows. The native host also monitors the managed parent PID.

Loopback HTTP security

Loopback is treated as transport, not authentication:

  • Kestrel binds only to 127.0.0.1 on an ephemeral port.
  • Every run creates a separate random 256-bit HTTP capability.
  • The host installs it as a host-only, HttpOnly, SameSite=Strict, nonpersistent cookie before first navigation.
  • An early startup-filter middleware rejects every request without an exact host and constant-time capability match.
  • Cross-site Origin and Fetch Metadata requests are rejected.
  • Every WebView uses a nonpersistent website data store.
  • WKWebView navigation is restricted to the assigned loopback origin.
  • Browser-to-desktop calls use WebView messaging rather than HTTP endpoints; the shell adds the trusted source window identity before forwarding them.

Plain HTTP cannot use a Secure cookie or the __Host- prefix. Another process running as the same user can also inspect process state or interfere with local resources. Production hardening should move the remaining control bootstrap capability away from process arguments, evaluate loopback HTTPS or IPC-backed HTTP, define CSP and download/external-link policies, and threat-model same-user compromise explicitly.

Publishing a macOS application

Publishing for a macOS runtime identifier creates a conventional .app bundle in addition to the loose .NET publish output:

dotnet publish samples/react \
  -c Release \
  -r osx-arm64 \
  -p:PublishAot=true
publish/Paperbark.React.app/
  Contents/
    Info.plist
    MacOS/
      Paperbark.React
      Paperbark.React.bin
      Paperbark.React.staticwebassets.endpoints.json
    Resources/app/
      appsettings.json
      wwwroot/

Paperbark.React is a minimal RID-specific launcher supplied by the NuGet package. It changes the working directory to Contents/Resources/app and uses exec to replace itself with Paperbark.React.bin, so it doesn't introduce a third long-lived process. ASP.NET consequently gets a deterministic content root even when Finder or Launch Services starts the application. Paperbark also points the web-root file provider at the bundled wwwroot.

The bundle consumes the final ASP.NET publish output rather than source directories. React/Vite output, Blazor framework files, scoped CSS, transitive static web assets, fingerprints, and precompressed files are therefore placed under the read-only resource root. The endpoint manifest remains beside the managed executable, where MapStaticAssets expects it.

The buildTransitive/Paperbark.targets file makes this part of an ordinary dotnet publish; no macOS workload or MAUI dependency is required. Available MSBuild properties include:

Property Default
PaperbarkCreateAppBundle true for osx-* publishes
PaperbarkAppBundleName $(AssemblyName)
PaperbarkBundleDisplayName app bundle name
PaperbarkBundleIdentifier dev.paperbark.$(AssemblyName)
PaperbarkBundleVersion $(VersionPrefix), then 1.0.0
PaperbarkMinimumMacOSVersion 13.0

Set PaperbarkCreateAppBundle=false to retain only the ordinary loose publish layout. The spike generates a valid but unsigned bundle. Production release work still needs icons, entitlements, hardened-runtime signing, notarization, and a DMG or ZIP distribution step.

Embedded native host and Native AOT

The macOS build compiles the transparent Swift source in native/macos/Paperbark.Host/main.swift and embeds the resulting RID-specific payload into the Paperbark assembly. The same payload is also packed as a normal NuGet runtime asset.

At startup, Paperbark:

  1. Selects the embedded resource for the current RID.
  2. Computes its SHA-256 content hash.
  3. Acquires a per-payload extraction lock.
  4. Reuses a cached executable only when its bytes match.
  5. Otherwise writes and flushes a temporary file, validates it, applies user-only permissions, and atomically moves it into place.

The macOS cache layout is:

~/Library/Caches/Paperbark/hosts/v1/osx-arm64/<sha256>/paperbark-host

Linux uses $XDG_CACHE_HOME/paperbark (or ~/.cache/paperbark) and Windows uses the user's local application data directory. Protocol version, RID, and content hash allow applications using identical Paperbark payloads to share the host without version collisions.

The minimal, React, and Asteroids samples can be published as Native AOT:

dotnet publish samples/minimal \
  -c Release \
  -r osx-arm64 \
  -p:PublishAot=true

The resulting .app contains a Native AOT application executable with the Paperbark host embedded inside it. The host is extracted on first launch and reused thereafter. Native AOT applications still need to follow normal ASP.NET trimming and source-generated serialization constraints. Interactive Server Blazor currently does not support trimming or Native AOT, so publish the Blazor sample framework-dependent or self-contained without PublishAot.

Build, test, and package

Prerequisites:

  • .NET SDK 10.0
  • macOS 13 or later and Xcode Command Line Tools with swiftc, or Windows with the Evergreen WebView2 Runtime
  • Node.js and npm for the React sample
dotnet build Paperbark.slnx
dotnet test tests/Paperbark.Tests/Paperbark.Tests.csproj --no-build
dotnet pack src/Paperbark/Paperbark.csproj -c Release

Packing requires both macOS architectures so an incomplete package cannot be published. On macOS they are built automatically. CI builds them once on a macOS runner, then supplies the complete tree to the cross-platform pack job:

scripts/build-macos-host.sh artifacts/native
pwsh scripts/build-windows-host.ps1 artifacts/native
dotnet pack src/Paperbark/Paperbark.csproj -c Release \
  -p:PaperbarkNativeAssetsRoot="$PWD/artifacts/native"

An automated native smoke mode verifies the full protected path:

PAPERBARK_SMOKE_TEST=1 \
  dotnet run --project samples/minimal

Close-confirmation options, generated protocol interoperability, and Windows policy lifecycle regressions run in dotnet test tests/Paperbark.Tests. On macOS, run bash scripts/test-macos-close-confirmation.sh for native policy and configuration parsing tests. With a macOS GUI session available, run bash scripts/test-macos-close-lifecycle.sh to exercise actual AppKit menu, primary close (including the bridge command), repeated requests, safe Escape/Enter, nested modal cancellation/completion, confirmed quit, managed shutdown, disconnect, and parent-loss paths. It creates isolated test windows and does not send system keyboard input to other apps. Interactive acceptance still requires checking Cmd+Q/Alt+F4 with WebView focus, primary close, cancel/Escape/Enter, repeated requests, active modal/secondary windows, and forced shutdown while confirmation is open on the target OS. Cross-building the Windows host does not verify Windows keyboard routing or native focus behavior.

node --test tests/client/close.test.cjs checks the injected close facade. dotnet run --project tests/Paperbark.CloseSmoke -c Release runs an isolated real native/managed close fixture: responsive control commands during policy, reentrant Busy, veto, exception, quarantine, reconciliation, actual closure, and observer-before-managed-shutdown ordering. It intentionally logs a storage exception without changing any external durable state. Both native CI jobs run this fixture against their built host; it needs a GUI session and, on Windows, WebView2. On Windows, pwsh -File scripts/test-windows-close-lifecycle.ps1 runs the real WinForms/WebView2 confirmation, modal, timeout/recovery, disposal, and transport-loss scenarios. The macOS lifecycle fixture also tests the native decision timeout and late replies. System power-off is simulated with a process-local notification, not by logging out or shutting down the machine. If AppKit termination arrives without an observed system notification, Paperbark conservatively applies user policy (including Dock Quit) instead of silently bypassing it.

It installs the cookie, loads protected content, changes the primary title, executes the injected JavaScript API through WebView postMessage, routes the request through ASP.NET/DI, opens an AppKit modal sheet over IPC, confirms its protected backend navigation, closes it from dialog JavaScript, verifies the returned result, and shuts down both processes.

The Blazor sample's circuit callback can be exercised specifically with:

PAPERBARK_SMOKE_TEST=1 \
PAPERBARK_SMOKE_DIALOG_ROUTE='/dialog?smoke=true' \
PAPERBARK_SMOKE_DIALOG_RESULT='blazor-circuit-smoke' \
  dotnet run --project samples/blazor

Versioning and releases

.github/workflows/build-release.yml computes one version and uses it for every platform build and the final package:

  • Pull requests targeting main or release/X.Y produce unique -pr.* packages and publish same-repository previews to GitHub Packages.
  • Pushes to main publish -alpha.* packages for the next minor line.
  • Pushes to release/X.Y publish -beta.* packages for that line's next patch.
  • A manual workflow dispatch with release enabled on release/X.Y publishes the stable version, tags it as vX.Y.Z, and creates a GitHub Release with the package artifacts.

Local builds use 0.1.0-dev; set PAPERBARK_VERSION to override it. NuGet.org publishing uses trusted publishing through the production GitHub environment. Configure that environment with a NUGET_USERNAME secret and a NuGet.org trusted-publishing policy scoped to this repository, workflow, and environment. GitHub Pages remains independently built and deployed from main by .github/workflows/pages.yml.

NuGet runtime assets

The intended package matrix is:

runtimes/osx-arm64/native/paperbark-host
runtimes/osx-x64/native/paperbark-host
runtimes/linux-arm64/native/paperbark-host
runtimes/linux-x64/native/paperbark-host
runtimes/win-arm64/native/paperbark-host.exe
runtimes/win-x64/native/paperbark-host.exe

The build scripts produce and embed both macOS and Windows architectures. The Windows spike is a framework-dependent, single-file WinForms/WebView2 host and therefore requires the .NET 10 Desktop Runtime and Evergreen WebView2 Runtime. NativeHostLocator selects by operating system and process architecture, tries the embedded content-addressed host first, then standard runtime asset locations. PaperbarkOptions.NativeHostPath and PAPERBARK_NATIVE_HOST_PATH remain available for development overrides. No generated native executable is committed.

Testing direction

The HTTP capability model also supports browser automation without pretending a native WebView is a new Playwright BrowserType. A future Paperbark.Testing fixture can start the real ASP.NET application, create a browser context, and install the same per-launch cookie with BrowserContext.AddCookiesAsync. Chromium, Firefox, and Playwright WebKit can then test application behavior and the server-side desktop bridge contract. A smaller native smoke suite should continue to cover AppKit/WKWebView creation, script injection, IPC, and process lifetime.

Platform follow-up

macOS: sign the generated app and its executables, package the host as a signed nested helper app rather than extracting a raw Mach-O, and add icons, entitlements, hardened runtime, notarization, and DMG/ZIP generation.

Windows: implement a Win32/WebView2 host, use an ACL-restricted named pipe, install the capability through CoreWebView2CookieManager, inject the client at document creation, and produce signed arm64/x64 runtime assets.

Linux: implement GTK/WebKitGTK, retain a permission-restricted Unix socket, install the capability through WebKitCookieManager, inject the client with a user content manager, and test Wayland/X11 lifecycle differences.

Cross-platform work also needs cache cleanup policy, crash recovery, protocol negotiation, richer multi-window identities, menus and dialogs, accessibility, native release builds for every future RID, and broader security hardening.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0-alpha.16.1.c9da1a1 82 9/15/2026
0.2.0-alpha.12.1.d460a66 305 9/12/2026
0.2.0-alpha.10.1.115f194 74 9/12/2026
0.1.0-beta.8.1.35ae3c3 202 8/12/2026