Morphir.Mep
0.5.0-alpha.3
Prefix Reserved
dotnet add package Morphir.Mep --version 0.5.0-alpha.3
NuGet\Install-Package Morphir.Mep -Version 0.5.0-alpha.3
<PackageReference Include="Morphir.Mep" Version="0.5.0-alpha.3" />
<PackageVersion Include="Morphir.Mep" Version="0.5.0-alpha.3" />
<PackageReference Include="Morphir.Mep" />
paket add Morphir.Mep --version 0.5.0-alpha.3
#r "nuget: Morphir.Mep, 0.5.0-alpha.3"
#:package Morphir.Mep@0.5.0-alpha.3
#addin nuget:?package=Morphir.Mep&version=0.5.0-alpha.3&prerelease
#tool nuget:?package=Morphir.Mep&version=0.5.0-alpha.3&prerelease
Morphir.Mep
One implementation of the Morphir Extension Protocol (MEP) 0.2.0-draft.1 for
morphir-dotnet hosts and guests: JSON-RPC 2.0 messages, Content-Length framing,
the session lifecycle and the model transport. Work is tracked by
#512 and
add-mep-core. The roadmap is the
Morphir extension model PRD, step 1.
Layers
| Layer | File | What it does |
|---|---|---|
| Wire types | Wire.fs |
Records and unions of the pinned contract, with parsed values (ProtocolVersion, SemanticVersion, Sha256Digest, RequestId). MepJson is generated from this file at build time by Morphir.Myriad.SystemTextJson. There is no reflection serializer. |
| Model transport | ModelTransport.fs |
Tuple selection, the host check of a selection, envelope creation and admission. Pure functions. |
| Message codec | Rpc.fs |
Classifies a JSON-RPC message by its members (JsonRpcMessage), keeps params raw, and writes messages. This is the only hand-written JSON. |
| Dispatcher | Dispatcher.fs |
DispatchAsync(bytes, ct) returns the reply bytes, or none for a notification. A method table maps method names to typed handlers; a gate refuses a method before its handler runs. |
| Framing | Framing.fs |
Content-Length frames on a Stream: header blocks up to 8192 bytes, bodies up to 64 MiB, checked before the body is read. A refused frame ends the connection. |
| Connection | Connection.fs |
Reads frames, dispatches inbound requests concurrently, matches inbound responses to the pending-request table, and sends and receives $/cancelRequest. |
| Session | Session.fs |
The pure lifecycle rules (Lifecycle.admit) and version negotiation, shared by hosts and guests. |
| Channel | Channel.fs |
How a host session reaches its guest: a framed stream pair, or ISingleMessageChannel for a guest that takes one message per call. SessionFailure says why a single-message session ended. |
| Peers | Peers.fs |
Guest (lifecycle methods, gate, extension methods) and HostSession (refuses to send what the guest must refuse). |
On the guest side, the stream path (Connection) and the single-message path
(Dispatcher.DispatchAsync) use the same dispatcher, so they give the same bytes. On the
host side, HostSession holds a channel and applies the same lifecycle, version and
model-transport rules on either. Starting a guest process is not in this project: a process
host connects a HostSession to the child's streams.
Channels
A host session sends a request and waits for its reply, and sends notifications. It does
both through a channel (CHAN-1).
HostSession checks each request against the lifecycle state before it uses the channel, so
a request refused on one channel is refused on the other with the same error, and the guest
never receives it.
| Channel | Constructor | What the guest is |
|---|---|---|
| Framed stream pair | HostSession(input, output, host, offer) |
A process with Content-Length frames on its standard streams. Requests run concurrently and are matched by id; a cancelled request sends $/cancelRequest; the guest can send morphir.progress. |
| Single message | HostSession(channel, host, offer) |
A guest that takes one whole message per call, such as the handle export of a WASM guest. The runtime implements ISingleMessageChannel; tests/Morphir.Mep.AotProbe has a C# implementation. |
ISingleMessageChannel has two members. Both take the method name, the bytes of one
complete UTF-8 JSON-RPC message with no Content-Length header, and a cancellation token.
Both return the bytes of the message the guest returned, or null when it returned none.
RequestAsynccalls the guest with a request.NotifyAsynccalls the guest with a notification, or withholds it when the guest's runtime does not deliver notifications to that guest. A withheld notification does not call the guest.HostSessionalways sendsmorphir.initializedandmorphir.exit; the runtime decides whether the guest receives them.
The implementation passes bytes and does not check them. HostSession applies these rules
to a single-message guest:
- One call at a time. A request waits until no other call is inside the guest.
- A request takes exactly one reply: the response or error response with the request's id.
No bytes, a response for another id, a request or notification (such as
morphir.progress), bytes that are not a JSON-RPC message, or a reply overContentLengthFraming.MaxBodyBytesfail the session. - A notification takes no reply. Bytes returned for one fail the session.
- The host never sends
$/cancelRequest. When a request's token is cancelled, the channel stops the call and ends it withOperationCanceledException. The request ends with-32800and the session ends. - An exception from the channel fails the session.
- After the session fails, no message reaches the guest.
Failure(C#:FailureOrNull()) says why, as aSessionFailure. The request that failed gets-32800when it was cancelled and-32603otherwise. Later requests get-32603without a call. A failure duringInitializeAsyncgivesInitializeFailure.Refusedwith that error.RunAsyncends withStoppedat exit or failure.
Which notifications a WASM guest accepts is read from its morphir.initialize result by
the runtime, not by HostSession. The member that advertises it is not fixed in the
contract yet (NTFY-1).
A guest and a host in memory
This example is compiled by tests/Morphir.Mep.Tests/ReadmeTests.fs. Keep the two equal.
let runSession (claims: CapabilityClaimSet) (capabilities: ExtensionCapabilities) =
task {
let guest =
Guest(
{ Claims = claims
Capabilities = capabilities
Methods = readOnlyDict [] }
)
let toGuest = Pipe()
let toHost = Pipe()
let guestRun = guest.RunAsync(toGuest.Reader.AsStream(), toHost.Writer.AsStream(), CancellationToken.None)
let host =
HostSession(
toHost.Reader.AsStream(),
toGuest.Writer.AsStream(),
{ Kind = PeerKind.Cli; Name = "morphir"; Version = "0.4.0" },
null
)
let hostRun = host.RunAsync CancellationToken.None
let! session = host.InitializeAsync CancellationToken.None
let! ping = host.PingAsync CancellationToken.None
let! _ = host.ShutdownAsync CancellationToken.None
do! host.ExitAsync()
let! guestEnd = guestRun
toHost.Writer.Complete()
let! _ = hostRun
return session, ping, guestEnd
}
Add a method
- Check that the pinned contract has the method and its params and result types. Do not add a method, member or error code that the pin does not have; raise an upstream issue.
- Write a failing wire test, then add the types to
Wire.fswith the attributes that the contract's wire rules need. Add the type to the contract test (ContractTests.fs): a complete sample, and the members that the contract text lets a reader default. - Add the method to
Methodif it is new, and its capability toLifecycle.capabilityif the contract makes it depend on one (the guest replies-32013without it). - Register a handler in the guest's
MethodswithHandlers.request(params bind as one object through their codec; absent or mismatched params give-32602), and add a typed call toHostSessionif hosts send it. - Add the method to the AOT probe (
tests/Morphir.Mep.AotProbe) when it changes what C# calls.
The pin
tests/Fixtures/Mep pins the upstream
contract model (mep.ir.json), the transcripts and the model-transport corpus by commit
and digest. Do not edit the fixtures. To update the pin:
- Copy the new files from
finos/morphirat one commit, and record the commit and the SHA-256 digests inPROVENANCE.md. - Run the tests.
ContractTestsfails for each renamed or added member, and the transcript and corpus tests fail for each changed behavior. - Change the types and rules in the same pull request. A new MEP version moves every MEP part in morphir-dotnet together; drafts match only exactly.
Where a transcript and the contract text disagree, the tests follow the contract text.
The legacy transcripts initialize with 0.1; the replays use 0.2.0-draft.1 instead.
C# callers
The project is trimmable and AOT-compatible, with F# nullness metadata. Parsed values have
TryParse and Create; FrameRead and SessionFailure have Match.
tests/Morphir.Mep.AotProbe is a C# consumer that runs the full session on streams and on a
C# ISingleMessageChannel; InteropTests and ChannelTests compile C# examples and check
the nullable-flow diagnostics.
Tests
dotnet run --project tests/Morphir.Mep.Tests -- --summary
./build.sh --target TestAot --configuration Release
The first command runs the wire, contract, JSON-RPC, connection, session, channel and
model-transport tests in memory. The channel tests use a fake single-message guest built on
Dispatcher.DispatchAsync. The second is the Native AOT lane of the black-box tests: it publishes
the probe (and the other probes and library hosts) with Native AOT, refuses IL diagnostics other
than the FSharp.Core summary baseline, and compares the native output with the managed output.
Both run in PR CI.
| Product | Versions 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. net11.0 is compatible. |
-
net10.0
- FSharp.Core (>= 10.1.403)
- Morphir.Myriad.SystemTextJson.Annotations (>= 0.5.0-alpha.3)
-
net11.0
- FSharp.Core (>= 10.1.403)
- Morphir.Myriad.SystemTextJson.Annotations (>= 0.5.0-alpha.3)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Morphir.Mep:
| Package | Downloads |
|---|---|
|
Morphir.Extensions
Morphir extension registry domain: parsed extension identities, capability claims and the extension catalog |
|
|
Morphir.Compilation.Mep
The MEP 0.2.0-draft.1 host side of guest processes: the guest process and session, and the frontend adapter, an IFrontend over a guest that serves morphir.frontend.compileModel. Trim-safe and AOT-safe. |
|
|
Morphir.Extensions.Wasm
The WASM extension runtime of morphir-dotnet: Wasmtime with the embedded Extism kernel module. |
|
|
Morphir.Extensions.Installation
Local extension package installation, inventory, selection and activation for Morphir tooling. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.5.0-alpha.3 | 57 | 11/14/2026 |
| 0.5.0-alpha.2 | 53 | 11/13/2026 |
| 0.5.0-alpha.1 | 55 | 11/13/2026 |
| 0.4.0-alpha.5 | 62 | 11/13/2026 |
| 0.4.0-alpha.4 | 65 | 11/10/2026 |
| 0.4.0-alpha.3 | 71 | 11/9/2026 |
### Added
- `morphir extension install nuget:<package-id>[@<version>] --home <abs-home> --no-probe` installs an extension from a NuGet feed: nuget.org, a private V3 feed such as Azure Artifacts, or a folder feed. It reads the sources, package source mapping, credentials (`packageSourceCredentials` and credential provider plugins) and signature policy of `nuget.config`, as `dotnet tool install` does, through the Microsoft NuGet client packages. `--source` names one source (a configured name, a V3 `index.json` URL or a folder), `--configfile` names the `nuget.config`, and `--interactive` lets a credential provider prompt; the three options apply only to a `nuget:` source. No version selects the highest stable listed version. The main package carries `morphir/release.json`; a process artifact is the subtree under its `root` of the main package (one package) or of a package whose id extends the main id (one package per platform, such as the RID packages of a self-contained .NET tool), pinned by its tree digest. A framework-dependent .NET tool is refused. Signatures are checked as the dotnet CLI checks them on each operating system. The command writes only to the Home: no NuGet HTTP cache, global packages folder or NuGet temporary folder. New codes: `extension.nuget_not_found`, `extension.nuget_unreachable`, `extension.nuget_unauthorized`, `extension.nuget_signature_invalid`. See [Publish an extension as a NuGet package](docs/content/docs/guides/publish-extension-nuget.md).
- `morphir extension install` installs WASM extensions from every provider: a local declaration, a GitHub release asset, or the entry `morphir/<filename>` of a NuGet main package. The module is checked against its descriptor `sha256` (`extension.digest_mismatch`) and stored in the single-file store; the lock records `runtime` `wasm` and the claims stay unchecked. The guest does not run during installation. `morphir compile` and `morphir generate` with `--extension-id` run an installed WASM extension through the host's WASM runtime, with the loader that configured modules use: they rehash the stored module against the lock first, and check the session against the installed claims. A host without a WASM runtime fails with `extension.runtime_unavailable`.
- On Linux and macOS, NuGet signature checks use the code signing and timestamping trust lists of the .NET SDK, which `Morphir.Extensions.Installation` carries, as the dotnet CLI does. Without them, NuGet on Linux has no trusted root, and a `require` policy refused every nuget.org package.
- `NuGetPackageProvider` in `Morphir.Extensions.Installation`, and `ExtensionSourceRequest.WithNuGetOptions`. `ExtensionSources.ProviderFor(request, http, home)` takes the Home for a NuGet source. The package ships `buildTransitive/Morphir.Extensions.Installation.props`, which sets the `NuGet.UseSystemTextJsonDeserialization` feature switch in each application that references it.
- `morphir extension install github:<owner>/<repo>[@<version-or-tag>] --home <abs-home> --no-probe` installs an extension package from a GitHub release ([#493](https://github.com/finos/morphir-dotnet/issues/493)). The release carries the extensions-family `release.json` as an asset; the command picks the artifact for this platform, streams the asset into a private stage and checks its SHA-256 against the descriptor before the installer reads it. `@<version>` selects the tag `v<version>`, else `<version>`; no `@` selects the latest release. It uses the anonymous GitHub API, or `GITHUB_TOKEN` or `GH_TOKEN` when set, and reports a rate limit as `extension.github_rate_limited`. `--local-developer` does not apply to a named release; `--no-probe` is still required. The installed extension then runs with `--extension-id`, with no network.
- Source providers in `Morphir.Extensions.Installation`: `IExtensionSourceProvider` (`Scheme`, `ResolveAsync`, `FetchAsync`), with `GitHubReleaseProvider` and `LocalRepositoryProvider`. A provider only resolves and fetches; validation, the package tree, and the catalog, lock and journal writes stay in one installer. A new source (a plain HTTPS URL, OCI, a registry index) is one more provider.
### Changed
- The extension installer writes and reads the extension family `0.1.0-preview.3` ([finos/morphir#1176](https://github.com/finos/morphir/pull/1176), merged as `65bbbc96`): the package format `nupkg` (a NuGet package, pinned by its tree digest only), the provenance `nuget-package` and the source `nuget-package-entry`. Catalogs, locks, journals, repositories and release descriptors of `0.1.0-preview.2` are refused and not converted: install again into a new Home, and publish `release.json` with the preview.3 `$schema`. A release descriptor may now offer `process` and `wasm` artifacts; the installer selects the `process` artifact for the platform, else a `wasm` artifact, and the lock records the selection.
- Build: the Native AOT publish of `morphir` fails on any trim or AOT warning from `NuGet.*` or `Newtonsoft.Json`. `morphir` and `dotnet-morphir` set `NuGet.UseSystemTextJsonDeserialization`, with which NuGet.Protocol has no such warning.
- The extension installer writes and reads the extension family `0.1.0-preview.2` ([finos/morphir#1173](https://github.com/finos/morphir/pull/1173)), where an artifact `source` and an `index` provenance are provider-tagged unions (`local-file` or `github-release-asset`, `local-directory` or `github-release`). Catalogs, locks, journals, repositories and release descriptors of `0.1.0-preview.1` are refused and not converted: install again into a new Home, and publish `release.json` with the preview.2 `$schema`. The package tree manifest format is unchanged.
- The install scripts `scripts/install-morphirup.sh` and `scripts/install-morphirup.ps1` install `morphirup` from the [finos/morphir releases](https://github.com/finos/morphir/releases): they download `morphirup` and `morphirup-v<version>.sha256` from `https://github.com/finos/morphir/releases/download/v<version>/`; the scripts stay hosted here. `morphirup` is on finos/morphir from `0.5.0-alpha.2` on; earlier `morphirup` releases stay on the [finos/morphir-dotnet releases](https://github.com/finos/morphir-dotnet/releases). With no version, the scripts take the newest finos/morphir release that has `morphirup` for the machine and skip the others, such as `v0.5.0-alpha.1` (the CLI only) and the releases of the earlier Rust CLI (`v0.4.0-beta.*` and older), in the release list (up to 100 releases from the GitHub API) and in the release feed `https://github.com/finos/morphir/releases.atom`. A given version whose release has no `morphirup` for the machine is refused with "finos/morphir release v<version> has no morphirup-<rid>; morphirup releases before 0.5.0-alpha.2 are on finos/morphir-dotnet": the `sh` script reads the HTTP status of the download instead of letting `curl -f` report a 404 as a network error, and the PowerShell script reads the status of the failed request. The line `downloading <file> <version> from <url>` names the download, and `MORPHIRUP_DOWNLOAD_URL` mirrors `https://github.com/finos/morphir/releases/download`. Two testing hooks, `MORPHIRUP_RELEASES_FILE` and `MORPHIRUP_FEED_FILE`, read the release list and the release feed from local files; the end-to-end scenarios of `InstallScripts.feature` use them with a local release mirror to check, with no network, that the script of the machine skips the releases without `morphirup` in both and refuses a Rust release given by version.
- mise installs `morphirup` with `mise use -g 'github:finos/morphir[matching=morphirup,version_prefix=v,prerelease=true]'`. mise also lists the releases without `morphirup`; with one of those it stops with "No matching asset found" and installs nothing, so name a release that has `morphirup`, such as `@0.5.0-alpha.2`.
- The proto plugin downloads the `morphir` CLI from `https://github.com/finos/morphir/releases/download/v<version>/`. The plugin itself is still released here.
- The manual workflows `install-scripts.yml` and `morphirup-release-journey.yml` use the finos/morphir releases. The pinned version of `install-scripts.yml` is `0.5.0-alpha.2`, each of its runs checks that the download comes from finos/morphir, and a run of the Rust release `0.4.0-beta.12` must be refused by name.