Spec: Capability statements across the extension lifecycle #921
Replies: 11 comments
Update, 2026-09-22: SemVer versions, as the defaultDecision 12 is added and the body is updated to match. Every versioned contract Morphir defines uses SemVer 2.0 version strings by default.
In this spec that gives: Recorded exceptions: MEP keeps The default is in |
Correction: new contracts start at
|
Implementation note: use the
|
Implementation note: TypeScript uses
|
Refinements from implementing
|
|
Status: slice 6 (bootstrap host release) is done.
The legacy compile envelope cannot be retired yet. The CLI sends process and WASM frontends the legacy request (top-level
|
|
Status: #921 is delivered, and the three transitional mechanisms from #915 are retired.
|
|
Status: capability statements are now capability claim sets, made of claims. The reasons are in kb
Next, now that the old names cannot reach a bundle: the Elm extension publishes a real version-2 process bundle, and first-party bundles publish version-2 descriptors that carry claim sets, so The title of this discussion keeps the old name, for link stability. |
|
Status: v0.4.0-beta.7 is released. It is the first CLI that reads capability claim sets (draft.2), so the version-2 bundles work with a released CLI.
Remaining under this thread: first-party morphir-rust bundles (and |
Status: first-party bundles carry capability claims (CLI v0.4.0-beta.8)The follow-on work after the rename to capability claim sets is done. Every first-party extension now publishes a version-2 release descriptor. Each artifact in it carries the claim set ( Releases
Verification
Update, 2026-09-25: morphir-scala v0.5.0-M10
Open
|
|
Closing: this spec is delivered.
The items still open are tracked as release gates for Morphir CLI
Each outcome is recorded as a Decision Record in the |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Problem
An extension's capabilities are stated in three places, and two of them must agree:
morphir.initialize;.github/extensions.tomlflags in morphir-rust,extension.jsonin morphir-elm;The host compares (1) with the installed record built from (2) at every session, and refuses the session when the capability kinds differ ("capability kinds changed"). So every new capability means editing the descriptor, the writer in each language, and the host parser. Every parser uses
deny_unknown_fields, so a released CLI rejects the new key until a new CLI ships. That is whyTRANSITIONAL_FIELDSexists in.mise/tasks/test/cli-release.Two more facts found while working on step 5:
morphir extension repository publishaccepts only a single-artifact WASM bundle ("local repository publication currently requires a WASM bundle"). It also rejects any file in the bundle directory other thanrelease.json, the artifact and its checksum..release.jsonwith anartifacts[]list. That shape is not the bundle format, and process bundles are refused anyway. No tool turns an Elm release into an installed record. The only installed Elm records today come from hand-written index lines in tests.The existing MEP draft already says "Receivers must ignore unknown object fields" for protocol messages. The distribution formats do the opposite.
Decisions
repository publishaccepts process bundles as well as WASM bundles.morphir.extension.describe, returns the capability statement. It is allowed beforemorphir.initializeand has no side effects.describeand refuses on a mismatch.extension install --no-probeskips the probe.extension infoshow it.describeis optional for guests. A host falls back toinitialize,capabilities,shutdown.declaredrather thanprobed.0.y.zthe minor acts as the major. A new contract starts at0.1.0-draft.1and moves to the1.0.0-draftline once settled; a revision of a shipped format starts at its next major's first draft. MEP keeps0.1until its next protocol change; Morphir IRformatVersionis not affected. Recorded inAGENTS.mdand morphir-cli decision 0003.The capability statement
A statement is what the guest says about itself. It has the same content in every phase:
{ "statementVersion": "0.1.0-draft.1", "protocolVersions": ["0.1"], "extension": { "id": "morphir-elm", "name": "Morphir Elm frontend", "version": "0.3.0", "types": ["frontend", "workspace"] }, "capabilities": { "frontend": { "languages": [{ "id": "elm", "fileExtensions": [".elm"] }], "irVersions": ["3"], "compile": true, "incremental": false, "multiDocument": false }, "workspace": { "protocolVersions": ["0.1.0-draft.1"], "discover": true } }, "requires": { "host": [">=0.4.0-alpha.7"] }, "critical": ["requires.host"] }typesare the capability kinds. Readers check them strictly: a kind a reader does not know is an error.capabilitiesmembers are carried unchanged by every reader. A reader uses the members it knows and ignores the rest.criticallists member paths that change meaning. A reader that does not understand a listed path refuses and names it.requires.hostis a list of single SemVer comparators over the host version, such as[">=0.4.0-alpha.7", "<0.5.0"], all of which must hold. Single comparators parse the same way in the Rustsemvercrate and in@std/semver. A host outside the range refuses with a message that names the range.morphir.extension.describemorphir.initialize, and in any later state before shutdown{ "protocolVersions": ["0.1"] }, the versions the caller understandsAfter
describethe caller may sendmorphir.exitwithout a session. A guest that does not implementdescribeanswers-32601, or refuses the request because it was sent beforeinitialize. In both cases the host falls back to a session that follows the lifecycle:initialize, theinitializednotification,morphir.extension.capabilities,shutdown,exit. A session reports less than a statement, so the statement rebuilt from it lists only the negotiated protocol and has norequiresorcritical.When a session agrees with a statement
An initialization result is not a statement: it carries one negotiated
protocolVersionand the capabilities of that session. The host does not test the two for equality. The result agrees with the statement when the extension'sid,nameandversionare equal, the negotiatedprotocolVersionis one of the statement'sprotocolVersions, every capability kind the session reports is among the statement'stypes, and every member it reports has the statement's value. A session may offer less than its statement; it may never offer more, or something different. Publication and installation compare two statements, which must be equal.Lifecycle phases
describerepository publish, where the artifact runs on the publishing hostdescribeextension install, for the selected artifactdescribe(skipped with--no-probe)initialize, whose result must agree with the statementshutdown, thenexitPublish, install, update and uninstall remain host operations. The guest takes part in them only through
describe, so it cannot change anything while being published or installed.What "probe" means per runtime
A probe starts the artifact and sends
describe.--no-probeexists for users who do not accept that.Bundle descriptor, version 2
One descriptor per release, with one entry per artifact:
{ "schemaVersion": "2.0.0-draft.1", "extensionId": "morphir-elm", "shortId": "elm", "version": "0.3.0", "gitCommit": "…40 hex…", "platformDifferences": "none", "artifacts": [ { "platform": "aarch64-apple-darwin", "runtime": "process", "filename": "morphir-elm-extension-0.3.0-aarch64-apple-darwin.tgz", "sha256": "…", "statement": { "…": "as returned by describe on that platform" } } ] }platform.platformDifferencesis"none"or"declared". When the probed statements differ across artifacts and this is"none", the release job fails. An accidental platform bug is caught; a deliberate difference is written down.Flows
Today: WASM extension
sequenceDiagram autonumber actor Author participant Pack as Packager participant GH as GitHub release actor User participant Repo as repository publish participant Inst as extension install participant Host as morphir compile participant Guest Author->>Pack: extensions.toml (capability flags by hand) Pack->>GH: release.json (flags copied) + artifact + checksum Note over Pack,Guest: the guest is never asked User->>Repo: publish --bundle Repo->>Repo: strict parse, WASM only, kinds from keys User->>Inst: install Inst->>Inst: verify digest, strict catalog record User->>Host: compile Host->>Guest: initialize Guest-->>Host: types + capabilities Host->>Host: kinds differ → "capability kinds changed"Today: Elm process extension
sequenceDiagram autonumber actor Author participant Rel as release task participant GH as GitHub release actor User participant Repo as repository publish Author->>Rel: extension.json (capabilities by hand) Rel->>GH: 6 archives + .release.json (artifacts[]) User-xRepo: publish --bundle Note over Repo: other schema, and process bundles are refusedProposed
sequenceDiagram autonumber actor Author participant CI as Release matrix (one job per platform) participant Guest participant Asm as Release assembly participant GH as GitHub release actor User participant Repo as repository publish participant Inst as extension install participant Host as morphir compile Author->>CI: tag CI->>Guest: describe Guest-->>CI: statement for this platform CI->>Asm: artifact + sha256 + statement Asm->>Asm: compare statements across platforms<br/>differ and platformDifferences = none → fail Asm->>GH: descriptor (version 2) + artifacts + checksums User->>Repo: publish --bundle (WASM or process) Repo->>Repo: verify every digest and checksum opt an artifact runs on this host Repo->>Guest: describe Repo->>Repo: equal to its statement, else refuse end Repo-->>User: index record: kinds strict, statements kept User->>Inst: install Inst->>Inst: select artifact for this platform, verify digest Inst->>Guest: describe (unless --no-probe) Inst->>Inst: equal to the record, else refuse Inst-->>User: note when this artifact differs from others User->>Host: compile Host->>Host: registry reads the stored statement Host->>Guest: initialize Host->>Host: agrees with the installed statement Host->>Guest: discover / compileCompatibility rules
describeresult) a reader ignores an optional member it does not understand, and refuses a member listed incriticalthat it does not understand.2.0.0-draft.1matches only exactly.requires.hostand lists it incritical.describeis optional. On-32601, or on a refusal beforeinitialize, the host falls back to a session.declared. Install and the first session verify it as usual.Release paths
requires.host(rules 1, 3)test:cli-releaseagainst the pinned CLI, extended to the Elm process bundletest:cli-releasepasses"Both" is needed only for a critical change. Everything else goes out on one side alone.
Bootstrap
Released hosts follow none of the rules above. The first step is a host-only release that implements rules 1 to 5 and still reads version-1 descriptors and records. Until it ships, extensions keep writing the flat version-1 keys. After it ships and the pins move, extensions adopt statements and version 2.
The same release also satisfies the removal condition of the three transitional mechanisms from #915: the legacy compile envelope in the Rust SDK,
compile_wire_requestin the CLI, and theTRANSITIONAL_FIELDSskip. They retire at the same time, when the pins move.Consequences for work in flight
feat/mep-workspace-discoverywritesworkspaceDiscovery: trueas a flat key. It is the old shape, but it is correct under version 1 and the bootstrap host converts it (rule 5). The branch keeps it. The PR waits until this spec is agreed.multiDocument(added in step 5) is not in any installed record today, so an installed provider always reads as single-document. Statements fix that without a new record field.Rejected alternatives
initializefor the probeOpen questions
All reactions