From af5ab0a1c5fb727e98f5d8f3568a93d7f86fd30e Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:46:06 -0500 Subject: [PATCH 01/26] docs(openspec): finalize the published site --- .../add-process-mep-extension/design.html | 4 +- .../add-process-mep-extension/tasks.md | 2 +- .../.openspec.yaml | 0 .../design.html | 25 ++++---- .../proposal.md | 0 .../specs/docs/cli-integration/spec.md | 0 .../specs/docs/examples/spec.md | 0 .../specs/docs/site/spec.md | 0 .../tasks.md | 2 +- openspec/specs/docs/cli-integration/spec.md | 50 +++++++++++++++ openspec/specs/docs/examples/spec.md | 58 ++++++++++++++++++ openspec/specs/docs/site/spec.md | 61 +++++++++++++++++++ 12 files changed, 186 insertions(+), 16 deletions(-) rename openspec/changes/{add-documentation-site => archive/2026-10-28-add-documentation-site}/.openspec.yaml (100%) rename openspec/changes/{add-documentation-site => archive/2026-10-28-add-documentation-site}/design.html (87%) rename openspec/changes/{add-documentation-site => archive/2026-10-28-add-documentation-site}/proposal.md (100%) rename openspec/changes/{add-documentation-site => archive/2026-10-28-add-documentation-site}/specs/docs/cli-integration/spec.md (100%) rename openspec/changes/{add-documentation-site => archive/2026-10-28-add-documentation-site}/specs/docs/examples/spec.md (100%) rename openspec/changes/{add-documentation-site => archive/2026-10-28-add-documentation-site}/specs/docs/site/spec.md (100%) rename openspec/changes/{add-documentation-site => archive/2026-10-28-add-documentation-site}/tasks.md (99%) create mode 100644 openspec/specs/docs/cli-integration/spec.md create mode 100644 openspec/specs/docs/examples/spec.md create mode 100644 openspec/specs/docs/site/spec.md diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 55297801..740d8c9c 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -22,11 +22,11 @@

add-process-mep-extension

-

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Proposed; no process or new-draft implementation

+

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Applying; published SDK prerequisite in progress

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The published SDK 0.3.0 exposes NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type still uses a numeric u64 ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The Python implementation does not opt into model transport and its manifest declares MEP 0.1 only. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

-

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

+

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. A candidate SDK 0.4.0 is under qualification and is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

The current proposal does not change protocol claims or release a guest. Every process task remains unstarted.

Repository evidence and references

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 3d78eb74..871bce00 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -1,6 +1,6 @@ # Tasks -This change is proposed. No process guest or newer-MEP implementation has started. +This change is being applied. The published SDK prerequisite is tracked in morphir-4wrx and finos/morphir-rust#316. Python process/model implementation tasks remain incomplete. ## 1. Negotiate current and legacy MEP (morphir-5hxy.1) diff --git a/openspec/changes/add-documentation-site/.openspec.yaml b/openspec/changes/archive/2026-10-28-add-documentation-site/.openspec.yaml similarity index 100% rename from openspec/changes/add-documentation-site/.openspec.yaml rename to openspec/changes/archive/2026-10-28-add-documentation-site/.openspec.yaml diff --git a/openspec/changes/add-documentation-site/design.html b/openspec/changes/archive/2026-10-28-add-documentation-site/design.html similarity index 87% rename from openspec/changes/add-documentation-site/design.html rename to openspec/changes/archive/2026-10-28-add-documentation-site/design.html index 52df927b..c0afa75c 100644 --- a/openspec/changes/add-documentation-site/design.html +++ b/openspec/changes/archive/2026-10-28-add-documentation-site/design.html @@ -13,16 +13,16 @@ Code goes in
 (or json, toml, bash, diff). Give sections an id and
   list them in nav.toc.
 -->
-
-
-
-
-
+
+
+
+
+
 
 
 

add-documentation-site

-

OpenSpec change · Bead morphir-ukvn.9 · 2026-10-28 · Published foundation; MoonBit visual alignment in progress

+

OpenSpec change · Bead morphir-ukvn.9 · 2026-10-28 · Published and verified; MoonBit visual alignment complete

diff --git a/openspec/changes/add-process-mep-extension/proposal.md b/openspec/changes/add-process-mep-extension/proposal.md index 9bb7cf18..6275950b 100644 --- a/openspec/changes/add-process-mep-extension/proposal.md +++ b/openspec/changes/add-process-mep-extension/proposal.md @@ -11,7 +11,6 @@ The Python compiler currently exposes a WASM MEP guest advertising legacy `0.1`, - Add exact `0.2.0-draft.1` negotiation and bilateral model transport admission, initially for the `morphir-ir`/`morphir-json` profile with the compiler's supported IR v3/v4 Library models. - Implement bounded, integrity-checked model envelopes and compileModel/generateModel adapters for the admitted tuples. Refuse unsupported profiles and guarantees explicitly; rich Ion transport is not advertised by this initial increment. - Publish and qualify both runtime choices, with accurate platform/runtime descriptors and host compatibility documentation. Qualify each documented host route independently. -- Keep this change proposed: no process guest or newer-MEP implementation has been added by PR #52. ## Capabilities diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a7898a12..7df8a3f9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1465,6 +1465,7 @@ dependencies = [ "cucumber", "morphir-core", "morphir-extension-sdk", + "morphir-json", "num-bigint 0.5.1", "ruff_python_ast", "ruff_python_codegen", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 60c90e72..afb5aa52 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -13,6 +13,7 @@ homepage = "https://github.com/finos/morphir-python" [workspace.dependencies] # Published Morphir crates. Never use sibling-checkout paths here. morphir-core = "=0.2.0" +morphir-json = "=0.2.0" morphir-extension-sdk = "=0.3.0" num-bigint = "0.5" # Ruff component crates evolve together; update this group as one unit. diff --git a/rust/crates/morphir-python-binding/Cargo.toml b/rust/crates/morphir-python-binding/Cargo.toml index 81ff6904..5e04cc85 100644 --- a/rust/crates/morphir-python-binding/Cargo.toml +++ b/rust/crates/morphir-python-binding/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["rlib", "cdylib"] [dependencies] num-bigint.workspace = true morphir-core.workspace = true +morphir-json.workspace = true morphir-extension-sdk.workspace = true ruff_python_parser.workspace = true ruff_python_ast.workspace = true diff --git a/rust/crates/morphir-python-binding/src/lib.rs b/rust/crates/morphir-python-binding/src/lib.rs index 77c08d98..46f3cbfd 100644 --- a/rust/crates/morphir-python-binding/src/lib.rs +++ b/rust/crates/morphir-python-binding/src/lib.rs @@ -26,6 +26,7 @@ mod backend; mod frontend; mod ir; +mod model_transport; mod modules; mod names; mod values; @@ -72,6 +73,13 @@ impl Extension for PythonExtension { } impl Frontend for PythonExtension { + fn compile_model( + &self, + request: morphir_extension_sdk::model::CompileModelRequest, + ) -> Result { + model_transport::compile(self, request) + } + fn compile(&self, request: CompileRequest) -> Result { let diagnostics = if frontend::parse_stage_requested(&request) { vec![Diagnostic { @@ -118,6 +126,13 @@ impl Frontend for PythonExtension { } impl Backend for PythonExtension { + fn generate_model( + &self, + request: morphir_extension_sdk::model::GenerateModelRequest, + ) -> Result { + model_transport::generate(self, request) + } + fn generate(&self, request: GenerateRequest) -> Result { Ok(match backend::generate(&request) { Ok(artifacts) => GenerateResult { diff --git a/rust/crates/morphir-python-binding/src/model_transport.rs b/rust/crates/morphir-python-binding/src/model_transport.rs new file mode 100644 index 00000000..a349eae5 --- /dev/null +++ b/rust/crates/morphir-python-binding/src/model_transport.rs @@ -0,0 +1,178 @@ +//! Explicit JSON-model admission around the shared compiler, without transcoding. +use crate::{PythonExtension, error}; +use morphir_extension_sdk::model::*; +use morphir_extension_sdk::prelude::*; + +pub(crate) fn support() -> ModelTransportSupport { + ModelTransportSupport { + tuples: [ModelOperation::CompileModel, ModelOperation::GenerateModel] + .into_iter() + .flat_map(|operation| { + ["3.0.0", "4.0.0"].map(|version| ModelTransportTuple { + operation, + profile: "morphir-ir".into(), + codec: ModelCodec::MorphirJson, + envelope_version: MODEL_ENVELOPE_VERSION.into(), + ir_version: version.into(), + }) + }) + .collect(), + // Base64 payloads leave at least 4 MiB for envelope/source metadata in + // the process transport's 16 MiB complete-message bound. + limits: ModelTransportLimits { + max_encoded_bytes: 12 * 1024 * 1024, + max_decoded_bytes: 8 * 1024 * 1024, + max_depth: 128, + max_nodes: 100_000, + }, + guarantees: vec![], + } +} + +fn invalid(error: impl std::fmt::Display) -> ExtensionError { + ExtensionError::InvalidParams(format!("MEP model admission: {error}")) +} + +fn admit(method: &str, value: &serde_json::Value) -> Result<()> { + let support = support(); + let admitted = NegotiatedModelTransport::select(&support, &support).map_err(invalid)?; + let mut capabilities = PythonExtension::capabilities(); + capabilities.model_transport = Some(support); + validate_model_invocation(&admitted, &capabilities, method, value).map_err(invalid) +} + +pub(crate) fn compile( + extension: &PythonExtension, + request: CompileModelRequest, +) -> Result { + admit( + "morphir.frontend.compileModel", + &serde_json::to_value(&request)?, + )?; + if !request.dependencies.is_empty() { + return Ok(CompileModelResult::Failed { + diagnostics: vec![error("PY001", "Expected Python and no dependencies")], + }); + } + let model_id = request.package.name.clone(); + let result = extension.compile(CompileRequest { + language_id: request.language_id, + sources: request.sources, + package: request.package, + dependencies: vec![], + options: request.options, + baseline: None, + })?; + if !result.success { + return Ok(CompileModelResult::Failed { + diagnostics: result.diagnostics, + }); + } + let ir = result + .ir + .ok_or_else(|| invalid("successful compiler result is missing IR"))?; + let bytes = bounded_json(&ir, support().limits.max_decoded_bytes)?; + let payload = ModelPayload::from_bytes(ModelCodec::MorphirJson, &bytes).map_err(invalid)?; + let model = MorphirModel { + envelope_version: MODEL_ENVELOPE_VERSION.into(), + model_id, + revision: format!("sha256:{}", payload.sha256), + ir_version: request.output.ir_version.clone(), + profile: request.output.profile.clone(), + payload, + evidence: vec![], + critical: vec![], + }; + retain_model(&model, &request.output, &support().limits).map_err(invalid)?; + Ok(CompileModelResult::Compiled { + model: Box::new(model), + diagnostics: result.diagnostics, + modules: result.modules, + }) +} + +pub(crate) fn generate( + extension: &PythonExtension, + request: GenerateModelRequest, +) -> Result { + admit( + "morphir.backend.generateModel", + &serde_json::to_value(&request)?, + )?; + let retained = + retain_model(&request.model, &request.input, &support().limits).map_err(invalid)?; + let limits = support().limits; + let source = std::str::from_utf8(retained.bytes()).map_err(invalid)?; + let ir = morphir_json::read( + source, + &mut JsonBudget { + remaining: limits.max_nodes, + depth: limits.max_depth as usize, + string_bytes: limits.max_decoded_bytes as usize, + }, + ) + .map_err(invalid)?; + extension.generate(GenerateRequest { + ir, + target: request.target, + options: request.options, + }) +} + +/// Stop JSON serialization before extending the payload buffer beyond its bound. +fn bounded_json(value: &serde_json::Value, limit: u32) -> Result> { + struct Buffer { + bytes: Vec, + limit: usize, + } + impl std::io::Write for Buffer { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + if self.bytes.len().saturating_add(bytes.len()) > self.limit { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "model payload exceeds decoded limit", + )); + } + self.bytes.extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + let mut buffer = Buffer { + bytes: Vec::new(), + limit: limit as usize, + }; + serde_json::to_writer(&mut buffer, value).map_err(invalid)?; + Ok(buffer.bytes) +} + +// Materialize only after SDK admission, using the published literal-JSON reader +// so serde-private-looking keys cannot acquire a different scalar meaning. +struct JsonBudget { + remaining: u32, + depth: usize, + string_bytes: usize, +} +impl morphir_json::ReadBudget for JsonBudget { + type Error = &'static str; + fn before_value( + &mut self, + depth: usize, + _: morphir_json::ValueKind, + ) -> std::result::Result<(), Self::Error> { + if depth >= self.depth { + return Err("model depth limit"); + } + self.remaining = self.remaining.checked_sub(1).ok_or("model node limit")?; + Ok(()) + } + fn before_string(&mut self, value: &str) -> std::result::Result<(), Self::Error> { + if value.len() > self.string_bytes { + Err("model string limit") + } else { + Ok(()) + } + } +} diff --git a/rust/crates/morphir-python-binding/tests/model_transport.rs b/rust/crates/morphir-python-binding/tests/model_transport.rs new file mode 100644 index 00000000..b9066d2d --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/model_transport.rs @@ -0,0 +1,228 @@ +//! The model boundary retains supported semantics and refuses unsupported admission. +use morphir_extension_sdk::model::*; +use morphir_extension_sdk::prelude::*; +use morphir_python_binding::PythonExtension; +use serde_json::{Value, json}; + +fn a_request(version: &str) -> CompileModelRequest { + CompileModelRequest { + language_id: "python".into(), + sources: SourceSet { + root: None, + documents: vec![SourceDocument { + uri: "model.py".into(), language_id: "python".into(), version: 1, + text: "from dataclasses import dataclass\n@dataclass(frozen=True)\nclass Box:\n value: int\n".into(), + }], + }, + package: CompilePackage { name: "acme".into(), exposed_modules: None }, + dependencies: vec![], + options: CompileOptions { ir_version: version.into(), ..Default::default() }, + context_digest: "a".repeat(64), + output: ModelTransportTuple { + operation: ModelOperation::CompileModel, profile: "morphir-ir".into(), + codec: ModelCodec::MorphirJson, envelope_version: MODEL_ENVELOPE_VERSION.into(), ir_version: version.into(), + }, + } +} + +fn a_model(version: &str) -> MorphirModel { + match PythonExtension.compile_model(a_request(version)).unwrap() { + CompileModelResult::Compiled { model, .. } => *model, + CompileModelResult::Failed { diagnostics } => panic!("{diagnostics:?}"), + } +} + +fn generation(model: MorphirModel) -> GenerateModelRequest { + GenerateModelRequest { + input: ModelTransportTuple { + operation: ModelOperation::GenerateModel, + profile: "morphir-ir".into(), + codec: ModelCodec::MorphirJson, + envelope_version: MODEL_ENVELOPE_VERSION.into(), + ir_version: model.ir_version.clone(), + }, + model, + target: "python".into(), + context_digest: "a".repeat(64), + required_guarantees: vec![], + options: Default::default(), + } +} + +#[test] +fn json_models_compile_and_generate_with_the_legacy_semantics() { + for version in ["3.0.0", "4.0.0"] { + let model = a_model(version); + assert_eq!(model.envelope_version, MODEL_ENVELOPE_VERSION); + assert_eq!(model.ir_version, version); + assert_eq!(model.model_id, "acme"); + let request = generation(model); + let retained = retain_model( + &request.model, + &request.input, + &ModelTransportLimits { + max_encoded_bytes: 1024 * 1024, + max_decoded_bytes: 1024 * 1024, + max_depth: 128, + max_nodes: 100_000, + }, + ) + .unwrap(); + let ir: Value = serde_json::from_slice(retained.bytes()).unwrap(); + let legacy = PythonExtension + .generate(GenerateRequest { + ir, + target: "python".into(), + options: Default::default(), + }) + .unwrap(); + let generated = PythonExtension.generate_model(request).unwrap(); + assert!(generated.success, "{:?}", generated.diagnostics); + let mut recompile = a_request(version); + recompile.sources.documents = generated + .artifacts + .iter() + .map(|artifact| SourceDocument { + uri: artifact.path.clone(), + language_id: "python".into(), + version: 1, + text: artifact.content.clone(), + }) + .collect(); + let reparsed = PythonExtension.compile_model(recompile).unwrap(); + let CompileModelResult::Compiled { + model: reparsed, .. + } = reparsed + else { + panic!("generated model should recompile") + }; + assert_eq!(reparsed.payload, a_model(version).payload); + assert_eq!( + serde_json::to_value(generated).unwrap(), + serde_json::to_value(legacy).unwrap() + ); + } +} + +#[test] +fn model_compilation_preserves_language_diagnostics() { + let mut request = a_request("4.0.0"); + request.sources.documents[0].text = "import os\n".into(); + let legacy = PythonExtension + .compile(CompileRequest { + language_id: request.language_id.clone(), + sources: request.sources.clone(), + package: request.package.clone(), + dependencies: vec![], + options: request.options.clone(), + baseline: None, + }) + .unwrap(); + assert!(!legacy.success); + let CompileModelResult::Failed { diagnostics } = + PythonExtension.compile_model(request).unwrap() + else { + panic!("unsupported source must fail") + }; + assert_eq!( + serde_json::to_value(diagnostics).unwrap(), + serde_json::to_value(legacy.diagnostics).unwrap() + ); +} + +#[test] +fn malformed_envelopes_guarantees_and_unadvertised_forms_are_refused() { + let baseline = generation(a_model("4.0.0")); + let mut value = serde_json::to_value(&baseline).unwrap(); + value["model"]["critical"] = json!([]); + for (path, invalid) in [ + ("/model/payload/sha256", json!("0".repeat(64))), + ("/model/payload/decodedByteLength", json!(1)), + ("/model/payload/content", json!("not canonical base64")), + ("/model/critical", json!(["future.required"])), + ("/model/irVersion", json!("3.0.0")), + ("/input/codec", json!("ion-binary")), + ("/input/profile", json!("rich-model")), + ("/input/operation", json!("morphir.frontend.compileModel")), + ( + "/requiredGuarantees", + json!([{"guarantee":"example.typed","scope":"model:acme"}]), + ), + ] { + let mut invalid_request = value.clone(); + *invalid_request.pointer_mut(path).unwrap() = invalid; + let request = serde_json::from_value(invalid_request).unwrap(); + assert!( + PythonExtension.generate_model(request).is_err(), + "accepted {path}" + ); + } + let mut request = a_request("4.0.0"); + request.output.ir_version = "3.1.0".into(); + assert!(PythonExtension.compile_model(request).is_err()); +} + +#[test] +fn model_round_trip_retains_large_integer_precision_and_v3_refuses_it() { + let decimal = "1234567890123456789012345678901234567890"; + for version in ["3.0.0", "4.0.0"] { + let mut request = a_request(version); + request.options.types_only = false; + request.sources.documents[0].text = format!("def number() -> int:\n return {decimal}\n"); + match PythonExtension.compile_model(request).unwrap() { + CompileModelResult::Compiled { model, .. } => { + assert_eq!(version, "4.0.0"); + let generated = PythonExtension.generate_model(generation(*model)).unwrap(); + assert!(generated.success, "{:?}", generated.diagnostics); + assert!( + generated + .artifacts + .iter() + .any(|artifact| artifact.content.contains(decimal)) + ); + } + CompileModelResult::Failed { diagnostics } => { + assert_eq!(version, "3.0.0"); + assert!( + diagnostics + .iter() + .any(|item| item.message.contains("64-bit")) + ); + } + } + } +} + +#[test] +fn aggregate_nodes_and_dependency_support_are_bounded() { + let mut request = generation(a_model("4.0.0")); + let retained = retain_model( + &request.model, + &request.input, + &ModelTransportLimits { + max_encoded_bytes: 1024 * 1024, + max_decoded_bytes: 1024 * 1024, + max_depth: 128, + max_nodes: 100_000, + }, + ) + .unwrap(); + let mut ir: Value = serde_json::from_slice(retained.bytes()).unwrap(); + ir["extra"] = json!(vec![0; 100_001]); + request.model.payload = + ModelPayload::from_bytes(ModelCodec::MorphirJson, &serde_json::to_vec(&ir).unwrap()) + .unwrap(); + assert!(PythonExtension.generate_model(request).is_err()); + + let mut request = a_request("4.0.0"); + request.dependencies.push(CompileModelDependency { + package_name: "dep".into(), + model: a_model("4.0.0"), + }); + let CompileModelResult::Failed { diagnostics } = + PythonExtension.compile_model(request).unwrap() + else { + panic!("dependencies are unsupported") + }; + assert_eq!(diagnostics[0].code.as_deref(), Some("PY001")); +} From 22c7a8311b274745f2f86ee6e2af7774c68ebddc Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:04:20 -0500 Subject: [PATCH 04/26] feat(extension): serve standalone MEP sessions --- .../add-process-mep-extension/tasks.md | 4 +- rust/Cargo.lock | 6 + rust/Cargo.toml | 1 + .../morphir-python-extension/Cargo.toml | 6 + .../crates/morphir-python-extension/README.md | 27 +++++ .../morphir-python-extension/src/lib.rs | 2 + .../morphir-python-extension/src/main.rs | 24 ++++ .../morphir-python-extension/src/server.rs | 114 ++++++++++++++++++ .../morphir-python-extension/tests/framing.rs | 13 ++ .../morphir-python-extension/tests/session.rs | 109 +++++++++++++++++ 10 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 rust/crates/morphir-python-extension/README.md create mode 100644 rust/crates/morphir-python-extension/src/main.rs create mode 100644 rust/crates/morphir-python-extension/src/server.rs create mode 100644 rust/crates/morphir-python-extension/tests/session.rs diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 871bce00..2ace45e8 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -16,8 +16,8 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## 3. Serve native stdio sessions (morphir-5hxy.3) -- [ ] 3.1 Add the thin Rust process guest and documented `morphir-python-extension --stdio` entry point; verify it builds and executes without CPython/PyO3 or a user Rust toolchain. -- [ ] 3.2 Implement bounded Content-Length framing around the SDK session; test Unicode byte lengths, partial/truncated messages, duplicate/invalid lengths, overflow, invalid UTF-8/JSON and header/body limits. +- [x] 3.1 Add the thin Rust process guest and documented `morphir-python-extension --stdio` entry point; verify it builds and executes without CPython/PyO3 or a user Rust toolchain. +- [x] 3.2 Implement bounded Content-Length framing around the SDK session; test Unicode byte lengths, partial/truncated messages, duplicate/invalid lengths, overflow, invalid UTF-8/JSON and header/body limits. - [ ] 3.3 Preserve request IDs, notification silence, fixed session capabilities, shutdown/exit and protocol-only stdout; verify a real multi-request process test and document stderr and supervision behavior. ## 4. Distribute both runtime choices (morphir-5hxy.4) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 7df8a3f9..5fbc6a77 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1479,6 +1479,12 @@ dependencies = [ [[package]] name = "morphir-python-extension" version = "0.1.0" +dependencies = [ + "morphir-extension-sdk", + "morphir-python-binding", + "serde", + "serde_json", +] [[package]] name = "morphir-python-native" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index afb5aa52..6c1c135c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,6 +22,7 @@ ruff_python_ast = "=0.0.15" ruff_python_codegen = "=0.0.15" ruff_text_size = "=0.0.15" serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } tempfile = "3" [profile.release] diff --git a/rust/crates/morphir-python-extension/Cargo.toml b/rust/crates/morphir-python-extension/Cargo.toml index a7947eed..84009521 100644 --- a/rust/crates/morphir-python-extension/Cargo.toml +++ b/rust/crates/morphir-python-extension/Cargo.toml @@ -8,3 +8,9 @@ license.workspace = true repository.workspace = true description = "Native stdio MEP guest for the Morphir Python compiler" publish = false + +[dependencies] +morphir-python-binding = { path = "../morphir-python-binding", version = "0.4.2" } +morphir-extension-sdk.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["raw_value"] } diff --git a/rust/crates/morphir-python-extension/README.md b/rust/crates/morphir-python-extension/README.md new file mode 100644 index 00000000..7b094043 --- /dev/null +++ b/rust/crates/morphir-python-extension/README.md @@ -0,0 +1,27 @@ +# Morphir Python process guest + +This crate runs the shared Python compiler as a native MEP process. It has no +CPython or PyO3 dependency, and the executable needs no user Rust toolchain. +Source is parsed statically and is never imported or executed. + +Development build and entry point: + +```bash +cargo build --manifest-path rust/Cargo.toml --release -p morphir-python-extension +./rust/target/release/morphir-python-extension --stdio +``` + +In stdio mode stdout contains only Content-Length-framed UTF-8 JSON-RPC. +Headers are bounded to 8 KiB and complete bodies to 16 MiB. Lengths count bytes; +unknown headers are ignored and duplicate lengths are refused. A damaged frame +ends the connection. Invalid JSON in an otherwise valid frame gets a parse-error +response and the next framed request can recover. Notifications get no response; +`morphir.exit` ends the process. Each process owns its SDK protocol session. + +The host owns deadlines and process cleanup. A native process has ordinary OS +rights; choosing it does not provide a WASM sandbox. + +The current development build advertises legacy MEP 0.1. Exact-draft discovery +and model transport wait for the qualified published SDK prerequisite and +conformance checks. Platform archives and installation recipes are still under +development. The retained WASM release remains the qualified installation route. diff --git a/rust/crates/morphir-python-extension/src/lib.rs b/rust/crates/morphir-python-extension/src/lib.rs index ea0e7d8b..aa2df2e5 100644 --- a/rust/crates/morphir-python-extension/src/lib.rs +++ b/rust/crates/morphir-python-extension/src/lib.rs @@ -1,2 +1,4 @@ //! Bounded stdio transport for the Morphir Python process guest. pub mod framing; +mod server; +pub use server::serve; diff --git a/rust/crates/morphir-python-extension/src/main.rs b/rust/crates/morphir-python-extension/src/main.rs new file mode 100644 index 00000000..e4cc81c3 --- /dev/null +++ b/rust/crates/morphir-python-extension/src/main.rs @@ -0,0 +1,24 @@ +//! Standalone process entry point; the compiler never imports or executes Python. +use morphir_python_extension::serve; + +fn main() { + let mut arguments = std::env::args_os().skip(1); + let argument = arguments.next(); + if arguments.next().is_some() { + eprintln!("Usage: morphir-python-extension --stdio"); + std::process::exit(2); + } + match argument.as_deref() { + Some(value) if value == "--stdio" => { + if let Err(error) = serve(&mut std::io::stdin().lock(), &mut std::io::stdout().lock()) { + eprintln!("morphir-python-extension: {error}"); + std::process::exit(1); + } + } + Some(value) if value == "--help" => println!("Usage: morphir-python-extension --stdio"), + _ => { + eprintln!("Usage: morphir-python-extension --stdio"); + std::process::exit(2); + } + } +} diff --git a/rust/crates/morphir-python-extension/src/server.rs b/rust/crates/morphir-python-extension/src/server.rs new file mode 100644 index 00000000..1017d056 --- /dev/null +++ b/rust/crates/morphir-python-extension/src/server.rs @@ -0,0 +1,114 @@ +//! Framed stdio sessions around the SDK's native protocol endpoint. +use crate::framing::{MAX_BODY_BYTES, read_frame, write_frame}; +use morphir_extension_sdk::{ + NativeExtension, + protocol::{ExtensionRequest, error_codes, methods}, +}; +use morphir_python_binding::PythonExtension; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::value::RawValue; +use std::io::{self, BufRead, Write}; + +#[derive(Deserialize)] +struct MessageHead { + jsonrpc: String, + method: String, + // Explicit null is a malformed request ID, not a notification. + #[serde(default, deserialize_with = "present_id")] + id: Option>, +} + +fn present_id<'de, D: Deserializer<'de>>(reader: D) -> Result>, D::Error> { + Box::::deserialize(reader).map(Some) +} + +fn write_error(writer: &mut impl Write, code: i32, message: &str) -> io::Result<()> { + write_response( + writer, + &serde_json::json!({ + "jsonrpc": "2.0", "id": null, "error": { "code": code, "message": message }, + }), + ) +} + +fn write_response(writer: &mut impl Write, value: &impl Serialize) -> io::Result<()> { + struct Body(Vec); + impl Write for Body { + fn write(&mut self, bytes: &[u8]) -> io::Result { + if self.0.len().saturating_add(bytes.len()) > MAX_BODY_BYTES { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "MEP response exceeds 16 MiB", + )); + } + self.0.extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + let mut body = Body(Vec::new()); + serde_json::to_writer(&mut body, value).map_err(io::Error::other)?; + write_frame(writer, &body.0) +} + +/// Serve a fresh SDK session, with protocol frames as the only output. +/// +/// Invalid JSON or request envelopes receive JSON-RPC errors without entering +/// the compiler. A damaged frame is fatal because its next boundary is unknown. +/// Notifications receive no response; exit stops processing further messages. +/// Every invocation owns an independent native SDK session. +pub fn serve(reader: &mut impl BufRead, writer: &mut impl Write) -> io::Result<()> { + let extension = NativeExtension::frontend_backend(PythonExtension) + .map_err(|error| io::Error::other(error.to_string()))?; + let session = extension.open_protocol(); + while let Some(body) = read_frame(reader)? { + // Borrow the raw document to validate syntax without materializing + // opaque parameters or giving serde-private-looking keys meaning. + if serde_json::from_slice::<&RawValue>(&body).is_err() { + write_error(writer, error_codes::PARSE_ERROR, "Invalid JSON")?; + continue; + } + let head = match serde_json::from_slice::(&body) { + Ok(head) if head.jsonrpc == "2.0" => head, + _ => { + write_error( + writer, + error_codes::INVALID_REQUEST, + "Invalid JSON-RPC request", + )?; + continue; + } + }; + if head.id.is_none() { + if head.method == methods::EXIT { + break; + } + // Initialized acknowledges a handshake already held by the SDK. + // Unknown or unadvertised notifications do no work. + continue; + } + let request = match serde_json::from_slice::(&body) { + Ok(request) => request, + Err(_) => { + write_error(writer, error_codes::INVALID_REQUEST, "Invalid MEP request")?; + continue; + } + }; + write_response(writer, &session.handle(request))?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn oversized_response_is_refused_before_any_frame_is_emitted() { + let mut output = Vec::new(); + let value = "x".repeat(MAX_BODY_BYTES); + assert!(write_response(&mut output, &value).is_err()); + assert!(output.is_empty()); + } +} diff --git a/rust/crates/morphir-python-extension/tests/framing.rs b/rust/crates/morphir-python-extension/tests/framing.rs index 7cdc4b16..619ff1e2 100644 --- a/rust/crates/morphir-python-extension/tests/framing.rs +++ b/rust/crates/morphir-python-extension/tests/framing.rs @@ -60,3 +60,16 @@ fn content_length_is_case_insensitive_and_uses_bytes() { write_frame(&mut wire, "λ".as_bytes()).unwrap(); assert_eq!(wire, "Content-Length: 2\r\n\r\nλ".as_bytes()); } + +#[test] +fn a_complete_header_at_the_limit_is_accepted() { + let prefix = "Content-Length: 2\r\nOther: "; + let suffix = "\r\n\r\n"; + let header = format!( + "{prefix}{}{suffix}", + "x".repeat(MAX_HEADER_BYTES - prefix.len() - suffix.len()) + ); + assert_eq!(header.len(), MAX_HEADER_BYTES); + let mut input = Cursor::new(format!("{header}{{}}")); + assert_eq!(read_frame(&mut input).unwrap().unwrap(), b"{}"); +} diff --git a/rust/crates/morphir-python-extension/tests/session.rs b/rust/crates/morphir-python-extension/tests/session.rs new file mode 100644 index 00000000..73d2fd60 --- /dev/null +++ b/rust/crates/morphir-python-extension/tests/session.rs @@ -0,0 +1,109 @@ +//! A real process and in-memory streams share the same SDK lifecycle. +use morphir_python_extension::{ + framing::{read_frame, write_frame}, + serve, +}; +use serde_json::{Value, json}; +use std::io::{Cursor, Write}; +use std::process::{Command, Stdio}; + +fn wire(messages: &[Value]) -> Vec { + let mut bytes = Vec::new(); + for message in messages { + write_frame(&mut bytes, &serde_json::to_vec(message).unwrap()).unwrap(); + } + bytes +} +fn responses(bytes: Vec) -> Vec { + let mut input = Cursor::new(bytes); + let mut responses = Vec::new(); + while let Some(body) = read_frame(&mut input).unwrap() { + responses.push(serde_json::from_slice(&body).unwrap()); + } + responses +} +fn initialize() -> Value { + json!({"jsonrpc":"2.0","id":1,"method":"morphir.initialize","params":{"protocolVersions":["0.1"],"host":{"name":"test","version":"1"}}}) +} + +#[test] +fn sessions_preserve_lifecycle_ids_and_notification_silence() { + let messages = [ + json!({"jsonrpc":"2.0","id":0,"method":"morphir.extension.info","params":{}}), + initialize(), + json!({"jsonrpc":"2.0","method":"morphir.initialized"}), + json!({"jsonrpc":"2.0","method":"unknown.notification","params":{"text":"λ"}}), + json!({"jsonrpc":"2.0","id":2,"method":"morphir.ping","params":{}}), + json!({"jsonrpc":"2.0","id":3,"method":"morphir.shutdown","params":{}}), + json!({"jsonrpc":"2.0","id":4,"method":"morphir.extension.info","params":{}}), + json!({"jsonrpc":"2.0","method":"morphir.exit"}), + json!({"jsonrpc":"2.0","id":5,"method":"morphir.ping","params":{}}), + ]; + let mut output = Vec::new(); + serve(&mut Cursor::new(wire(&messages)), &mut output).unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 5); + assert_eq!( + replies + .iter() + .map(|value| value["id"].as_u64().unwrap()) + .collect::>(), + [0, 1, 2, 3, 4] + ); + assert_eq!(replies[0]["error"]["code"], -32014); + assert_eq!(replies[1]["result"]["protocolVersion"], "0.1"); + assert_eq!(replies[2]["result"]["ok"], true); + assert_eq!(replies[4]["error"]["code"], -32014); +} + +#[test] +fn invalid_json_is_refused_and_the_next_framed_request_can_recover() { + let mut input = Vec::new(); + write_frame(&mut input, b"{invalid").unwrap(); + input.extend(wire(&[ + json!({"jsonrpc":"2.0","id":7,"method":"morphir.ping","params":{}}), + ])); + let mut output = Vec::new(); + serve(&mut Cursor::new(input), &mut output).unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 2); + assert_eq!(replies[0]["error"]["code"], -32700); + assert_eq!(replies[0]["id"], Value::Null); + assert_eq!(replies[1]["id"], 7); +} + +#[test] +fn malformed_frames_fail_without_writing_non_protocol_output() { + let mut output = Vec::new(); + assert!( + serve( + &mut Cursor::new(b"Content-Length: 999999999\r\n\r\n"), + &mut output + ) + .is_err() + ); + assert!(output.is_empty()); +} + +#[test] +fn standalone_process_needs_no_python_or_rust_environment() { + let mut child = Command::new(env!("CARGO_BIN_EXE_morphir-python-extension")) + .arg("--stdio") + .env_clear() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let input = wire(&[ + initialize(), + json!({"jsonrpc":"2.0","method":"morphir.exit"}), + ]); + child.stdin.take().unwrap().write_all(&input).unwrap(); + let result = child.wait_with_output().unwrap(); + assert!(result.status.success(), "{:?}", result.stderr); + assert!(result.stderr.is_empty()); + let replies = responses(result.stdout); + assert_eq!(replies.len(), 1); + assert_eq!(replies[0]["result"]["extension"]["id"], "morphir-python"); +} From 0df65849fb5e35343da3a98c20d6ecb86bd29504 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:08:06 -0500 Subject: [PATCH 05/26] fix(binding): enforce portable model admission --- .../add-process-mep-extension/tasks.md | 6 +- rust/crates/morphir-python-binding/README.md | 29 +++++++- .../src/model_transport.rs | 37 +++++++++++ .../tests/model_transport.rs | 66 ++++++++++++++++++- 4 files changed, 133 insertions(+), 5 deletions(-) diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 2ace45e8..9d7eea6c 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -10,9 +10,9 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## 2. Adapt supported model envelopes (morphir-5hxy.2) -- [ ] 2.1 Implement bounded canonical JSON model-envelope admission using SDK helpers; verify base64, byte length, hash, critical fields, semantic IR version and size-limit negative cases. -- [ ] 2.2 Adapt compileModel/generateModel for admitted JSON-profile v3/v4 Library tuples through the shared compiler; verify compile/generate/recompile semantics and diagnostic parity with legacy operations. -- [ ] 2.3 Refuse unadvertised Ion profiles, unsupported distributions and guarantees without lossy fallback; test these refusals and document the initial support matrix. +- [x] 2.1 Implement bounded canonical JSON model-envelope admission using SDK helpers; verify base64, byte length, hash, critical fields, semantic IR version and size-limit negative cases. +- [x] 2.2 Adapt compileModel/generateModel for admitted JSON-profile v3/v4 Library tuples through the shared compiler; verify compile/generate/recompile semantics and diagnostic parity with legacy operations. +- [x] 2.3 Refuse unadvertised Ion profiles, unsupported distributions and guarantees without lossy fallback; test these refusals and document the initial support matrix. ## 3. Serve native stdio sessions (morphir-5hxy.3) diff --git a/rust/crates/morphir-python-binding/README.md b/rust/crates/morphir-python-binding/README.md index 1b67cebc..e52d8f7f 100644 --- a/rust/crates/morphir-python-binding/README.md +++ b/rust/crates/morphir-python-binding/README.md @@ -11,7 +11,8 @@ This is an initial implementation of a defined subset of Python 3.12 and later and Morphir IR v3 and v4. It supports ADT declarations, fixed tuples and annotated pure conditional functions, typed calls and unary lambdas. It does not implement all Python syntax or all Morphir IR nodes, and does not claim full Morphir Compatibility Kit conformance for this extension. -The crate depends on the published `morphir-core` and `morphir-extension-sdk` crates only. +Morphir dependencies use published `morphir-core`, `morphir-json` and +`morphir-extension-sdk` crates. It does not link CPython; the separate `morphir-python-native` crate in this workspace adapts it for the `morphir-python` Python package. @@ -23,6 +24,32 @@ and releases it. Bundles up to `extension/python/v0.4.0` remain available from t function support, and Morphir CLI `0.4.0-beta.1` and later cannot compile with it. Use the [extension guide](../../../docs/python-extension.md) to build and install the bundle. +## Development model adapter + +The direct Rust `Frontend::compile_model` and `Backend::generate_model` methods +share the legacy compiler. Model transport is not yet advertised in MEP discovery; +protocol qualification and the published SDK prerequisite remain in progress in +`add-process-mep-extension`. + +| Boundary | Initial support | +| --- | --- | +| Operation | `compileModel`, `generateModel` | +| Profile and codec | `morphir-ir`, `morphir-json` | +| Envelope version | `0.1.0-draft.1` | +| Semantic IR versions | `3.0.0`, draft `4.0.0` | +| Distribution | `Library`, within the documented Python subset | +| Evidence | JSON only; no preservation or typing guarantees established | +| Encoded / decoded bytes | 12 MiB / 8 MiB, aggregated with dependencies and evidence | +| Parse budget | 128 levels, 100,000 aggregate value nodes | + +Admission checks canonical padded base64, exact decoded length and SHA-256, +semantic IR version, critical fields and resource bounds before generation. +Compilation output has the same decoded-byte bound. Model compilation preserves +legacy diagnostics and refuses dependency compilation. Generation refuses `Specs` +and `Application` distributions with `PY004`. Ion payloads and evidence, other +profiles, unsupported tuples and required guarantees are refused without +conversion. Limits negotiated by a session can only narrow these static bounds. + ## A complete model Save this as `models.py`. It is valid Python and supported frontend input: diff --git a/rust/crates/morphir-python-binding/src/model_transport.rs b/rust/crates/morphir-python-binding/src/model_transport.rs index a349eae5..0243b950 100644 --- a/rust/crates/morphir-python-binding/src/model_transport.rs +++ b/rust/crates/morphir-python-binding/src/model_transport.rs @@ -41,10 +41,29 @@ fn admit(method: &str, value: &serde_json::Value) -> Result<()> { validate_model_invocation(&admitted, &capabilities, method, value).map_err(invalid) } +// Native SDK builds can inspect Ion evidence, but WASM cannot. Keep this +// adapter's accepted subset identical across runtimes, including sidecars. +fn require_json(model: &MorphirModel) -> Result<()> { + if model.payload.codec != ModelCodec::MorphirJson + || model + .evidence + .iter() + .any(|item| item.evidence.codec != ModelCodec::MorphirJson) + { + return Err(invalid( + "Python model transport is JSON-only, including evidence", + )); + } + Ok(()) +} + pub(crate) fn compile( extension: &PythonExtension, request: CompileModelRequest, ) -> Result { + for dependency in &request.dependencies { + require_json(&dependency.model)?; + } admit( "morphir.frontend.compileModel", &serde_json::to_value(&request)?, @@ -95,6 +114,7 @@ pub(crate) fn generate( extension: &PythonExtension, request: GenerateModelRequest, ) -> Result { + require_json(&request.model)?; admit( "morphir.backend.generateModel", &serde_json::to_value(&request)?, @@ -176,3 +196,20 @@ impl morphir_json::ReadBudget for JsonBudget { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn output_json_stops_at_its_decoded_budget() { + let value = serde_json::json!("λ"); + assert_eq!(bounded_json(&value, 4).unwrap(), "\"λ\"".as_bytes()); + assert!( + bounded_json(&value, 3) + .unwrap_err() + .to_string() + .contains("decoded limit") + ); + } +} diff --git a/rust/crates/morphir-python-binding/tests/model_transport.rs b/rust/crates/morphir-python-binding/tests/model_transport.rs index b9066d2d..9ee74cb4 100644 --- a/rust/crates/morphir-python-binding/tests/model_transport.rs +++ b/rust/crates/morphir-python-binding/tests/model_transport.rs @@ -212,7 +212,8 @@ fn aggregate_nodes_and_dependency_support_are_bounded() { request.model.payload = ModelPayload::from_bytes(ModelCodec::MorphirJson, &serde_json::to_vec(&ir).unwrap()) .unwrap(); - assert!(PythonExtension.generate_model(request).is_err()); + let error = PythonExtension.generate_model(request).unwrap_err(); + assert!(error.to_string().contains("node_limit"), "{error}"); let mut request = a_request("4.0.0"); request.dependencies.push(CompileModelDependency { @@ -226,3 +227,66 @@ fn aggregate_nodes_and_dependency_support_are_bounded() { }; assert_eq!(diagnostics[0].code.as_deref(), Some("PY001")); } + +#[test] +fn ion_evidence_is_refused_uniformly_across_native_and_wasm() { + let mut request = generation(a_model("4.0.0")); + request.model.evidence.push(ModelGuaranteeEvidence { + guarantee: "urn:example:optional".into(), + model_id: request.model.model_id.clone(), + model_revision: request.model.revision.clone(), + context_digest: request.context_digest.clone(), + scope: format!("model:{}", request.model.model_id), + producer: "fixture".into(), + evidence: ModelPayload::from_bytes(ModelCodec::IonBinary, &[0xe0, 1, 0, 0xea, 0x21, 1]) + .unwrap(), + }); + let error = PythonExtension.generate_model(request).unwrap_err(); + assert!(error.to_string().contains("JSON-only"), "{error}"); +} + +#[test] +fn unsupported_distribution_forms_are_explicit_language_refusals() { + for distribution in [ + json!({"Specs":{"packageName":"acme","dependencies":{},"spec":{"modules":{}}}}), + json!({"Application":{"packageName":"acme","dependencies":{},"def":{"modules":{}},"entryPoints":{}}}), + ] { + let ir = json!({"formatVersion":4,"distribution":distribution}); + // Prove these are complete valid IR, rather than malformed enum inputs. + serde_json::from_value::(ir.clone()).unwrap(); + let mut request = generation(a_model("4.0.0")); + request.model.payload = + ModelPayload::from_bytes(ModelCodec::MorphirJson, &serde_json::to_vec(&ir).unwrap()) + .unwrap(); + let result = PythonExtension.generate_model(request).unwrap(); + assert!(!result.success); + assert!(result.artifacts.is_empty()); + assert_eq!(result.diagnostics[0].code.as_deref(), Some("PY004")); + assert!(result.diagnostics[0].message.contains("Only Library")); + } +} + +#[test] +fn semantic_versions_and_payload_resource_limits_are_checked_before_generation() { + let payloads = [ + ( + br#"{"formatVersion":3,"distribution":{}}"#.to_vec(), + "unsupported_ir_version", + ), + ( + format!("{}0{}", "[".repeat(128), "]".repeat(128)).into_bytes(), + "depth_limit", + ), + (vec![b' '; 8 * 1024 * 1024 + 1], "decoded_limit"), + ]; + for (bytes, reason) in payloads { + let mut request = generation(a_model("4.0.0")); + request.model.payload = ModelPayload::from_bytes(ModelCodec::MorphirJson, &bytes).unwrap(); + let error = PythonExtension.generate_model(request).unwrap_err(); + assert!(error.to_string().contains(reason), "{reason}: {error}"); + } + let mut request = generation(a_model("4.0.0")); + request.model.payload.content = "A".repeat(12 * 1024 * 1024 + 4); + let error = PythonExtension.generate_model(request).unwrap_err(); + assert!(error.to_string().contains("encoded_limit"), "{error}"); +} From 30a216aed0cb60e55343601019769a7baf8f9583 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:16:40 -0500 Subject: [PATCH 06/26] build(extension): stage canonical process bundles --- .../add-process-mep-extension/design.html | 4 +- .../add-process-mep-extension/tasks.md | 2 +- .../crates/morphir-python-extension/README.md | 32 ++++ .../morphir-python-extension/src/main.rs | 15 +- .../morphir-python-extension/tests/session.rs | 7 +- rust/extension.toml | 2 + scripts/extension_bundle.py | 44 +++-- scripts/process_extension.py | 178 ++++++++++++++++++ tests/unit/test_extension_bundle.py | 45 +++++ tests/unit/test_process_extension.py | 114 +++++++++++ 10 files changed, 419 insertions(+), 24 deletions(-) create mode 100644 scripts/process_extension.py create mode 100644 tests/unit/test_process_extension.py diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 9d44561e..4bed3902 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -26,8 +26,8 @@

add-process-mep-extension

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The published SDK 0.3.0 exposes NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type still uses a numeric u64 ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The Python implementation does not opt into model transport and its manifest declares MEP 0.1 only. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

-

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. A candidate SDK 0.4.0 is under qualification and is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

-

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.
+

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. Crate-tag publication approval is pending; the SDK is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

+

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. Approval is pending for a separate upstream Python JSON profile; no existing MoonBit check will be relaxed and no unit-test result substitutes for executable qualification.

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. Protocol discovery remains legacy-only until the published SDK prerequisite and candidate-bound conformance pass. All five task groups remain incomplete.

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk 0.3.0: native.rs, traits.rs and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 9d7eea6c..6520b064 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -35,5 +35,5 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## Workflow follow-up -- Implementation requires separately applying this proposal after review. +- Implementation is in progress under the active apply goal. SDK publication and a separate upstream Python MCK profile decision are pending; the new draft remains unadvertised. - Merge, tags and publication require maintainer approval; sync/archive only after implementation and qualification are complete. diff --git a/rust/crates/morphir-python-extension/README.md b/rust/crates/morphir-python-extension/README.md index 7b094043..0d1f220e 100644 --- a/rust/crates/morphir-python-extension/README.md +++ b/rust/crates/morphir-python-extension/README.md @@ -11,6 +11,10 @@ cargo build --manifest-path rust/Cargo.toml --release -p morphir-python-extensio ./rust/target/release/morphir-python-extension --stdio ``` +With no arguments the executable also serves stdio, matching the canonical +native bundler and installed process launcher. `--stdio` is an explicit alias; +`--help` prints usage and exits. + In stdio mode stdout contains only Content-Length-framed UTF-8 JSON-RPC. Headers are bounded to 8 KiB and complete bodies to 16 MiB. Lengths count bytes; unknown headers are ignored and duplicate lengths are refused. A damaged frame @@ -25,3 +29,31 @@ The current development build advertises legacy MEP 0.1. Exact-draft discovery and model transport wait for the qualified published SDK prerequisite and conformance checks. Platform archives and installation recipes are still under development. The retained WASM release remains the qualified installation route. + +## Stage a development bundle + +From a clean candidate checkout, build the release executable and run the pinned +Morphir CLI's native bundler through the packaging helper: + +```bash +python -m scripts.process_extension \ + --executable rust/target/release/morphir-python-extension \ + --output .dev/process-bundle \ + --commit "$(git rev-parse HEAD)" \ + --report .dev/qualification/process-package.json \ + --host-check +``` + +On Windows the executable path ends in `.exe`. The helper selects an explicit +supported target, verifies the pinned CLI archive and stages a versioned raw +executable, its checksum and canonical `release.json`. It records the canonical +default argument list, explicit `--stdio` alias, executable mode, source commit, +Cargo lock digest, bundler identity, exact artifact bytes and live claims in the +separate report. Receipts are not additional members of the canonical bundle. + +`--host-check` requires the development Python environment with the matching +native compiler. It publishes into a temporary local repository, installs into +an isolated Morphir home, deletes the repository and runs offline v3/v4 +compile/generate/recompile comparisons against the Python API. This legacy route +check does not qualify the new MEP draft. Platform release builds, new-draft +qualification and promotion gates remain in progress. diff --git a/rust/crates/morphir-python-extension/src/main.rs b/rust/crates/morphir-python-extension/src/main.rs index e4cc81c3..03e22589 100644 --- a/rust/crates/morphir-python-extension/src/main.rs +++ b/rust/crates/morphir-python-extension/src/main.rs @@ -8,13 +8,16 @@ fn main() { eprintln!("Usage: morphir-python-extension --stdio"); std::process::exit(2); } - match argument.as_deref() { - Some(value) if value == "--stdio" => { - if let Err(error) = serve(&mut std::io::stdin().lock(), &mut std::io::stdout().lock()) { - eprintln!("morphir-python-extension: {error}"); - std::process::exit(1); - } + // Canonical native bundles launch their executable without arguments. + // Keep --stdio as an explicit alias for host configurations and manual use. + if argument.as_deref().is_none_or(|value| value == "--stdio") { + if let Err(error) = serve(&mut std::io::stdin().lock(), &mut std::io::stdout().lock()) { + eprintln!("morphir-python-extension: {error}"); + std::process::exit(1); } + return; + } + match argument.as_deref() { Some(value) if value == "--help" => println!("Usage: morphir-python-extension --stdio"), _ => { eprintln!("Usage: morphir-python-extension --stdio"); diff --git a/rust/crates/morphir-python-extension/tests/session.rs b/rust/crates/morphir-python-extension/tests/session.rs index 73d2fd60..10e59615 100644 --- a/rust/crates/morphir-python-extension/tests/session.rs +++ b/rust/crates/morphir-python-extension/tests/session.rs @@ -87,8 +87,13 @@ fn malformed_frames_fail_without_writing_non_protocol_output() { #[test] fn standalone_process_needs_no_python_or_rust_environment() { + standalone(&["--stdio"]); + standalone(&[]); +} + +fn standalone(arguments: &[&str]) { let mut child = Command::new(env!("CARGO_BIN_EXE_morphir-python-extension")) - .arg("--stdio") + .args(arguments) .env_clear() .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/rust/extension.toml b/rust/extension.toml index 6f3dcf62..e978c300 100644 --- a/rust/extension.toml +++ b/rust/extension.toml @@ -32,3 +32,5 @@ release = "https://github.com/finos/morphir/releases/download/v0.4.0-beta.10" "aarch64-unknown-linux-gnu" = "76a92a11030cd254d2283e98363cf203f86aaecd583fa480bdf6f508c86710f6" "aarch64-apple-darwin" = "98635ec74fbdc3c2cef1d4ae184bb1c5ed9e2960575f3d8abb4b97e0e4da363f" "x86_64-apple-darwin" = "4080dcfb04f6e1c902bce2780756c6af44320ac0dd6ff6bd953ca74a0da8e128" + +"x86_64-pc-windows-msvc" = "c178002d570f4cb8c9ceed4bf7b51f04d4ebec2458765935839c90ac1cc01176" diff --git a/scripts/extension_bundle.py b/scripts/extension_bundle.py index 53f9699e..bc41bd99 100644 --- a/scripts/extension_bundle.py +++ b/scripts/extension_bundle.py @@ -32,6 +32,7 @@ import tempfile import tomllib import urllib.request +import zipfile from pathlib import Path from typing import Any @@ -220,9 +221,11 @@ def receipt(bundle: Path, *, version: str | None = None) -> dict[str, object]: def _target() -> str: machine = platform.machine().lower() arch = {"arm64": "aarch64", "amd64": "x86_64"}.get(machine, machine) - system = {"Darwin": "apple-darwin", "Linux": "unknown-linux-gnu"}.get( - platform.system() - ) + system = { + "Darwin": "apple-darwin", + "Linux": "unknown-linux-gnu", + "Windows": "pc-windows-msvc", + }.get(platform.system()) if system is None: raise BundleError(f"no pinned Morphir CLI for {platform.system()}") return f"{arch}-{system}" @@ -236,8 +239,10 @@ def morphir_cli() -> tuple[Path, str]: if expected is None: raise BundleError(f"no pinned Morphir CLI digest for {target}") directory = CACHE / f"morphir-{host['version']}-{target}" - binary = directory / "morphir" - archive_name = f"morphir-{host['version']}-{target}.tgz" + is_windows = target.endswith("windows-msvc") + binary = directory / ("morphir.exe" if is_windows else "morphir") + extension = "zip" if is_windows else "tgz" + archive_name = f"morphir-{host['version']}-{target}.{extension}" if not binary.is_file(): with urllib.request.urlopen( f"{host['release']}/{archive_name}", timeout=120 @@ -246,24 +251,35 @@ def morphir_cli() -> tuple[Path, str]: if sha256(data) != expected: raise BundleError(f"{archive_name} digest differs from the pin") directory.mkdir(parents=True, exist_ok=True) - with tarfile.open(fileobj=io.BytesIO(data)) as archive: - member = next( - m for m in archive.getmembers() if Path(m.name).name == "morphir" - ) - extracted = archive.extractfile(member) - if extracted is None: - raise BundleError(f"{archive_name} has no morphir executable") - binary.write_bytes(extracted.read()) + if is_windows: + with zipfile.ZipFile(io.BytesIO(data)) as archive: + zip_member = next( + m for m in archive.namelist() if Path(m).name == "morphir.exe" + ) + binary.write_bytes(archive.read(zip_member)) + else: + with tarfile.open(fileobj=io.BytesIO(data)) as archive: + member = next( + m for m in archive.getmembers() if Path(m.name).name == "morphir" + ) + extracted = archive.extractfile(member) + if extracted is None: + raise BundleError(f"{archive_name} has no morphir executable") + binary.write_bytes(extracted.read()) binary.chmod(0o755) return binary, expected def host_check(bundle: Path, *, version: str | None = None) -> dict[str, object]: """Install the bundle with the pinned CLI and round trip offline.""" + return host_round_trips(bundle, receipt(bundle, version=version)) + + +def host_round_trips(bundle: Path, described: dict[str, Any]) -> dict[str, object]: + """Qualify an admitted runtime bundle through installed, offline CLI routes.""" from morphir_tools.python import native_compiler bundle = bundle.resolve() - described = receipt(bundle, version=version) cli, cli_digest = morphir_cli() with tempfile.TemporaryDirectory(prefix="python-bundle-host-") as scratch: work = Path(scratch) diff --git a/scripts/process_extension.py b/scripts/process_extension.py new file mode 100644 index 00000000..26df824a --- /dev/null +++ b/scripts/process_extension.py @@ -0,0 +1,178 @@ +"""Stage process extensions with the pinned Morphir CLI's canonical bundler. + +Receipts live outside the canonical three-file bundle. They bind its source, +platform, launch convention, executable mode and exact bytes. Publication and +installed-host qualification are separate steps. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import stat +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle + +PLATFORMS = ( + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", +) + + +def artifact_name(version: str, platform: str) -> str: + """Return the raw executable name for an explicitly supported platform.""" + if platform not in PLATFORMS: + raise bundle.BundleError(f"unsupported process platform {platform}") + suffix = ".exe" if platform.endswith("windows-msvc") else "" + return f"morphir-python-extension-{version}-{platform}{suffix}" + + +def receipt(directory: Path, commit: str, platform: str) -> dict[str, Any]: + """Refuse a candidate whose descriptor, bytes, identity or mode disagree.""" + if re.fullmatch(r"[0-9a-f]{40}", commit) is None: + raise bundle.BundleError("source commit must be a full lowercase Git SHA") + version = bundle.crate_version() + name = artifact_name(version, platform) + expected = {name, f"{name}.sha256", "release.json"} + if {p.name for p in directory.iterdir()} != expected: + raise bundle.BundleError("process bundle has missing or extra files") + if any(p.is_symlink() or not p.is_file() for p in directory.iterdir()): + raise bundle.BundleError("process bundle files must be regular files") + try: + descriptor = json.loads((directory / "release.json").read_text()) + (artifact,) = descriptor["artifacts"] + claims = artifact["claims"] + digest = bundle.sha256((directory / name).read_bytes()) + if not ( + descriptor["schemaVersion"] == "2.0.0-draft.2" + and descriptor["extensionId"] == "morphir-python" + and descriptor["shortId"] == "python" + and descriptor["version"] == version + and descriptor["gitCommit"] == commit + and artifact["runtime"] == "process" + and artifact["platform"] == platform + and artifact["filename"] == name + and artifact["sha256"] == digest + and claims["extension"]["id"] == descriptor["extensionId"] + and claims["extension"]["version"] == version + and claims["protocolVersions"] + == bundle.declaration()["extension"]["mep_versions"] + ): + raise bundle.BundleError("process descriptor disagrees with candidate") + if (directory / f"{name}.sha256").read_text() != f"{digest} {name}\n": + raise bundle.BundleError("process checksum disagrees with executable") + except (KeyError, ValueError, TypeError) as error: + raise bundle.BundleError(f"invalid process descriptor: {error}") from error + mode = stat.S_IMODE((directory / name).stat().st_mode) + if os.name != "nt" and mode & stat.S_IXUSR == 0: + raise bundle.BundleError("process artifact lacks owner executable mode") + return { + "schemaVersion": 1, + "sourceCommit": commit, + "runtime": "process", + "platform": platform, + "version": version, + "arguments": [], + "explicitArguments": ["--stdio"], + "executable": True, + "mode": oct(mode), + "files": { + name: bundle.sha256((directory / name).read_bytes()) + for name in sorted(expected) + }, + "claims": claims, + } + + +def package(executable: Path, output: Path, commit: str) -> dict[str, Any]: + """Run the pinned native bundler on a uniquely named copy of exact bytes.""" + platform = bundle._target() + name = artifact_name(bundle.crate_version(), platform) + if re.fullmatch(r"[0-9a-f]{40}", commit) is None: + raise bundle.BundleError("source commit must be a full lowercase Git SHA") + if output.exists() or output.is_symlink(): + raise bundle.BundleError(f"bundle directory {output} already exists") + if executable.is_symlink() or not executable.is_file(): + raise bundle.BundleError("process executable must be a regular file") + cli, archive_digest = bundle.morphir_cli() + with tempfile.TemporaryDirectory(prefix="python-process-bundle-") as temporary: + copied = Path(temporary) / name + shutil.copyfile(executable, copied) + copied.chmod(0o700) + subprocess.run( + [ + str(cli.resolve()), + "--no-banner", + "extension", + "bundle", + "--executable", + str(copied), + "--extension-id", + "morphir-python", + "--short-id", + "python", + "--version", + bundle.crate_version(), + "--platform", + platform, + "--output", + str(output.resolve()), + ], + check=True, + ) + # gitCommit is a canonical optional descriptor member. All runtime fields + # and claims come from the upstream writer and its live describe probe. + descriptor_path = output / "release.json" + descriptor = json.loads(descriptor_path.read_text()) + descriptor["gitCommit"] = commit + descriptor_path.write_text(json.dumps(descriptor, indent=2) + "\n") + result = receipt(output, commit, platform) + result["bundler"] = { + "version": bundle.declaration()["host"]["version"], + "archiveSha256": archive_digest, + "binarySha256": bundle.sha256(cli.read_bytes()), + } + result["cargoLockSha256"] = bundle.sha256( + (bundle.ROOT / "rust/Cargo.lock").read_bytes() + ) + return result + + +def main(argv: list[str] | None = None) -> int: + """Write a source-bound process receipt outside the canonical bundle.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--executable", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--commit", required=True) + parser.add_argument("--report", type=Path, required=True) + parser.add_argument( + "--host-check", + action="store_true", + help="install offline and compare v3/v4 with the matching Python API", + ) + args = parser.parse_args(argv) + if args.report.resolve().is_relative_to(args.output.resolve()): + parser.error("the report must be outside the canonical bundle") + try: + result = package(args.executable, args.output, args.commit) + if args.host_check: + result["installedHost"] = bundle.host_round_trips(args.output, result) + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(result, indent=2) + "\n") + except (bundle.BundleError, OSError, subprocess.CalledProcessError) as error: + parser.exit(1, f"process bundle refused: {error}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_extension_bundle.py b/tests/unit/test_extension_bundle.py index c4d81cee..e692a2d3 100644 --- a/tests/unit/test_extension_bundle.py +++ b/tests/unit/test_extension_bundle.py @@ -2,7 +2,9 @@ from __future__ import annotations +import io import json +import zipfile from typing import TYPE_CHECKING import pytest @@ -51,3 +53,46 @@ def test_bundle_requires_multi_document_claim( else: with pytest.raises(bundle.BundleError, match="claims lack"): bundle.receipt(tmp_path) + + +def test_windows_cli_archive_is_verified_and_only_the_binary_is_extracted( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(bundle.platform, "system", lambda: "Windows") + monkeypatch.setattr(bundle.platform, "machine", lambda: "AMD64") + monkeypatch.setattr(bundle, "CACHE", tmp_path) + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + archive.writestr("release/morphir.exe", b"verified CLI") + archive.writestr("../../unwanted", b"do not extract") + archive_bytes = stream.getvalue() + digest = bundle.sha256(archive_bytes) + monkeypatch.setattr( + bundle, + "declaration", + lambda: { + "host": { + "version": "test", + "release": "https://example.invalid/releases", + "sha256": {"x86_64-pc-windows-msvc": digest}, + } + }, + ) + urls: list[str] = [] + + def download(url: str, *, timeout: int) -> io.BytesIO: + assert timeout > 0 + urls.append(url) + return io.BytesIO(archive_bytes) + + monkeypatch.setattr(bundle.urllib.request, "urlopen", download) + executable, actual_digest = bundle.morphir_cli() + assert actual_digest == digest + assert executable.name == "morphir.exe" + assert executable.read_bytes() == b"verified CLI" + assert urls == [ + "https://example.invalid/releases/morphir-test-x86_64-pc-windows-msvc.zip" + ] + assert sorted( + p.relative_to(tmp_path).as_posix() for p in tmp_path.rglob("*") if p.is_file() + ) == ["morphir-test-x86_64-pc-windows-msvc/morphir.exe"] diff --git a/tests/unit/test_process_extension.py b/tests/unit/test_process_extension.py new file mode 100644 index 00000000..39cdd087 --- /dev/null +++ b/tests/unit/test_process_extension.py @@ -0,0 +1,114 @@ +"""Process release receipts bind canonical descriptors and exact executable bytes.""" + +from __future__ import annotations + +import json +import os +from typing import TYPE_CHECKING + +import pytest + +from scripts import extension_bundle as wasm +from scripts import process_extension as process + +if TYPE_CHECKING: + from pathlib import Path + + +def candidate(directory: Path) -> str: + version = wasm.crate_version() + name = process.artifact_name(version, "aarch64-apple-darwin") + binary = directory / name + binary.write_bytes(b"executable fixture") + binary.chmod(0o700) + digest = wasm.sha256(binary.read_bytes()) + (directory / f"{name}.sha256").write_text(f"{digest} {name}\n") + (directory / "release.json").write_text( + json.dumps( + { + "schemaVersion": "2.0.0-draft.2", + "extensionId": "morphir-python", + "shortId": "python", + "version": version, + "gitCommit": "a" * 40, + "platformDifferences": "none", + "artifacts": [ + { + "runtime": "process", + "platform": "aarch64-apple-darwin", + "filename": name, + "sha256": digest, + "claimCheck": "unchecked", + "claims": { + "protocolVersions": ["0.1"], + "extension": {"id": "morphir-python", "version": version}, + "capabilities": {}, + }, + } + ], + } + ) + ) + return name + + +def test_process_receipt_binds_source_bytes_launch_and_mode(tmp_path: Path) -> None: + name = candidate(tmp_path) + receipt = process.receipt(tmp_path, "a" * 40, "aarch64-apple-darwin") + assert receipt["sourceCommit"] == "a" * 40 + assert receipt["arguments"] == [] + assert receipt["explicitArguments"] == ["--stdio"] + assert receipt["runtime"] == "process" + assert receipt["files"][name] == wasm.sha256((tmp_path / name).read_bytes()) + assert receipt["executable"] is True + + +@pytest.mark.parametrize( + "change", + [ + "bytes", + "digest", + "source", + "platform", + "runtime", + "claims", + "missing", + "extra", + "mode", + ], +) +def test_process_receipt_refuses_mismatched_candidates( + tmp_path: Path, change: str +) -> None: + name = candidate(tmp_path) + descriptor = json.loads((tmp_path / "release.json").read_text()) + if change == "bytes": + (tmp_path / name).write_bytes(b"changed") + elif change == "digest": + (tmp_path / f"{name}.sha256").write_text("wrong") + elif change == "source": + descriptor["gitCommit"] = "b" * 40 + elif change == "platform": + descriptor["artifacts"][0]["platform"] = "x86_64-pc-windows-msvc" + elif change == "runtime": + descriptor["artifacts"][0]["runtime"] = "wasm" + elif change == "claims": + descriptor["artifacts"][0]["claims"]["extension"]["version"] = "0.0.0" + elif change == "missing": + (tmp_path / name).unlink() + elif change == "extra": + (tmp_path / "unqualified").touch() + elif change == "mode": + if os.name == "nt": + pytest.skip("POSIX executable mode") + (tmp_path / name).chmod(0o600) + (tmp_path / "release.json").write_text(json.dumps(descriptor)) + with pytest.raises(wasm.BundleError): + process.receipt(tmp_path, "a" * 40, "aarch64-apple-darwin") + + +def test_process_platforms_are_explicit_and_windows_uses_exe() -> None: + assert len(process.PLATFORMS) == 5 + assert process.artifact_name("0.4.2", "x86_64-pc-windows-msvc").endswith(".exe") + with pytest.raises(wasm.BundleError, match="unsupported process platform"): + process.artifact_name("0.4.2", "riscv64-unknown-linux-gnu") From 6d8f2915d71a6126e48b588186eb62a668c687de Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:22:40 -0500 Subject: [PATCH 07/26] test(extension): verify isolated process sessions --- .../morphir-python-extension/tests/session.rs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/rust/crates/morphir-python-extension/tests/session.rs b/rust/crates/morphir-python-extension/tests/session.rs index 10e59615..e2cf3583 100644 --- a/rust/crates/morphir-python-extension/tests/session.rs +++ b/rust/crates/morphir-python-extension/tests/session.rs @@ -112,3 +112,96 @@ fn standalone(arguments: &[&str]) { assert_eq!(replies.len(), 1); assert_eq!(replies[0]["result"]["extension"]["id"], "morphir-python"); } + +#[test] +fn invalid_request_ids_do_not_become_notifications_or_poison_the_session() { + let mut messages = Vec::new(); + for id in [ + Value::Null, + json!(true), + json!([]), + json!({}), + json!(-1), + json!(1.5), + ] { + messages.push(json!({"jsonrpc":"2.0","id":id,"method":"morphir.ping"})); + } + messages.push(json!({"jsonrpc":"2.0","id":77,"method":"morphir.ping","params":{}})); + let mut output = Vec::new(); + serve(&mut Cursor::new(wire(&messages)), &mut output).unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 7); + for reply in &replies[..6] { + assert_eq!(reply["id"], Value::Null); + assert_eq!(reply["error"]["code"], -32600); + } + assert_eq!(replies[6]["id"], 77); + assert_eq!(replies[6]["result"]["ok"], true); +} + +#[test] +fn independent_stream_sessions_cannot_share_initialization_state() { + let mut first = Vec::new(); + serve(&mut Cursor::new(wire(&[initialize()])), &mut first).unwrap(); + assert_eq!(responses(first)[0]["result"]["protocolVersion"], "0.1"); + let mut second = Vec::new(); + serve( + &mut Cursor::new(wire(&[ + json!({"jsonrpc":"2.0","id":2,"method":"morphir.extension.info","params":{}}), + ])), + &mut second, + ) + .unwrap(); + assert_eq!(responses(second)[0]["error"]["code"], -32014); +} + +#[test] +fn truncated_input_exits_with_diagnostics_only_on_stderr() { + let mut child = Command::new(env!("CARGO_BIN_EXE_morphir-python-extension")) + .env_clear() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .unwrap() + .write_all(b"Content-Length: 10\r\n\r\n{}") + .unwrap(); + let result = child.wait_with_output().unwrap(); + assert_eq!(result.status.code(), Some(1)); + assert!(result.stdout.is_empty()); + assert!( + String::from_utf8(result.stderr) + .unwrap() + .contains("morphir-python-extension:") + ); +} + +#[test] +fn description_is_fixed_across_initialization_and_reinitialization_refusal() { + let describe = |id| json!({"jsonrpc":"2.0","id":id,"method":"morphir.extension.describe","params":{"protocolVersions":["0.1"]}}); + let mut second_init = initialize(); + second_init["id"] = json!(3); + second_init["params"]["protocolVersions"] = json!(["0.2.0-draft.99"]); + let mut output = Vec::new(); + serve( + &mut Cursor::new(wire(&[ + describe(0), + initialize(), + describe(2), + second_init, + describe(4), + ])), + &mut output, + ) + .unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 5); + assert_eq!(replies[0]["result"]["protocolVersions"], json!(["0.1"])); + assert_eq!(replies[0]["result"], replies[2]["result"]); + assert_eq!(replies[2]["result"], replies[4]["result"]); + assert_eq!(replies[3]["error"]["code"], -32600); +} From 6945b7fcf34d194073bcb5f6a7d3f76e58c75696 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:31:54 -0500 Subject: [PATCH 08/26] ci(extension): qualify portable process artifacts --- .github/workflows/ci.yml | 38 ++++++++++++ .github/workflows/release.yml | 61 ++++++++++++++++++- .../add-process-mep-extension/design.html | 20 +++++- 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fcadc99..6954981f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,6 +110,11 @@ jobs: rust-toolchain: '1.98.1' manylinux: '2_28' target: x86_64-unknown-linux-gnu + # Build the standalone guest in the same glibc 2.28 container. Its + # shared compiler objects are then reused by the Python wheel build. + before-script-linux: >- + cargo build --manifest-path ../../rust/Cargo.toml --locked --release + -p morphir-python-extension --target x86_64-unknown-linux-gnu args: --release --locked --sdist --out ../../dist/native - name: Audit the Linux baseline and bundled libraries @@ -130,6 +135,13 @@ jobs: path: dist/native/ if-no-files-found: error + - name: Upload the process executable from the portable build + uses: actions/upload-artifact@v6 + with: + name: process-linux-x86_64 + path: rust/target/x86_64-unknown-linux-gnu/release/morphir-python-extension + if-no-files-found: error + test: name: Test needs: [changes, native-wheel] @@ -218,9 +230,35 @@ jobs: run: | MORPHIR_NATIVE_WHEEL="$(ls dist/native/*.whl)" mise run install + - name: Download the portable process executable + uses: actions/download-artifact@v7 + with: + name: process-linux-x86_64 + path: dist/process/ + - name: Package and qualify the bundle offline run: mise run extension:bundle + - name: Package and qualify the process guest offline + run: | + mkdir -p .morphir/build/extensions .dev/qualification + uv run --no-sync python -m scripts.process_extension \ + --executable dist/process/morphir-python-extension \ + --output .morphir/build/extensions/python-process \ + --commit "$GITHUB_SHA" \ + --report .dev/qualification/process-x86_64-unknown-linux-gnu.json \ + --host-check + + - name: Upload the qualified process bundle and receipt + uses: actions/upload-artifact@v6 + with: + name: morphir-python-process-x86_64-unknown-linux-gnu + path: | + .morphir/build/extensions/python-process/ + .dev/qualification/process-x86_64-unknown-linux-gnu.json + include-hidden-files: true + if-no-files-found: error + - name: Upload the extension bundle uses: actions/upload-artifact@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4f72300..d0bf7959 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,8 @@ jobs: if: >- github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/native-v') || - startsWith(github.ref, 'refs/tags/python-v') + startsWith(github.ref, 'refs/tags/python-v') || + startsWith(github.ref, 'refs/tags/extension/python/v') strategy: fail-fast: false matrix: @@ -89,6 +90,11 @@ jobs: rust-toolchain: '1.98.1' target: ${{ matrix.target }} manylinux: ${{ matrix.manylinux || 'off' }} + # Process guests share the wheel's Linux baseline and compiled core. + before-script-linux: >- + ${{ (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) && + format('cargo build --manifest-path ../../rust/Cargo.toml --locked --release -p morphir-python-extension --target {0}', matrix.target) || '' }} args: --release --locked --out ../../dist/native - name: Set up Python 3.14 @@ -159,6 +165,57 @@ jobs: path: dist/mise-install.json if-no-files-found: error + # Linux x86_64 already produced and qualified this exact guest in qualify. + # The other producers use their matching native wheel for API comparison. + - name: Build the process executable on macOS and Windows + if: >- + matrix.manylinux == '' && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + shell: bash + env: + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.target == 'aarch64-apple-darwin' && '11.0' || '10.12' }} + run: >- + cargo +1.98.1 build --manifest-path rust/Cargo.toml --locked --release + -p morphir-python-extension --target '${{ matrix.target }}' + + - name: Qualify the process bundle against the matching Python API + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + shell: bash + run: | + uv venv .dev/process-check --python 3.14 + qualifier=.dev/process-check/bin/python + executable=rust/target/${{ matrix.target }}/release/morphir-python-extension + if [[ "${{ runner.os }}" == Windows ]]; then + qualifier=.dev/process-check/Scripts/python.exe + executable="$executable.exe" + fi + uv pip install --python "$qualifier" dist/pure/*.whl dist/native/*.whl + mkdir -p .morphir/build/extensions .dev/qualification + "$qualifier" -m scripts.process_extension \ + --executable "$executable" \ + --output .morphir/build/extensions/python-process \ + --commit "$GITHUB_SHA" \ + --report .dev/qualification/process-${{ matrix.target }}.json \ + --host-check + + - name: Upload the qualified process bundle and receipt + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + uses: actions/upload-artifact@v6 + with: + name: morphir-python-process-${{ matrix.target }} + path: | + .morphir/build/extensions/python-process/ + .dev/qualification/process-${{ matrix.target }}.json + include-hidden-files: true + if-no-files-found: error + - name: Upload the qualified wheel uses: actions/upload-artifact@v6 with: @@ -239,7 +296,7 @@ jobs: extension-candidate: name: Verify the extension bundle candidate - needs: qualify + needs: [qualify, native-wheels] if: startsWith(github.ref, 'refs/tags/extension/python/v') runs-on: ubuntu-latest steps: diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 4bed3902..d9d59200 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -23,12 +23,30 @@

add-process-mep-extension

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Applying; published SDK prerequisite in progress

- +

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The published SDK 0.3.0 exposes NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type still uses a numeric u64 ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The Python implementation does not opt into model transport and its manifest declares MEP 0.1 only. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. Crate-tag publication approval is pending; the SDK is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. Approval is pending for a separate upstream Python JSON profile; no existing MoonBit check will be relaxed and no unit-test result substitutes for executable qualification.

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. Protocol discovery remains legacy-only until the published SDK prerequisite and candidate-bound conformance pass. All five task groups remain incomplete.

+

Build and release assessment

+

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

+ + + + +
Risk and deadlineExecution and ownerEvidence and failure response
Incorrect shared compiler or process bytes, before mergeExisting Linux PR bundle category, repository maintainersCanonical bundle probe, isolated installation, deleted source repository, offline v3/v4 comparison with the exact native wheel; failure blocks the selected aggregate.
Platform-specific executable or wheel failure, before promotionExisting five-platform release matrix, explicit manual qualification and extension tag eventsNative target and compiler/toolchain identity, exact digest/claims/source receipts and installed host comparisons. Missing, failed or skipped selected producers block extension publication through its needs graph.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteThe Python MCK profile and published SDK 0.4.0 remain pending. Platform packaging does not establish draft qualification. Promotion assurance must consume and recheck the candidate-bound executable MCK reports before the change is complete.
+

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Linux builds reuse shared Cargo output between process and Python artifacts. macOS and Windows use the same native target and deployment configuration as their wheel producer. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

+ + + + + + + +
CriterionWeightCurrent assessment
Risk and coverage fit253: focused local refusal tests and installed macOS legacy evidence pass; matrix results pending.
Feedback and placement203: one Linux PR path; broader work stays manual/release. Added CI latency not yet measured.
Release evidence and trust20U: current-draft MCK prerequisite and final promotion assembly pending.
Execution efficiency15U: shared producer/output design inspected, actual CI reuse and elapsed time pending measurement.
Ownership and structure104: existing bundle and native-wheel categories, explicit needs, repository maintainers own failures.
Operability and recovery102: candidate refusal tests pass; platform rerun/transfer and final promotion refusals pending.
+

Total withheld while 35 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

+

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk 0.3.0: native.rs, traits.rs and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

From 5962a5837e68ce83b9acda48d324dadd981ba236 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:50:52 -0500 Subject: [PATCH 09/26] build(extension): verify complete process candidates --- .gitattributes | 4 + .github/workflows/ci.yml | 4 + .github/workflows/release.yml | 52 +++++ .../add-process-mep-extension/design.html | 4 +- scripts/extension_bundle.py | 70 ++++--- scripts/process_release.py | 192 ++++++++++++++++++ tests/unit/test_extension_bundle.py | 18 +- tests/unit/test_process_release.py | 183 +++++++++++++++++ 8 files changed, 501 insertions(+), 26 deletions(-) create mode 100644 scripts/process_release.py create mode 100644 tests/unit/test_process_release.py diff --git a/.gitattributes b/.gitattributes index 807d5983..8d7c2c29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,7 @@ # Use bd merge for beads JSONL files .beads/issues.jsonl merge=beads + +# Release receipts bind these inputs as bytes on every runner OS. +rust/Cargo.lock text eol=lf +scripts/*.py text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6954981f..0fbabb31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,10 @@ jobs: - name: Build the manylinux_2_28 wheel and source archive uses: PyO3/maturin-action@v1 + env: + # Maturin builds the wheel from an isolated sdist. Share registry + # dependency objects with the process hook across that source path. + CARGO_TARGET_DIR: ${{ github.workspace }}/rust/target with: working-directory: packages/morphir-python-native maturin-version: '1.15.0' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0bf7959..7326a49f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,6 +84,7 @@ jobs: uses: PyO3/maturin-action@v1 env: MACOSX_DEPLOYMENT_TARGET: ${{ matrix.target == 'aarch64-apple-darwin' && '11.0' || '10.12' }} + CARGO_TARGET_DIR: ${{ github.workspace }}/rust/target with: working-directory: packages/morphir-python-native maturin-version: '1.15.0' @@ -265,6 +266,57 @@ jobs: path: dist/candidates/ if-no-files-found: error + # This stages a process-only candidate. Publication additionally requires + # independent MEP guest qualification; no process release is published here. + process-candidates: + name: Verify the process release candidate set + needs: [qualify, native-wheels] + if: >- + github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v') + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Download each qualified process platform + uses: actions/download-artifact@v7 + with: + pattern: morphir-python-process-* + path: dist/process-inputs/ + + - name: Download the qualified WASM reference + uses: actions/download-artifact@v7 + with: + name: morphir-python-extension-bundle + path: dist/wasm-check/ + + - name: Refuse mixed sources, claims, bytes or installed results + run: | + python -m scripts.process_release \ + --inputs dist/process-inputs \ + --output dist/process-release \ + --commit "$GITHUB_SHA" \ + --wasm-bundle dist/wasm-check/.morphir/build/extensions/python \ + --wasm-report dist/wasm-check/.dev/qualification/bundle-host.json \ + --report dist/process-assembly.json + + - name: Upload the process-only candidate and its evidence + uses: actions/upload-artifact@v6 + with: + name: process-release-candidate + path: | + dist/process-release/ + dist/process-assembly.json + dist/process-inputs/*/.dev/qualification/*.json + include-hidden-files: true + if-no-files-found: error + publish-native: name: Publish morphir-python-native to PyPI if: >- diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index d9d59200..830776da 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -30,13 +30,13 @@

add-process-mep-extension

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. Approval is pending for a separate upstream Python JSON profile; no existing MoonBit check will be relaxed and no unit-test result substitutes for executable qualification.

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. Protocol discovery remains legacy-only until the published SDK prerequisite and candidate-bound conformance pass. All five task groups remain incomplete.

Build and release assessment

-

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

+

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. A dependent read-only assembly job requires all five byte-bound receipts and their agreement with the WASM reference; it uploads a process-only candidate and does not publish it. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

Risk and deadlineExecution and ownerEvidence and failure response
Incorrect shared compiler or process bytes, before mergeExisting Linux PR bundle category, repository maintainersCanonical bundle probe, isolated installation, deleted source repository, offline v3/v4 comparison with the exact native wheel; failure blocks the selected aggregate.
Platform-specific executable or wheel failure, before promotionExisting five-platform release matrix, explicit manual qualification and extension tag eventsNative target and compiler/toolchain identity, exact digest/claims/source receipts and installed host comparisons. Missing, failed or skipped selected producers block extension publication through its needs graph.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteThe Python MCK profile and published SDK 0.4.0 remain pending. Platform packaging does not establish draft qualification. Promotion assurance must consume and recheck the candidate-bound executable MCK reports before the change is complete.
-

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Linux builds reuse shared Cargo output between process and Python artifacts. macOS and Windows use the same native target and deployment configuration as their wheel producer. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

+

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Maturin builds the CI wheel from an isolated source archive, so a hook alone did not share registry dependency objects. Run 40672914229 showed two compilations: 3m07s for the process guest and 2m57s for the archive wheel; the whole producer took 7m07s, compared with 2m57s for earlier job 122760321213. An explicit Cargo target directory now allows compatible registry objects to be reused across those source paths while retaining the independent archive build. Its measured effect is pending a replacement run. Local path-dependent compiler objects may still rebuild because their source identity differs. macOS and Windows use the same native target and deployment configuration as their wheel producer. Cached CLI bytes are checked against the digest-verified archive before execution. Host receipts retain normalized IR and generated artifact digests, actual CLI and qualification-script identity. Byte-bound lockfiles and script sources use LF on every checkout platform. Artifact ZIP transfer restores only the declared executable mode; payload digests and the original producer mode remain checked. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

diff --git a/scripts/extension_bundle.py b/scripts/extension_bundle.py index bc41bd99..be05bae3 100644 --- a/scripts/extension_bundle.py +++ b/scripts/extension_bundle.py @@ -243,30 +243,41 @@ def morphir_cli() -> tuple[Path, str]: binary = directory / ("morphir.exe" if is_windows else "morphir") extension = "zip" if is_windows else "tgz" archive_name = f"morphir-{host['version']}-{target}.{extension}" - if not binary.is_file(): + archive_path = directory / archive_name + if archive_path.is_symlink() or binary.is_symlink(): + raise BundleError("cached Morphir CLI and archive must be regular files") + if archive_path.is_file(): + data = archive_path.read_bytes() + else: with urllib.request.urlopen( f"{host['release']}/{archive_name}", timeout=120 - ) as r: - data = r.read() - if sha256(data) != expected: - raise BundleError(f"{archive_name} digest differs from the pin") - directory.mkdir(parents=True, exist_ok=True) - if is_windows: - with zipfile.ZipFile(io.BytesIO(data)) as archive: - zip_member = next( - m for m in archive.namelist() if Path(m).name == "morphir.exe" - ) - binary.write_bytes(archive.read(zip_member)) - else: - with tarfile.open(fileobj=io.BytesIO(data)) as archive: - member = next( - m for m in archive.getmembers() if Path(m.name).name == "morphir" - ) - extracted = archive.extractfile(member) - if extracted is None: - raise BundleError(f"{archive_name} has no morphir executable") - binary.write_bytes(extracted.read()) - binary.chmod(0o755) + ) as response: + data = response.read() + if sha256(data) != expected: + raise BundleError(f"{archive_name} digest differs from the pin") + if is_windows: + with zipfile.ZipFile(io.BytesIO(data)) as archive: + zip_member = next( + m for m in archive.namelist() if Path(m).name == "morphir.exe" + ) + executable_bytes = archive.read(zip_member) + else: + with tarfile.open(fileobj=io.BytesIO(data)) as archive: + member = next( + m for m in archive.getmembers() if Path(m.name).name == "morphir" + ) + extracted = archive.extractfile(member) + if extracted is None: + raise BundleError(f"{archive_name} has no morphir executable") + executable_bytes = extracted.read() + if binary.exists() and binary.read_bytes() != executable_bytes: + raise BundleError("cached Morphir CLI differs from the verified archive") + directory.mkdir(parents=True, exist_ok=True) + if not archive_path.exists(): + archive_path.write_bytes(data) + if not binary.exists(): + binary.write_bytes(executable_bytes) + binary.chmod(0o755) return binary, expected @@ -306,6 +317,7 @@ def morphir(*args: str, offline: bool = False) -> str: capture_output=True, text=True, check=False, + timeout=60, ) if result.returncode != 0: raise BundleError(f"morphir {' '.join(args)}: {result.stderr}") @@ -350,13 +362,25 @@ def morphir(*args: str, offline: bool = False) -> str: ) results[f"v{tag}"] = { "modules": files, + "ir_sha256": sha256( + json.dumps(first, sort_keys=True, separators=(",", ":")).encode() + ), + "generated_sha256": { + path: sha256((project / generated / path).read_bytes()) + for path in files + }, "agrees_with_python_api": agrees_with_api( first, tag, compiler_version=str(described["version"]) ), } return { **described, - "host": {"morphir": declaration()["host"]["version"], "sha256": cli_digest}, + "host": { + "morphir": declaration()["host"]["version"], + "sha256": cli_digest, + "binary_sha256": sha256(cli.read_bytes()), + }, + "qualification_script_sha256": sha256(Path(__file__).read_bytes()), "python_api": { "native_version": native_compiler().version, "compiler_version": native_compiler().compiler_version, diff --git a/scripts/process_release.py b/scripts/process_release.py new file mode 100644 index 00000000..af4f65d9 --- /dev/null +++ b/scripts/process_release.py @@ -0,0 +1,192 @@ +"""Assemble verified native platforms into a canonical process-only bundle. + +This is artifact assembly, not MEP conformance or publication approval. Every +platform must have an exact-byte installed-host receipt matching the WASM +candidate's claims and supported legacy results. The release promotion gate +must additionally require independent executable MEP qualification. +""" + +from __future__ import annotations + +import argparse +import json +import shutil +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle +from scripts import process_extension as process +from scripts.qualify_native import tools_pin +from scripts.release_candidates import CandidateError, bundle_candidates + + +def _read(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text()) + if not isinstance(value, dict): + raise bundle.BundleError(f"expected an object in {path}") + return value + + +def assemble( + inputs: Path, + output: Path, + commit: str, + wasm_claims: dict[str, Any], + reference_results: dict[str, Any], +) -> dict[str, Any]: + """Refuse mixed candidates and combine all canonical process entries.""" + if output.exists() or output.is_symlink(): + raise FileExistsError(output) + expected = {f"morphir-python-process-{target}" for target in process.PLATFORMS} + if {path.name for path in inputs.iterdir()} != expected: + raise bundle.BundleError("process set must contain exactly five platforms") + if set(reference_results) != {"v3", "v4"} or not all( + result.get("agrees_with_python_api") is True and result.get("modules") + for result in reference_results.values() + ): + raise bundle.BundleError("WASM reference lacks successful v3/v4 results") + version = bundle.crate_version() + lock_digest = bundle.sha256((bundle.ROOT / "rust/Cargo.lock").read_bytes()) + source_files: list[Path] = [] + artifacts: list[dict[str, Any]] = [] + reports: dict[str, str] = {} + for target in process.PLATFORMS: + root = inputs / f"morphir-python-process-{target}" + directory = root / ".morphir/build/extensions/python-process" + report_path = root / f".dev/qualification/process-{target}.json" + report = _read(report_path) + name = process.artifact_name(version, target) + executable = directory / name + # GitHub artifact ZIP transfer strips execute bits. Restore only the + # declared raw executable; its bytes and original mode remain checked. + if executable.is_symlink() or not executable.is_file(): + raise bundle.BundleError("process artifact is not a regular file") + executable.chmod(0o700) + checked = process.receipt(directory, commit, target) + try: + for field in ( + "sourceCommit", + "runtime", + "platform", + "version", + "files", + "arguments", + "explicitArguments", + "executable", + "claims", + ): + if report[field] != checked[field]: + raise bundle.BundleError(f"{target}: mismatched {field}") + host = report["installedHost"] + pin = bundle.declaration()["host"] + if not ( + report["claims"] == wasm_claims + and report["cargoLockSha256"] == lock_digest + and report["bundler"]["version"] == pin["version"] + and report["bundler"]["archiveSha256"] == pin["sha256"][target] + and host["sourceCommit"] == commit + and host["version"] == version + and host["files"] == checked["files"] + and host["host"]["morphir"] == pin["version"] + and host["host"]["sha256"] == pin["sha256"][target] + and host["host"]["binary_sha256"] == report["bundler"]["binarySha256"] + and host["qualification_script_sha256"] + == bundle.sha256( + (bundle.ROOT / "scripts/extension_bundle.py").read_bytes() + ) + and host["python_api"]["native_version"] == tools_pin() + and host["python_api"]["compiler_version"] == version + and host["offline_round_trips"] == reference_results + and (target.endswith("windows-msvc") or report["mode"] == "0o700") + ): + raise bundle.BundleError(f"{target}: qualification evidence differs") + descriptor = _read(directory / "release.json") + artifacts.append(descriptor["artifacts"][0]) + except (KeyError, TypeError, IndexError) as error: + raise bundle.BundleError( + f"{target}: incomplete process evidence" + ) from error + source_files.extend([executable, directory / f"{name}.sha256"]) + reports[target] = bundle.sha256(report_path.read_bytes()) + descriptor = { + "schemaVersion": "2.0.0-draft.2", + "extensionId": "morphir-python", + "shortId": "python", + "version": version, + "gitCommit": commit, + "platformDifferences": "none", + "artifacts": artifacts, + } + # mkdir is exclusive; a concurrent or existing destination is never replaced. + output.mkdir() + try: + for path in source_files: + shutil.copyfile(path, output / path.name) + if not path.name.endswith(".sha256"): + (output / path.name).chmod(0o700) + (output / "release.json").write_text(json.dumps(descriptor, indent=2) + "\n") + except BaseException: + shutil.rmtree(output) + raise + return { + "kind": "process-release-assembly", + "sourceCommit": commit, + "version": version, + "platforms": list(process.PLATFORMS), + "files": { + path.name: bundle.sha256(path.read_bytes()) + for path in sorted(output.iterdir()) + }, + "inputReports": reports, + } + + +def main(argv: list[str] | None = None) -> int: + """Assemble a process candidate against source-bound WASM reference bytes.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--inputs", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--commit", required=True) + parser.add_argument("--wasm-bundle", type=Path, required=True) + parser.add_argument("--wasm-report", type=Path, required=True) + parser.add_argument("--report", type=Path, required=True) + args = parser.parse_args(argv) + if args.report.resolve().is_relative_to(args.output.resolve()): + parser.error("assembly report must remain outside the canonical bundle") + try: + bundle_candidates(args.wasm_bundle, bundle.crate_version(), args.commit) + checked = bundle.receipt(args.wasm_bundle) + reference = _read(args.wasm_report) + if not ( + reference["git_commit"] == args.commit + and reference["bundle"] == checked["bundle"] + and reference["qualification_script_sha256"] + == bundle.sha256((bundle.ROOT / "scripts/extension_bundle.py").read_bytes()) + and reference["python_api"]["native_version"] == tools_pin() + and reference["python_api"]["compiler_version"] == bundle.crate_version() + ): + raise bundle.BundleError("WASM reference is not this qualified candidate") + claims = _read(args.wasm_bundle / "release.json")["artifacts"][0]["claims"] + result = assemble( + args.inputs, + args.output, + args.commit, + claims, + reference["offline_round_trips"], + ) + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(result, indent=2) + "\n") + except ( + bundle.BundleError, + CandidateError, + OSError, + KeyError, + ValueError, + TypeError, + ) as error: + parser.exit(1, f"process release assembly refused: {error}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_extension_bundle.py b/tests/unit/test_extension_bundle.py index e692a2d3..ede87089 100644 --- a/tests/unit/test_extension_bundle.py +++ b/tests/unit/test_extension_bundle.py @@ -95,4 +95,20 @@ def download(url: str, *, timeout: int) -> io.BytesIO: ] assert sorted( p.relative_to(tmp_path).as_posix() for p in tmp_path.rglob("*") if p.is_file() - ) == ["morphir-test-x86_64-pc-windows-msvc/morphir.exe"] + ) == [ + "morphir-test-x86_64-pc-windows-msvc/morphir-test-x86_64-pc-windows-msvc.zip", + "morphir-test-x86_64-pc-windows-msvc/morphir.exe", + ] + + executable.write_bytes(b"changed cached CLI") + with pytest.raises(bundle.BundleError, match="cached Morphir CLI"): + bundle.morphir_cli() + assert len(urls) == 1 + + executable.write_bytes(b"verified CLI") + (executable.parent / "morphir-test-x86_64-pc-windows-msvc.zip").write_bytes( + b"changed archive" + ) + with pytest.raises(bundle.BundleError, match="digest differs"): + bundle.morphir_cli() + assert len(urls) == 1 diff --git a/tests/unit/test_process_release.py b/tests/unit/test_process_release.py new file mode 100644 index 00000000..37097d2c --- /dev/null +++ b/tests/unit/test_process_release.py @@ -0,0 +1,183 @@ +"""A release must contain the same qualified candidate on every platform.""" + +from __future__ import annotations + +import copy +import json +from typing import TYPE_CHECKING, Any + +import pytest + +from scripts import extension_bundle as bundle +from scripts import process_extension as process +from scripts import process_release as release +from scripts.qualify_native import tools_pin + +if TYPE_CHECKING: + from pathlib import Path + +COMMIT = "a" * 40 + + +def candidates(root: Path) -> tuple[dict[str, Any], dict[str, Any]]: + version = bundle.crate_version() + claims = { + "protocolVersions": bundle.declaration()["extension"]["mep_versions"], + "extension": {"id": "morphir-python", "version": version}, + "capabilities": {}, + } + results = { + v: { + "modules": ["acme/models.py", "acme/rules.py"], + "agrees_with_python_api": True, + } + for v in ["v3", "v4"] + } + for target in process.PLATFORMS: + platform_root = root / f"morphir-python-process-{target}" + directory = platform_root / ".morphir/build/extensions/python-process" + directory.mkdir(parents=True) + name = process.artifact_name(version, target) + executable = directory / name + executable.write_bytes(target.encode()) + executable.chmod(0o700) + digest = bundle.sha256(executable.read_bytes()) + (directory / f"{name}.sha256").write_text(f"{digest} {name}\n") + descriptor = { + "schemaVersion": "2.0.0-draft.2", + "shortId": "python", + "extensionId": "morphir-python", + "version": version, + "gitCommit": COMMIT, + "platformDifferences": "none", + "artifacts": [ + { + "runtime": "process", + "platform": target, + "filename": name, + "sha256": digest, + "claims": claims, + "claimCheck": "unchecked", + } + ], + } + (directory / "release.json").write_text(json.dumps(descriptor)) + receipt = process.receipt(directory, COMMIT, target) + archive_digest = bundle.declaration()["host"]["sha256"][target] + receipt["bundler"] = { + "version": bundle.declaration()["host"]["version"], + "archiveSha256": archive_digest, + "binarySha256": "b" * 64, + } + receipt["cargoLockSha256"] = bundle.sha256( + (bundle.ROOT / "rust/Cargo.lock").read_bytes() + ) + receipt["installedHost"] = { + **copy.deepcopy(receipt), + "host": { + "morphir": bundle.declaration()["host"]["version"], + "sha256": archive_digest, + "binary_sha256": "b" * 64, + }, + "python_api": {"native_version": tools_pin(), "compiler_version": version}, + "offline_round_trips": results, + "qualification_script_sha256": bundle.sha256( + (bundle.ROOT / "scripts/extension_bundle.py").read_bytes() + ), + } + path = platform_root / f".dev/qualification/process-{target}.json" + path.parent.mkdir(parents=True) + path.write_text(json.dumps(receipt)) + # Artifact transfer preserves bytes, but not POSIX execute bits. + executable.chmod(0o600) + return claims, results + + +def test_complete_process_set_keeps_canonical_per_platform_artifacts( + tmp_path: Path, +) -> None: + inputs, output = tmp_path / "inputs", tmp_path / "release" + claims, results = candidates(inputs) + report = release.assemble(inputs, output, COMMIT, claims, results) + assert report["sourceCommit"] == COMMIT + assert report["platforms"] == list(process.PLATFORMS) + assert len(report["files"]) == 11 + descriptor = json.loads((output / "release.json").read_text()) + assert len(descriptor["artifacts"]) == 5 + assert {artifact["runtime"] for artifact in descriptor["artifacts"]} == {"process"} + assert all(artifact["claims"] == claims for artifact in descriptor["artifacts"]) + + +@pytest.mark.parametrize( + "change", + [ + "missing", + "extra", + "source", + "lock", + "files", + "claims", + "host", + "api", + "v4", + "results", + "bytes", + "checker", + "binary", + ], +) +def test_mixed_missing_or_unqualified_process_sets_are_refused( + tmp_path: Path, change: str +) -> None: + inputs, output = tmp_path / "inputs", tmp_path / "release" + claims, results = candidates(inputs) + target = process.PLATFORMS[0] + platform_root = inputs / f"morphir-python-process-{target}" + receipt_path = platform_root / f".dev/qualification/process-{target}.json" + receipt = json.loads(receipt_path.read_text()) + if change == "missing": + receipt_path.unlink() + elif change == "extra": + (inputs / "morphir-python-process-unknown").mkdir() + elif change == "source": + receipt["sourceCommit"] = "c" * 40 + elif change == "lock": + receipt["cargoLockSha256"] = "c" * 64 + elif change == "files": + receipt["files"]["release.json"] = "c" * 64 + elif change == "claims": + receipt["claims"]["capabilities"]["progress"] = True + elif change == "host": + receipt["bundler"]["archiveSha256"] = "c" * 64 + elif change == "checker": + receipt["installedHost"]["qualification_script_sha256"] = "c" * 64 + elif change == "binary": + receipt["installedHost"]["host"]["binary_sha256"] = "c" * 64 + elif change == "api": + receipt["installedHost"]["python_api"]["native_version"] = "0.0.0" + elif change == "v4": + del receipt["installedHost"]["offline_round_trips"]["v4"] + elif change == "results": + receipt["installedHost"]["offline_round_trips"]["v4"]["modules"] = [] + elif change == "bytes": + name = process.artifact_name(bundle.crate_version(), target) + (platform_root / ".morphir/build/extensions/python-process" / name).write_bytes( + b"changed executable" + ) + if change != "missing": + receipt_path.write_text(json.dumps(receipt)) + with pytest.raises((bundle.BundleError, OSError)): + release.assemble(inputs, output, COMMIT, claims, results) + assert not output.exists() + + +def test_process_assembly_does_not_overwrite_an_existing_directory( + tmp_path: Path, +) -> None: + inputs, output = tmp_path / "inputs", tmp_path / "release" + claims, results = candidates(inputs) + output.mkdir() + (output / "keep").write_text("user data") + with pytest.raises(FileExistsError): + release.assemble(inputs, output, COMMIT, claims, results) + assert (output / "keep").read_text() == "user data" From b2c6dfae00524ffd4bacc3115d54b062501bc688 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:55:56 -0500 Subject: [PATCH 10/26] fix(release): isolate mismatched install metadata --- scripts/qualify_native.py | 9 +++++++- tests/unit/test_native_qualification.py | 30 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/scripts/qualify_native.py b/scripts/qualify_native.py index 18f22e10..8e5ef914 100644 --- a/scripts/qualify_native.py +++ b/scripts/qualify_native.py @@ -305,7 +305,14 @@ def _mismatch(python: Path, path: str) -> None: text=True, ).stdout.strip() file = Path(metadata) / "METADATA" - file.write_text(re.sub(r"(?m)^Version: .*$", "Version: 0.0.0", file.read_text())) + # uv may hard-link installed metadata to its wheel cache. Replace this + # installation's directory entry so the negative fixture cannot corrupt + # the cache or another environment's copy of the qualified distribution. + replacement = file.with_name("METADATA.mismatch") + replacement.write_text( + re.sub(r"(?m)^Version: .*$", "Version: 0.0.0", file.read_text()) + ) + replacement.replace(file) result = subprocess.run( [ _script(python, "morphir-python"), diff --git a/tests/unit/test_native_qualification.py b/tests/unit/test_native_qualification.py index 3e3623ad..8dade6d9 100644 --- a/tests/unit/test_native_qualification.py +++ b/tests/unit/test_native_qualification.py @@ -111,3 +111,33 @@ def test_rust_free_path_drops_directories_holding_only_rustup( rustup.chmod(0o755) monkeypatch.setenv("PATH", f"{tools}{os.pathsep}/usr/bin") assert str(tools) not in q.rust_free_path(tmp_path / "venv").split(os.pathsep) + + +def test_mismatched_install_does_not_modify_hardlinked_wheel_cache( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + import subprocess + + cached = tmp_path / "cached-metadata" + original = "Name: morphir-python-native\nVersion: 0.1.1\n" + cached.write_text(original) + installed = tmp_path / "installed.dist-info" + installed.mkdir() + metadata = installed / "METADATA" + metadata.hardlink_to(cached) + + def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + if "from importlib.metadata" in command[-1]: + return subprocess.CompletedProcess( + command, 0, stdout=str(installed), stderr="" + ) + assert "Version: 0.0.0" in metadata.read_text() + return subprocess.CompletedProcess( + command, 1, stdout="", stderr="morphir-python-native 0.0.0" + ) + + monkeypatch.setattr(q.subprocess, "run", run) + monkeypatch.setattr(q, "_script", lambda python, name: "not-executed") + q._mismatch(tmp_path / "python", "empty-path") + assert cached.read_text() == original + assert "Version: 0.0.0" in metadata.read_text() From 820c9d0dd19e873ae23d9d73972f89119460e6c1 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:05:07 -0500 Subject: [PATCH 11/26] fix(extension): enforce installed runtime integrity --- .gitattributes | 1 + .github/scripts/check_process_windows.ps1 | 45 +++ .github/workflows/release.yml | 23 ++ docs/python-extension.md | 63 +++- .../add-process-mep-extension/design.html | 4 +- scripts/extension_bundle.py | 100 ++++++ scripts/process_release.py | 65 ++++ scripts/qualify_runtime.py | 320 ++++++++++++++++++ tests/tools/test_runtime_choice.py | 36 ++ tests/unit/test_extension_bundle.py | 18 + tests/unit/test_process_release.py | 66 +++- 11 files changed, 737 insertions(+), 4 deletions(-) create mode 100644 .github/scripts/check_process_windows.ps1 create mode 100644 scripts/qualify_runtime.py create mode 100644 tests/tools/test_runtime_choice.py diff --git a/.gitattributes b/.gitattributes index 8d7c2c29..4c1e09f2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ # Release receipts bind these inputs as bytes on every runner OS. rust/Cargo.lock text eol=lf scripts/*.py text eol=lf +.github/scripts/*.ps1 text eol=lf diff --git a/.github/scripts/check_process_windows.ps1 b/.github/scripts/check_process_windows.ps1 new file mode 100644 index 00000000..ec33ff57 --- /dev/null +++ b/.github/scripts/check_process_windows.ps1 @@ -0,0 +1,45 @@ +param( + [Parameter(Mandatory = $true)][string]$Executable, + [Parameter(Mandatory = $true)][string]$Report +) +$ErrorActionPreference = "Stop" +$processBinary = (Resolve-Path -LiteralPath $Executable).Path +$vsWhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" +$toolCandidates = @(& $vsWhere -latest -products "*" -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -find "VC\Tools\MSVC\**\bin\Hostx64\x64\dumpbin.exe") +$toolLocation = $toolCandidates | Select-Object -First 1 +if ($LASTEXITCODE -ne 0 -or !$toolLocation) { + throw "Cannot locate the MSVC dependency inspector" +} +$dependencyOutput = & $toolLocation /DEPENDENTS $processBinary +if ($LASTEXITCODE -ne 0) { + throw "MSVC dependency inspection failed" +} +$dependencies = @( + [regex]::Matches(($dependencyOutput -join "`n"), '(?i)\b[A-Za-z0-9_.-]+\.dll\b') | + ForEach-Object { $_.Value.ToLowerInvariant() } | + Sort-Object -Unique +) +if ($dependencies.Count -eq 0) { + throw "Dependency inspection returned no Windows system DLLs" +} +if ($dependencies -match '^(vcruntime|msvcp|concrt|python)\d') { + throw "Standalone process imports an external VC or Python runtime: $($dependencies -join ', ')" +} +$receipt = @{ + sourceCommit = $env:GITHUB_SHA + crt = "static" + artifact = @{ + sha256 = (Get-FileHash -LiteralPath $processBinary -Algorithm SHA256).Hash.ToLowerInvariant() + } + checkerSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant() + tool = @{ + name = "dumpbin.exe" + sha256 = (Get-FileHash -LiteralPath $toolLocation -Algorithm SHA256).Hash.ToLowerInvariant() + version = (Get-Item -LiteralPath $toolLocation).VersionInfo.FileVersion + } + dependencies = $dependencies +} +$reportFile = [System.IO.Path]::GetFullPath($Report) +[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($reportFile)) | Out-Null +[System.IO.File]::WriteAllText($reportFile, ($receipt | ConvertTo-Json -Depth 5) + "`n", [System.Text.UTF8Encoding]::new($false)) +$receipt | ConvertTo-Json -Depth 5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7326a49f..14d50b98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -176,10 +176,24 @@ jobs: shell: bash env: MACOSX_DEPLOYMENT_TARGET: ${{ matrix.target == 'aarch64-apple-darwin' && '11.0' || '10.12' }} + # The standalone guest must not require the VC redistributable. + # This setting is confined to the executable, not the Python wheel. + RUSTFLAGS: ${{ runner.os == 'Windows' && '-C target-feature=+crt-static' || '' }} run: >- cargo +1.98.1 build --manifest-path rust/Cargo.toml --locked --release -p morphir-python-extension --target '${{ matrix.target }}' + - name: Refuse external VC and Python runtime dependencies + if: >- + runner.os == 'Windows' && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + shell: pwsh + run: | + ./.github/scripts/check_process_windows.ps1 ` + -Executable rust/target/x86_64-pc-windows-msvc/release/morphir-python-extension.exe ` + -Report .dev/qualification/windows-process-imports.json + - name: Qualify the process bundle against the matching Python API if: >- !matrix.reuse && @@ -214,6 +228,7 @@ jobs: path: | .morphir/build/extensions/python-process/ .dev/qualification/process-${{ matrix.target }}.json + .dev/qualification/*-process-imports.json include-hidden-files: true if-no-files-found: error @@ -306,6 +321,13 @@ jobs: --wasm-report dist/wasm-check/.dev/qualification/bundle-host.json \ --report dist/process-assembly.json + - name: Verify runtime switching, installed integrity and platform refusal + run: | + python -m scripts.qualify_runtime \ + --wasm dist/wasm-check/.morphir/build/extensions/python \ + --process dist/process-release \ + --report dist/runtime-choice.json + - name: Upload the process-only candidate and its evidence uses: actions/upload-artifact@v6 with: @@ -313,6 +335,7 @@ jobs: path: | dist/process-release/ dist/process-assembly.json + dist/runtime-choice.json dist/process-inputs/*/.dev/qualification/*.json include-hidden-files: true if-no-files-found: error diff --git a/docs/python-extension.md b/docs/python-extension.md index 5ce7d5cb..42b3588b 100644 --- a/docs/python-extension.md +++ b/docs/python-extension.md @@ -110,6 +110,65 @@ Use `python3 scripts/docs_hosts.py qualify` to repeat the displayed workflow and offline checks. Receipts record pins, file hashes, host identity, checker and input digest. Download failure never counts as a passing qualification. +## Choose a runtime in development + +Published bundles remain WASM. The process bundle is a development candidate +from the manual release-qualification workflow, not a published installation +channel yet. Its executable is a MEP guest for the finos/morphir host. The +`morphir-python` CLI continues to use its pinned native Python wheel through the +[mise/pipx installation](python-compiler.md); choosing an extension runtime does +not configure that CLI. + +Use two separate canonical bundle directories and named local repositories. +The WASM bundle contains its `.wasm`, checksum and `release.json`. The process +bundle contains the raw executables, their checksums and its own `release.json`. +The canonical repository writer refuses a bundle mixing both runtimes. +The process candidate includes Linux x86_64/ARM64, macOS x86_64/ARM64 and Windows +x86_64. Installation chooses the matching platform and refuses an absent one. + +Set `WASM_BUNDLE` and `PROCESS_BUNDLE` to those unmodified directories. With +`MORPHIR_HOST` pointing at the pinned host described above, an isolated trial is: + +```bash +export MORPHIR_HOME="$(mktemp -d)" +VERSION=0.4.2 # Current development candidate version from release.json. +"$MORPHIR_HOST" extension repository init ./wasm-repository +"$MORPHIR_HOST" extension repository init ./process-repository +"$MORPHIR_HOST" extension repository add python-wasm --directory ./wasm-repository +"$MORPHIR_HOST" extension repository add python-process --directory ./process-repository +"$MORPHIR_HOST" extension repository publish python-wasm --bundle "$WASM_BUNDLE" +"$MORPHIR_HOST" extension repository publish python-process --bundle "$PROCESS_BUNDLE" +"$MORPHIR_HOST" extension install morphir-python --repository python-wasm --version "$VERSION" + +# Switch the active provider in this isolated Morphir home. +"$MORPHIR_HOST" extension uninstall morphir-python +"$MORPHIR_HOST" extension install morphir-python --repository python-process --version "$VERSION" +``` + +The repository selects the runtime; this host has no `--runtime` installation +flag. It permits one active `morphir-python` provider per home. Uninstall it +before installing the other runtime from its explicit repository. Compile and +generate commands remain the same. The process guest starts in stdio mode with +no arguments; `--stdio` is also accepted by hosts configured to pass it. +Installation restores executable permissions, so the Morphir host can launch a +downloaded raw executable without a user Rust toolchain or Python interpreter. +A process has ordinary OS rights; WASM uses the host's Extism isolation. + +The release-assembly check runs this switch in both directions with fresh +project caches, compares v3/v4 IR and generated-file digests, rejects an actual +foreign-only bundle, and checks that changing installed bytes causes a digest +refusal before compilation. Each platform's own bundle qualifier also checks +installed-byte integrity. These are legacy-route checks. They do not establish +support for the new MEP draft, which remains unadvertised pending the published +SDK prerequisite and independent executable conformance. + +To verify already built candidates without rebuilding them: + +```bash +python -m scripts.qualify_runtime --wasm "$WASM_BUNDLE" \ + --process "$PROCESS_BUNDLE" --report .dev/qualification/runtime-choice.json +``` + ## Build a development bundle From the repository root: @@ -133,7 +192,9 @@ This task: deletes the repository, blocks network access, and compiles, generates and recompiles a two-module model in IR v3 and v4; 4. checks that the IR from the extension equals the IR from the Python API for - the same sources. + the same sources; +5. requires digest refusal when installed bytes are changed in a fresh project, + then restores the original artifact. The bundle is written to `.morphir/build/extensions/python/`, and JSON receipts with every digest are written to `.dev/qualification/`. A bundle built from a diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 830776da..5bf07250 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -26,7 +26,7 @@

add-process-mep-extension

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The published SDK 0.3.0 exposes NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type still uses a numeric u64 ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The Python implementation does not opt into model transport and its manifest declares MEP 0.1 only. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

-

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

CriterionWeightCurrent assessment
Risk and coverage fit253: focused local refusal tests and installed macOS legacy evidence pass; matrix results pending.
Feedback and placement203: one Linux PR path; broader work stays manual/release. Added CI latency not yet measured.
BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. Crate-tag publication approval is pending; the SDK is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

+

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. Crate-tag publication approval is pending; the SDK is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. Approval is pending for a separate upstream Python JSON profile; no existing MoonBit check will be relaxed and no unit-test result substitutes for executable qualification.

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. Protocol discovery remains legacy-only until the published SDK prerequisite and candidate-bound conformance pass. All five task groups remain incomplete.

Build and release assessment

@@ -45,7 +45,7 @@

add-process-mep-extension

Ownership and structure104: existing bundle and native-wheel categories, explicit needs, repository maintainers own failures. Operability and recovery102: candidate refusal tests pass; platform rerun/transfer and final promotion refusals pending. -

Total withheld while 35 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

+

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. The new Windows static-CRT producer and its dependency proof still need a replacement candidate run.

Total withheld while 35 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

Repository evidence and references

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/scripts/extension_bundle.py b/scripts/extension_bundle.py index be05bae3..eb053b6b 100644 --- a/scripts/extension_bundle.py +++ b/scripts/extension_bundle.py @@ -25,7 +25,9 @@ import json import os import platform +import re import shutil +import stat import subprocess import sys import tarfile @@ -281,6 +283,102 @@ def morphir_cli() -> tuple[Path, str]: return binary, expected +def is_digest_mismatch(text: str, expected: str, observed: str) -> bool: + """Require the verifier's mismatch and both exact hashes, allowing wrapping.""" + compact = re.sub(r"[\s│╰─]", "", text.lower()) + return "digestmismatch" in compact and expected in compact and observed in compact + + +def check_installed_integrity( + cli: Path, home: Path, environment: dict[str, str] +) -> dict[str, Any]: + """Refuse changed installed bytes in a fresh project, then restore them.""" + catalog = json.loads((home / "catalog/extensions.json").read_text()) + (installed,) = catalog["extensions"] + stored = home / installed["storePath"] + if ( + stored.is_symlink() + or not stored.is_file() + or not stored.resolve().is_relative_to(home.resolve()) + ): + raise BundleError("integrity fixture must be a regular file inside its home") + original = stored.read_bytes() + expected = sha256(original) + if installed["digest"] != expected: + raise BundleError("installed artifact already disagrees with its catalog") + modified = bytearray(original) + modified[-1] ^= 1 + observed = sha256(bytes(modified)) + mode = stat.S_IMODE(stored.stat().st_mode) + + def replace_bytes(data: bytes) -> None: + # Replacing the directory entry preserves any hardlinked cache or input. + with tempfile.NamedTemporaryFile(dir=stored.parent, delete=False) as file: + file.write(data) + replacement = Path(file.name) + try: + replacement.chmod(mode) + replacement.replace(stored) + finally: + replacement.unlink(missing_ok=True) + + with tempfile.TemporaryDirectory(prefix="morphir-integrity-") as temporary: + project = Path(temporary) + shutil.copytree(SAMPLE, project / "src") + (project / "morphir.toml").write_text( + '[project]\nname = "acme/example"\nversion = "0.1.0"\n' + 'source_directory = "src"\n[frontend]\nlanguage = "python"\n' + "emit_parse_stage = false\n" + ) + env = {**environment, "MORPHIR_HOME": str(home)} + for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + env[name] = env[name.lower()] = OFFLINE + env.pop("NO_PROXY", None) + env.pop("no_proxy", None) + command = [ + str(cli), + "--no-banner", + "compile", + "--language", + "python", + "--input", + "src", + "--ir-version", + "4", + "--output", + "must-not-exist", + ] + replace_bytes(bytes(modified)) + try: + result = subprocess.run( + command, + cwd=project, + env=env, + capture_output=True, + text=True, + timeout=60, + ) + reason = result.stdout + result.stderr + if ( + result.returncode == 0 + or not is_digest_mismatch(reason, expected, observed) + or (project / "must-not-exist").exists() + ): + raise BundleError( + f"installed artifact integrity was not enforced: {reason}" + ) + finally: + replace_bytes(original) + return { + "expected_sha256": expected, + "tampered_sha256": observed, + "exit_code": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "fresh_project": True, + } + + def host_check(bundle: Path, *, version: str | None = None) -> dict[str, object]: """Install the bundle with the pinned CLI and round trip offline.""" return host_round_trips(bundle, receipt(bundle, version=version)) @@ -373,8 +471,10 @@ def morphir(*args: str, offline: bool = False) -> str: first, tag, compiler_version=str(described["version"]) ), } + integrity = check_installed_integrity(cli, home, base) return { **described, + "installed_integrity": integrity, "host": { "morphir": declaration()["host"]["version"], "sha256": cli_digest, diff --git a/scripts/process_release.py b/scripts/process_release.py index af4f65d9..dc818ee3 100644 --- a/scripts/process_release.py +++ b/scripts/process_release.py @@ -10,6 +10,7 @@ import argparse import json +import re import shutil from pathlib import Path from typing import Any @@ -27,6 +28,39 @@ def _read(path: Path) -> dict[str, Any]: return value +def _windows_imports(root: Path, artifact_digest: str, commit: str) -> str: + path = root / ".dev/qualification/windows-process-imports.json" + proof = _read(path) + try: + dependencies = proof["dependencies"] + if not ( + proof["sourceCommit"] == commit + and proof["crt"] == "static" + and proof["artifact"]["sha256"] == artifact_digest + and proof["checkerSha256"] + == bundle.sha256( + (bundle.ROOT / ".github/scripts/check_process_windows.ps1").read_bytes() + ) + and proof["tool"]["name"] == "dumpbin.exe" + and re.fullmatch(r"[0-9a-f]{64}", proof["tool"]["sha256"]) + and proof["tool"]["version"] + and isinstance(dependencies, list) + and dependencies + and all( + isinstance(name, str) and re.fullmatch(r"(?i)[a-z0-9_.-]+\.dll", name) + for name in dependencies + ) + and not any( + re.match(r"(?i)(vcruntime|msvcp|concrt|python)\d", name) + for name in dependencies + ) + ): + raise bundle.BundleError("Windows process runtime dependency proof differs") + except (KeyError, TypeError) as error: + raise bundle.BundleError("incomplete Windows process runtime proof") from error + return bundle.sha256(path.read_bytes()) + + def assemble( inputs: Path, output: Path, @@ -50,6 +84,7 @@ def assemble( source_files: list[Path] = [] artifacts: list[dict[str, Any]] = [] reports: dict[str, str] = {} + windows_imports_digest = "" for target in process.PLATFORMS: root = inputs / f"morphir-python-process-{target}" directory = root / ".morphir/build/extensions/python-process" @@ -63,6 +98,10 @@ def assemble( raise bundle.BundleError("process artifact is not a regular file") executable.chmod(0o700) checked = process.receipt(directory, commit, target) + if target.endswith("windows-msvc"): + windows_imports_digest = _windows_imports( + root, checked["files"][name], commit + ) try: for field in ( "sourceCommit", @@ -87,6 +126,21 @@ def assemble( and host["sourceCommit"] == commit and host["version"] == version and host["files"] == checked["files"] + and host["installed_integrity"]["expected_sha256"] + == checked["files"][name] + and re.fullmatch( + r"[0-9a-f]{64}", host["installed_integrity"]["tampered_sha256"] + ) + and host["installed_integrity"]["tampered_sha256"] + != checked["files"][name] + and host["installed_integrity"]["exit_code"] != 0 + and host["installed_integrity"]["fresh_project"] is True + and bundle.is_digest_mismatch( + host["installed_integrity"]["stdout"] + + host["installed_integrity"]["stderr"], + checked["files"][name], + host["installed_integrity"]["tampered_sha256"], + ) and host["host"]["morphir"] == pin["version"] and host["host"]["sha256"] == pin["sha256"][target] and host["host"]["binary_sha256"] == report["bundler"]["binarySha256"] @@ -138,6 +192,7 @@ def assemble( for path in sorted(output.iterdir()) }, "inputReports": reports, + "windowsImportsSha256": windows_imports_digest, } @@ -160,6 +215,16 @@ def main(argv: list[str] | None = None) -> int: if not ( reference["git_commit"] == args.commit and reference["bundle"] == checked["bundle"] + and reference["installed_integrity"]["expected_sha256"] + == checked["wasm_sha256"] + and reference["installed_integrity"]["exit_code"] != 0 + and reference["installed_integrity"]["fresh_project"] is True + and bundle.is_digest_mismatch( + reference["installed_integrity"]["stdout"] + + reference["installed_integrity"]["stderr"], + str(checked["wasm_sha256"]), + reference["installed_integrity"]["tampered_sha256"], + ) and reference["qualification_script_sha256"] == bundle.sha256((bundle.ROOT / "scripts/extension_bundle.py").read_bytes()) and reference["python_api"]["native_version"] == tools_pin() diff --git a/scripts/qualify_runtime.py b/scripts/qualify_runtime.py new file mode 100644 index 00000000..80d68524 --- /dev/null +++ b/scripts/qualify_runtime.py @@ -0,0 +1,320 @@ +"""Verify runtime selection and installed-byte integrity through the pinned CLI. + +Use canonical WASM and process bundles from the same candidate. The process +bundle must include the current platform and a real foreign platform. This is +installed legacy-route qualification, independent of executable MEP conformance. +""" + +from __future__ import annotations + +import argparse +import copy +import json +import os +import shutil +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle + + +def _descriptor(directory: Path, runtime: str) -> dict[str, Any]: + descriptor = json.loads((directory / "release.json").read_text()) + expected = {"release.json"} + for entry in descriptor["artifacts"]: + name = entry["filename"] + if "/" in name or "\\" in name or name in {".", ".."}: + raise bundle.BundleError("bundle artifact filename must be portable") + path = directory / name + if entry["runtime"] != runtime or path.is_symlink() or not path.is_file(): + raise bundle.BundleError("bundle runtime or artifact kind differs") + digest = bundle.sha256(path.read_bytes()) + if entry["sha256"] != digest: + raise bundle.BundleError("bundle artifact digest differs") + if (directory / f"{name}.sha256").read_text() != f"{digest} {name}\n": + raise bundle.BundleError("bundle checksum differs") + expected.update((name, f"{name}.sha256")) + if {path.name for path in directory.iterdir()} != expected: + raise bundle.BundleError("bundle inventory differs") + return dict(descriptor) + + +def qualify(wasm: Path, process: Path) -> dict[str, Any]: + """Exercise actual selected bytes in one home with fresh project caches.""" + wasm, process = wasm.resolve(), process.resolve() + declarations = { + "wasm": _descriptor(wasm, "wasm"), + "process": _descriptor(process, "process"), + } + first, second = declarations.values() + if not ( + first["extensionId"] == second["extensionId"] == "morphir-python" + and first["version"] == second["version"] == bundle.crate_version() + and first["gitCommit"] == second["gitCommit"] + ): + raise bundle.BundleError("runtime bundles are not the same candidate") + platform = bundle._target() + foreign = [entry for entry in second["artifacts"] if entry["platform"] != platform] + if not foreign: + raise bundle.BundleError("platform refusal check needs a real foreign artifact") + cli, archive_digest = bundle.morphir_cli() + cli = cli.resolve() + switches: list[dict[str, Any]] = [] + with tempfile.TemporaryDirectory(prefix="morphir-runtime-choice-") as temporary: + root = Path(temporary) + home, admin = root / "home", root / "admin" + admin.mkdir() + env = {**os.environ, "MORPHIR_HOME": str(home), "MORPHIR_NO_BANNER": "1"} + for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + env[name] = env[name.lower()] = bundle.OFFLINE + env.pop("NO_PROXY", None) + env.pop("no_proxy", None) + + def invoke( + cwd: Path, *arguments: str, fail: bool = False + ) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + [str(cli), "--no-banner", *arguments], + cwd=cwd, + env=env, + capture_output=True, + text=True, + timeout=60, + ) + if (result.returncode != 0) != fail: + raise bundle.BundleError( + f"runtime check {arguments}: {result.stdout} {result.stderr}" + ) + return result + + def project(name: str) -> Path: + directory = root / name + shutil.copytree(bundle.SAMPLE, directory / "src") + (directory / "morphir.toml").write_text( + '[project]\nname = "acme/example"\nversion = "0.1.0"\n' + 'source_directory = "src"\n[frontend]\nlanguage = "python"\n' + "emit_parse_stage = false\n" + ) + return directory + + for runtime, directory in (("wasm", wasm), ("process", process)): + repository = root / f"repository-{runtime}" + name = f"python-{runtime}" + invoke(admin, "extension", "repository", "init", str(repository)) + invoke( + admin, + "extension", + "repository", + "add", + name, + "--directory", + str(repository), + ) + invoke( + admin, + "extension", + "repository", + "publish", + name, + "--bundle", + str(directory), + ) + invoke(admin, "extension", "repository", "verify", name) + + for index, runtime in enumerate(("wasm", "process", "wasm")): + current = project(f"project-{index}") + invoke( + current, + "extension", + "install", + "morphir-python", + "--repository", + f"python-{runtime}", + "--version", + first["version"], + ) + catalog = json.loads((home / "catalog/extensions.json").read_text()) + (installed,) = catalog["extensions"] + entries = declarations[runtime]["artifacts"] + (selected,) = [ + entry + for entry in entries + if runtime == "wasm" or entry["platform"] == platform + ] + if not ( + installed["runtime"] == runtime + and installed["digest"] == selected["sha256"] + and installed["version"] == first["version"] + and installed["args"] == [] + ): + raise bundle.BundleError( + "installed catalog differs from the selected runtime" + ) + stored = home / installed["storePath"] + if bundle.sha256(stored.read_bytes()) != selected["sha256"]: + raise bundle.BundleError( + "installed bytes differ from selected artifact" + ) + results: dict[str, dict[str, object]] = {} + for version in ("3.0.0", "4") if index < 2 else ("4",): + tag = version[0] + invoke( + current, + "compile", + "--language", + "python", + "--input", + "src", + "--ir-version", + version, + "--output", + f"compiled-{tag}", + ) + document = json.loads( + (current / f"compiled-{tag}/morphir-ir.json").read_text() + ) + results[f"v{tag}"] = { + "ir_sha256": bundle.sha256( + json.dumps( + document, sort_keys=True, separators=(",", ":") + ).encode() + ) + } + if index < 2: + generated = current / f"generated-{tag}" + invoke( + current, + "generate", + "--target", + "python", + "--input", + f"compiled-{tag}/morphir-ir.json", + "--output", + str(generated), + ) + results[f"v{tag}"]["generated_sha256"] = { + file.relative_to(generated).as_posix(): bundle.sha256( + file.read_bytes() + ) + for file in sorted(generated.rglob("*.py")) + } + switches.append( + { + "runtime": runtime, + "artifact_sha256": selected["sha256"], + "results": results, + } + ) + if index == 1 and switches[0]["results"] != results: + raise bundle.BundleError("WASM and process installed results differ") + if index == 2: + if ( + results["v4"]["ir_sha256"] + != switches[0]["results"]["v4"]["ir_sha256"] + ): + raise bundle.BundleError( + "switching back to WASM changed the result" + ) + integrity = bundle.check_installed_integrity(cli, home, env) + invoke(current, "extension", "uninstall", "morphir-python") + + # Publish only real non-current platform bytes into a separate local index. + foreign_bundle = root / "foreign-bundle" + foreign_bundle.mkdir() + descriptor = copy.deepcopy(second) + descriptor["artifacts"] = foreign + for entry in foreign: + for name in (entry["filename"], f"{entry['filename']}.sha256"): + shutil.copyfile(process / name, foreign_bundle / name) + (foreign_bundle / "release.json").write_text( + json.dumps(descriptor, indent=2) + "\n" + ) + foreign_repo = root / "repository-foreign" + invoke(admin, "extension", "repository", "init", str(foreign_repo)) + invoke( + admin, + "extension", + "repository", + "add", + "python-foreign", + "--directory", + str(foreign_repo), + ) + invoke( + admin, + "extension", + "repository", + "publish", + "python-foreign", + "--bundle", + str(foreign_bundle), + ) + rejected = invoke( + admin, + "extension", + "install", + "morphir-python", + "--repository", + "python-foreign", + "--version", + first["version"], + fail=True, + ) + if "platform" not in (rejected.stdout + rejected.stderr).lower(): + raise bundle.BundleError("foreign-only bundle was not refused for platform") + final_catalog = json.loads((home / "catalog/extensions.json").read_text()) + if final_catalog["extensions"]: + raise bundle.BundleError("unsupported platform unexpectedly installed") + return { + "scope": "installed-legacy-runtime-selection", + "sourceCommit": first["gitCommit"], + "version": first["version"], + "host": { + "version": bundle.declaration()["host"]["version"], + "archive_sha256": archive_digest, + "binary_sha256": bundle.sha256(cli.read_bytes()), + }, + "qualification_script_sha256": bundle.sha256(Path(__file__).read_bytes()), + "descriptors": { + runtime: bundle.sha256((directory / "release.json").read_bytes()) + for runtime, directory in (("wasm", wasm), ("process", process)) + }, + "integrity_checker_sha256": bundle.sha256(Path(bundle.__file__).read_bytes()), + "installed_integrity": integrity, + "switches": switches, + "tampered_install_refused": True, + "unsupported_platform_refused": True, + } + + +def main(argv: list[str] | None = None) -> int: + """Write actual runtime-choice evidence outside the supplied bundles.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--wasm", type=Path, required=True) + parser.add_argument("--process", type=Path, required=True) + parser.add_argument("--report", type=Path, required=True) + args = parser.parse_args(argv) + if any( + args.report.resolve().is_relative_to(path.resolve()) + for path in (args.wasm, args.process) + ): + parser.error("report must remain outside canonical input bundles") + try: + result = qualify(args.wasm, args.process) + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(result, indent=2) + "\n") + except ( + bundle.BundleError, + OSError, + KeyError, + ValueError, + subprocess.TimeoutExpired, + ) as error: + parser.exit(1, f"runtime qualification refused: {error}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/tools/test_runtime_choice.py b/tests/tools/test_runtime_choice.py new file mode 100644 index 00000000..526af61e --- /dev/null +++ b/tests/tools/test_runtime_choice.py @@ -0,0 +1,36 @@ +"""Exercise real installed runtime selection and byte-integrity refusals.""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + + +@pytest.mark.skipif( + not ( + os.environ.get("MORPHIR_TEST_WASM_BUNDLE") + and os.environ.get("MORPHIR_TEST_PROCESS_BUNDLE") + ), + reason="requires exact built WASM and process bundles plus the pinned host CLI", +) +def test_installed_runtime_choices_are_equivalent_and_tamper_is_refused() -> None: + from scripts.qualify_runtime import qualify + + report = qualify( + Path(os.environ["MORPHIR_TEST_WASM_BUNDLE"]), + Path(os.environ["MORPHIR_TEST_PROCESS_BUNDLE"]), + ) + assert [step["runtime"] for step in report["switches"]] == [ + "wasm", + "process", + "wasm", + ] + assert report["switches"][0]["results"] == report["switches"][1]["results"] + assert ( + report["switches"][2]["results"]["v4"]["ir_sha256"] + == report["switches"][0]["results"]["v4"]["ir_sha256"] + ) + assert report["tampered_install_refused"] is True + assert report["unsupported_platform_refused"] is True diff --git a/tests/unit/test_extension_bundle.py b/tests/unit/test_extension_bundle.py index ede87089..05d4fac1 100644 --- a/tests/unit/test_extension_bundle.py +++ b/tests/unit/test_extension_bundle.py @@ -112,3 +112,21 @@ def download(url: str, *, timeout: int) -> io.BytesIO: with pytest.raises(bundle.BundleError, match="digest differs"): bundle.morphir_cli() assert len(urls) == 1 + + +def test_integrity_refusal_requires_the_mismatch_and_both_actual_hashes() -> None: + expected, observed = "a" * 64, "b" * 64 + assert bundle.is_digest_mismatch( + f"digest mismatch: expected {expected[:32]}\n│ {expected[32:]}, got {observed}", + expected, + observed, + ) + assert not bundle.is_digest_mismatch( + "cannot read morphir-integrity/sha256/log", expected, observed + ) + assert not bundle.is_digest_mismatch( + f"digest mismatch: expected {expected}", expected, observed + ) + assert not bundle.is_digest_mismatch( + f"invalid module at sha256/{expected}", expected, observed + ) diff --git a/tests/unit/test_process_release.py b/tests/unit/test_process_release.py index 37097d2c..d8d45852 100644 --- a/tests/unit/test_process_release.py +++ b/tests/unit/test_process_release.py @@ -81,6 +81,14 @@ def candidates(root: Path) -> tuple[dict[str, Any], dict[str, Any]]: }, "python_api": {"native_version": tools_pin(), "compiler_version": version}, "offline_round_trips": results, + "installed_integrity": { + "expected_sha256": digest, + "tampered_sha256": "e" * 64, + "exit_code": 1, + "stdout": "", + "stderr": f"digest mismatch: expected {digest}, got {'e' * 64}", + "fresh_project": True, + }, "qualification_script_sha256": bundle.sha256( (bundle.ROOT / "scripts/extension_bundle.py").read_bytes() ), @@ -88,6 +96,28 @@ def candidates(root: Path) -> tuple[dict[str, Any], dict[str, Any]]: path = platform_root / f".dev/qualification/process-{target}.json" path.parent.mkdir(parents=True) path.write_text(json.dumps(receipt)) + if target.endswith("windows-msvc"): + (path.parent / "windows-process-imports.json").write_text( + json.dumps( + { + "sourceCommit": COMMIT, + "crt": "static", + "artifact": {"sha256": digest}, + "checkerSha256": bundle.sha256( + ( + bundle.ROOT + / ".github/scripts/check_process_windows.ps1" + ).read_bytes() + ), + "tool": { + "name": "dumpbin.exe", + "sha256": "d" * 64, + "version": "test", + }, + "dependencies": ["kernel32.dll", "ntdll.dll"], + } + ) + ) # Artifact transfer preserves bytes, but not POSIX execute bits. executable.chmod(0o600) return claims, results @@ -124,6 +154,14 @@ def test_complete_process_set_keeps_canonical_per_platform_artifacts( "bytes", "checker", "binary", + "windows-runtime", + "windows-bytes", + "windows-checker", + "windows-missing", + "integrity-digest", + "integrity-not-run", + "integrity-cache", + "integrity-reason", ], ) def test_mixed_missing_or_unqualified_process_sets_are_refused( @@ -135,7 +173,33 @@ def test_mixed_missing_or_unqualified_process_sets_are_refused( platform_root = inputs / f"morphir-python-process-{target}" receipt_path = platform_root / f".dev/qualification/process-{target}.json" receipt = json.loads(receipt_path.read_text()) - if change == "missing": + if change.startswith("integrity-"): + proof = receipt["installedHost"]["installed_integrity"] + if change == "integrity-digest": + proof["expected_sha256"] = "c" * 64 + elif change == "integrity-not-run": + proof["exit_code"] = 0 + elif change == "integrity-cache": + proof["fresh_project"] = False + elif change == "integrity-reason": + proof["stderr"] = "unrelated permission error" + elif change.startswith("windows-"): + windows = ( + inputs + / "morphir-python-process-x86_64-pc-windows-msvc/.dev/qualification/windows-process-imports.json" + ) + proof = json.loads(windows.read_text()) + if change == "windows-missing": + windows.unlink() + else: + if change == "windows-runtime": + proof["dependencies"].append("VCRUNTIME140.dll") + elif change == "windows-bytes": + proof["artifact"]["sha256"] = "c" * 64 + elif change == "windows-checker": + proof["checkerSha256"] = "c" * 64 + windows.write_text(json.dumps(proof)) + elif change == "missing": receipt_path.unlink() elif change == "extra": (inputs / "morphir-python-process-unknown").mkdir() From 981d1825d987007d82ce8b2b787c2361f1f7ac67 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:40:27 -0500 Subject: [PATCH 12/26] test(extension): qualify Rust host runtimes --- .gitattributes | 4 + .github/workflows/release.yml | 35 +- docs/python-extension.md | 8 + .../add-process-mep-extension/design.html | 12 +- .../add-process-mep-extension/tasks.md | 11 +- scripts/benchmark_extension.py | 226 + tests/tools/test_extension_benchmark.py | 33 + tools/extension-qualification/Cargo.lock | 5309 +++++++++++++++++ tools/extension-qualification/Cargo.toml | 22 + tools/extension-qualification/README.md | 66 + tools/extension-qualification/build.rs | 17 + tools/extension-qualification/src/main.rs | 383 ++ 12 files changed, 6114 insertions(+), 12 deletions(-) create mode 100644 scripts/benchmark_extension.py create mode 100644 tests/tools/test_extension_benchmark.py create mode 100644 tools/extension-qualification/Cargo.lock create mode 100644 tools/extension-qualification/Cargo.toml create mode 100644 tools/extension-qualification/README.md create mode 100644 tools/extension-qualification/build.rs create mode 100644 tools/extension-qualification/src/main.rs diff --git a/.gitattributes b/.gitattributes index 4c1e09f2..f061ce6a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,7 @@ rust/Cargo.lock text eol=lf scripts/*.py text eol=lf .github/scripts/*.ps1 text eol=lf +tools/extension-qualification/Cargo.lock text eol=lf +tools/extension-qualification/Cargo.toml text eol=lf +tools/extension-qualification/*.rs text eol=lf +tools/extension-qualification/src/*.rs text eol=lf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14d50b98..ccfc0aec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -328,6 +328,38 @@ jobs: --process dist/process-release \ --report dist/runtime-choice.json + # Source-pinned host assurance belongs to the existing manual/release + # candidate consumer. Build once; exercise the downloaded guest bytes. + - name: Set up the pinned Rust host toolchain + uses: jdx/mise-action@v3 + with: + install_args: rust python + + - name: Cache the independent Rust host build + uses: Swatinem/rust-cache@v2 + with: + workspaces: tools/extension-qualification -> ../../.dev/host-qualification-target + + - name: Build and check the source-pinned Rust host qualifier + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/.dev/host-qualification-target + run: | + manifest=tools/extension-qualification/Cargo.toml + cargo fmt --manifest-path "$manifest" --check + cargo clippy --locked --release --manifest-path "$manifest" --all-targets --no-deps -- -D warnings + cargo test --locked --release --manifest-path "$manifest" + cargo build --locked --release --manifest-path "$manifest" + + - name: Qualify real host channels and retain repeated timing samples + run: | + wasm=dist/wasm-check/.morphir/build/extensions/python + version="$(python -c 'from scripts.extension_bundle import crate_version; print(crate_version())')" + python -m scripts.benchmark_extension \ + --checker .dev/host-qualification-target/release/morphir-python-host-qualification \ + --wasm "$wasm/morphir-python-binding-$version.wasm" \ + --process "dist/process-release/morphir-python-extension-$version-x86_64-unknown-linux-gnu" \ + --output dist/host-qualification --samples 5 + - name: Upload the process-only candidate and its evidence uses: actions/upload-artifact@v6 with: @@ -336,6 +368,7 @@ jobs: dist/process-release/ dist/process-assembly.json dist/runtime-choice.json + dist/host-qualification/ dist/process-inputs/*/.dev/qualification/*.json include-hidden-files: true if-no-files-found: error @@ -371,7 +404,7 @@ jobs: extension-candidate: name: Verify the extension bundle candidate - needs: [qualify, native-wheels] + needs: [qualify, native-wheels, process-candidates] if: startsWith(github.ref, 'refs/tags/extension/python/v') runs-on: ubuntu-latest steps: diff --git a/docs/python-extension.md b/docs/python-extension.md index 42b3588b..4ac41116 100644 --- a/docs/python-extension.md +++ b/docs/python-extension.md @@ -154,6 +154,14 @@ Installation restores executable permissions, so the Morphir host can launch a downloaded raw executable without a user Rust toolchain or Python interpreter. A process has ordinary OS rights; WASM uses the host's Extism isolation. +The developer [Rust host checker](https://github.com/finos/morphir-python/tree/main/tools/extension-qualification) +pins the upstream host source and tests actual WASM/process bytes independently +of the installation recipe. It verifies legacy v3/v4 operations, diagnostic +parity, orderly shutdown and process timeout cleanup. Its timing runner retains +fresh-guest startup and compile/generate samples for small and larger inputs, +including the machine, build profile, input and artifact identities. These +observations do not establish newer-draft conformance or a general speed claim. + The release-assembly check runs this switch in both directions with fresh project caches, compares v3/v4 IR and generated-file digests, rejects an actual foreign-only bundle, and checks that changing installed bytes causes a digest diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 5bf07250..5316a741 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -26,7 +26,7 @@

add-process-mep-extension

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The published SDK 0.3.0 exposes NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type still uses a numeric u64 ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The Python implementation does not opt into model transport and its manifest declares MEP 0.1 only. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

-

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. Crate-tag publication approval is pending; the SDK is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

+

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. Crate-tag publication approval is pending; the SDK is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with canonical empty arguments selecting stdio by default. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. Approval is pending for a separate upstream Python JSON profile; no existing MoonBit check will be relaxed and no unit-test result substitutes for executable qualification.

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. Protocol discovery remains legacy-only until the published SDK prerequisite and candidate-bound conformance pass. All five task groups remain incomplete.

Build and release assessment

@@ -36,17 +36,17 @@

add-process-mep-extension

Platform-specific executable or wheel failure, before promotionExisting five-platform release matrix, explicit manual qualification and extension tag eventsNative target and compiler/toolchain identity, exact digest/claims/source receipts and installed host comparisons. Missing, failed or skipped selected producers block extension publication through its needs graph. New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteThe Python MCK profile and published SDK 0.4.0 remain pending. Platform packaging does not establish draft qualification. Promotion assurance must consume and recheck the candidate-bound executable MCK reports before the change is complete. -

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Maturin builds the CI wheel from an isolated source archive, so a hook alone did not share registry dependency objects. Run 40672914229 showed two compilations: 3m07s for the process guest and 2m57s for the archive wheel; the whole producer took 7m07s, compared with 2m57s for earlier job 122760321213. An explicit Cargo target directory now allows compatible registry objects to be reused across those source paths while retaining the independent archive build. Its measured effect is pending a replacement run. Local path-dependent compiler objects may still rebuild because their source identity differs. macOS and Windows use the same native target and deployment configuration as their wheel producer. Cached CLI bytes are checked against the digest-verified archive before execution. Host receipts retain normalized IR and generated artifact digests, actual CLI and qualification-script identity. Byte-bound lockfiles and script sources use LF on every checkout platform. Artifact ZIP transfer restores only the declared executable mode; payload digests and the original producer mode remain checked. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

+

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Maturin builds the CI wheel from an isolated source archive, so a hook alone did not share registry dependency objects. Run 40672914229 showed two compilations: 3m07s for the process guest and 2m57s for the archive wheel; the whole producer took 7m07s, compared with 2m57s for earlier job 122760321213. An explicit Cargo target directory now allows compatible registry objects to be reused across those source paths while retaining the independent archive build. The replacement b38e00e Linux producer took 5m01s versus 7m07s, and the two reported compile durations changed from 188/194 seconds to 188/58 seconds. This single observation does not establish a universal speedup. Local path-dependent compiler objects may still rebuild because their source identity differs. macOS and Windows use the same native target and deployment configuration as their wheel producer. Cached CLI bytes are checked against the digest-verified archive before execution. Host receipts retain normalized IR and generated artifact digests, actual CLI and qualification-script identity. Byte-bound lockfiles and script sources use LF on every checkout platform. Artifact ZIP transfer restores only the declared executable mode; payload digests and the original producer mode remain checked. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

- + - +
CriterionWeightCurrent assessment
Risk and coverage fit253: focused local refusal tests and installed macOS legacy evidence pass; matrix results pending.
Risk and coverage fit253: focused refusals, installed macOS checks and the b38e00e five-platform matrix pass; replacement source 820c9d0 also passes Windows static-CRT and installed-integrity proof.
Feedback and placement203: one Linux PR path; broader work stays manual/release. Added CI latency not yet measured.
Release evidence and trust20U: current-draft MCK prerequisite and final promotion assembly pending.
Execution efficiency15U: shared producer/output design inspected, actual CI reuse and elapsed time pending measurement.
Execution efficiency153: compatible target reuse reduced one observed Linux producer from 7m07s to 5m01s; platform-specific CRT objects remain separate.
Ownership and structure104: existing bundle and native-wheel categories, explicit needs, repository maintainers own failures.
Operability and recovery102: candidate refusal tests pass; platform rerun/transfer and final promotion refusals pending.
-

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. The new Windows static-CRT producer and its dependency proof still need a replacement candidate run.

Total withheld while 35 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

-
+

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. Replacement source 820c9d0 passed all five producers and assembly in run 40685569455. Its Windows dependency proof admits only OS DLLs, with no VC or Python runtime imports. All eleven combined release files, the five producer receipts and the Windows checker proof were independently matched to the collected assembly hashes. Runtime switching, installed-byte tampering and foreign-platform refusal passed on the assembled Linux candidate.

Total withheld while 20 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

+

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

Repository evidence and references

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 6520b064..18172f96 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -23,17 +23,18 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## 4. Distribute both runtime choices (morphir-5hxy.4) - [ ] 4.1 Build process artifacts for each advertised platform and retain the WASM artifact; verify both use the shared compiler and produce matching supported results and diagnostics. -- [ ] 4.2 Stage canonical runtime/platform/mode/argument/digest/claim descriptors and source receipts with pinned tooling; verify host readers, installed-byte integrity, offline activation and explicit unsupported-platform refusal. +- [x] 4.2 Stage canonical runtime/platform/mode/argument/digest/claim descriptors and source receipts with pinned tooling; verify host readers, installed-byte integrity, offline activation and explicit unsupported-platform refusal. - [ ] 4.3 Add release assurance and runtime-selection documentation alongside each artifact producer, including exact native/CLI pairing where affected; verify missing or mismatched evidence blocks promotion and historical WASM assets remain available. ## 5. Qualify host interoperability (morphir-5hxy.5) - [ ] 5.1 Run the pinned executable MEP guest suite for both runtimes and protocol offers, including supported model operations and negative admission; retain source/artifact-bound conformance receipts. -- [ ] 5.2 Qualify the concrete Rust host process and WASM routes using a pinned compatible release or source build; verify initialization, v3/v4 operations, parity, timeout and child cleanup with a clean child environment. -- [ ] 5.3 Test documented MoonBit routes independently, record prerequisite upstream work for any unsupported path and publish only evidenced compatibility claims; verify documentation recipes against the specified host revisions. -- [ ] 5.4 Measure representative cold-start and compile/generate workloads for process and WASM without making an unmeasured speed claim; retain the reproducible inputs, toolchain and timing report. +- [x] 5.2 Qualify the concrete Rust host process and WASM routes using a pinned compatible release or source build; verify initialization, v3/v4 operations, parity, timeout and child cleanup with a clean child environment. +- [x] 5.3 Test documented MoonBit routes independently, record prerequisite upstream work for any unsupported path and publish only evidenced compatibility claims; verify documentation recipes against the specified host revisions. +- [x] 5.4 Measure representative cold-start and compile/generate workloads for process and WASM without making an unmeasured speed claim; retain the reproducible inputs, toolchain and timing report. ## Workflow follow-up - Implementation is in progress under the active apply goal. SDK publication and a separate upstream Python MCK profile decision are pending; the new draft remains unadvertised. -- Merge, tags and publication require maintainer approval; sync/archive only after implementation and qualification are complete. +- Source-pinned legacy Rust host checks pass for both actual b38e00e runtime artifacts, including v3/v4 parity, timeout and cleanup. Five release-host timing samples per workload retain exact inputs and raw reports in `.dev/qualification/timings-b38-release/`; the manual/release candidate job owns reruns and artifact retention. MoonBit remains unsupported at the documented pin, with explicit v3/v4 envelope refusals reverified by run 40685569455. These checks do not substitute for task 5.1's canonical executable conformance. +- All merges are approved. Tags and publication still require maintainer approval; sync/archive only after implementation and qualification are complete. diff --git a/scripts/benchmark_extension.py b/scripts/benchmark_extension.py new file mode 100644 index 00000000..9f8d0e74 --- /dev/null +++ b/scripts/benchmark_extension.py @@ -0,0 +1,226 @@ +"""Measure fresh guests and ordered v3/v4 operations using the real Rust host. + +The executable checker verifies outputs and diagnostics before accepting a sample. +This is a local performance observation, not canonical MEP conformance evidence. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import platform +import statistics +import subprocess +import time +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] + + +def sha(path: Path) -> str: + """Hash retained input, checker, artifact or result bytes.""" + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def sources(records: int, functions: int) -> tuple[str, str]: + """Create frozen records and typed conditional functions deterministically.""" + + def suffix(index: int) -> str: + # Classic v3 word arrays cannot preserve numeric identifier parts. + result = "" + while True: + result = chr(ord("a") + index % 26) + result + index = index // 26 - 1 + if index < 0: + return result + + types = "from dataclasses import dataclass\n" + "".join( + f"\n@dataclass(frozen=True)\nclass Item{suffix(index).capitalize()}:\n" + " value: int\n" + for index in range(records) + ) + values = types + "".join( + f"\ndef choose_{suffix(index)}(flag: bool, first: int, second: int) -> int:\n" + " return first if flag else second\n" + for index in range(functions) + ) + return types, values + + +def summarize(observations: dict[str, list[int]]) -> dict[str, Any]: + """Retain raw nanoseconds with descriptive statistics and no speed claim.""" + return { + name: { + "samplesNs": values, + "minNs": min(values), + "medianNs": statistics.median(values), + "maxNs": max(values), + } + for name, values in observations.items() + } + + +def benchmark( + checker: Path, wasm: Path, process: Path, output: Path, samples: int +) -> None: + """Run fresh checker processes, verify identities, and save all raw results.""" + checker, wasm, process = ( + path.resolve(strict=True) for path in (checker, wasm, process) + ) + output.mkdir(parents=True, exist_ok=False) + expected = {"wasmSha256": sha(wasm), "processSha256": sha(process)} + definitions = { + name: sha(ROOT / "tools/extension-qualification" / name) + for name in ("Cargo.toml", "Cargo.lock", "build.rs", "src/main.rs") + } + checker_digest = sha(checker) + host_metadata: dict[str, Any] = {} + reports: list[dict[str, Any]] = [] + measurements: dict[str, list[int]] = {} + for name, record_count, function_count in (("small", 1, 1), ("medium", 200, 100)): + directory = output / name + directory.mkdir() + types, values = sources(record_count, function_count) + type_path, value_path = directory / "types.py", directory / "values.py" + type_path.write_text(types) + value_path.write_text(values) + inputs = { + "typesOnlySha256": sha(type_path), + "typesAndValuesSha256": sha(value_path), + } + for index in range(samples): + report = directory / f"sample-{index}.json" + started = time.perf_counter_ns() + result = subprocess.run( + [ + str(checker), + str(wasm), + str(process), + str(report), + str(type_path), + str(value_path), + ], + capture_output=True, + text=True, + timeout=120, + ) + elapsed = time.perf_counter_ns() - started + (directory / f"sample-{index}.log").write_text( + result.stdout + result.stderr + ) + if result.returncode: + raise RuntimeError(f"Checker failed: {result.stderr}") + raw = json.loads(report.read_text()) + if ( + raw.get("scope") != "source-pinned-legacy-host-interoperability" + or raw.get("checkerBinarySha256") != checker_digest + or raw.get("inputs") != inputs + or raw.get("definitionInputs") != definitions + or any(raw.get(key) != value for key, value in expected.items()) + ): + raise ValueError( + "Checker report does not match the measured inputs/bytes" + ) + observed_host = {key: raw[key] for key in ("hostSource", "rustc", "build")} + if host_metadata and host_metadata != observed_host: + raise ValueError( + "Measured host or build settings changed between samples" + ) + host_metadata = observed_host + measurements.setdefault(f"{name}/checker-wall", []).append(elapsed) + for runtime in raw["results"]: + prefix = f"{name}/{runtime['runtime']}" + measurements.setdefault(prefix + "/fresh-guest-initialize", []).append( + runtime["startupNs"] + ) + for operation in runtime["operations"]: + kind = "types" if operation["typesOnly"] else "values" + key = f"{prefix}/v{operation['irVersion']}/{kind}" + for phase in ("compile", "generate"): + measurements.setdefault(key + "/" + phase, []).append( + operation[phase + "Ns"] + ) + reports.append( + { + "path": report.relative_to(output).as_posix(), + "sha256": sha(report), + "stdout": result.stdout, + } + ) + if sha(checker) != checker_digest or any( + sha(path) != expected[key] + for path, key in ((wasm, "wasmSha256"), (process, "processSha256")) + ): + raise ValueError("Measured checker or artifact changed during execution") + summary = { + "scope": "local-legacy-runtime-timings", + "state": "succeeded", + "samplesPerWorkload": samples, + "method": { + "host": "Fresh checker process per sample; WASM then process, sequentially", + "startup": ( + "Fresh guest creation and MEP initialization; " + "WASM includes module compilation" + ), + "operations": ( + "One session per runtime: v3 types, v3 values, v4 types, v4 values; " + "no guest restart between operations" + ), + "cache": ( + "OS filesystem caches are not cleared; " + "no persistent Wasmtime module cache is configured" + ), + "validation": ( + "Every sample checks compile/generate/recompile semantics, runtime " + "diagnostic parity, orderly shutdown and process timeout cleanup" + ), + "limits": ( + "One machine/toolchain and fixed runtime order; " + "descriptive measurements only, not a general speed claim" + ), + }, + "environment": { + "platform": platform.platform(), + "machine": platform.machine(), + "processor": platform.processor(), + "logicalCpus": os.cpu_count(), + "python": platform.python_version(), + }, + "checkerBinarySha256": checker_digest, + **host_metadata, + "artifacts": expected, + "definitionInputs": { + name: sha(ROOT / name) + for name in ( + "scripts/benchmark_extension.py", + "tools/extension-qualification/Cargo.toml", + "tools/extension-qualification/Cargo.lock", + "tools/extension-qualification/build.rs", + "tools/extension-qualification/src/main.rs", + ) + }, + "reports": reports, + "measurements": summarize(measurements), + } + (output / "timings.json").write_text(json.dumps(summary, indent=2) + "\n") + print( + f"Measured {samples} samples of each workload; report {output / 'timings.json'}" + ) + + +def main() -> None: + """Accept exact built artifacts and a new destination for retained evidence.""" + parser = argparse.ArgumentParser(description=__doc__) + for name in ("checker", "wasm", "process", "output"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--samples", type=int, default=5, choices=range(3, 31)) + args = parser.parse_args() + benchmark(args.checker, args.wasm, args.process, args.output, args.samples) + + +if __name__ == "__main__": + main() diff --git a/tests/tools/test_extension_benchmark.py b/tests/tools/test_extension_benchmark.py new file mode 100644 index 00000000..00abb49d --- /dev/null +++ b/tests/tools/test_extension_benchmark.py @@ -0,0 +1,33 @@ +"""Keep timing fixtures within the supported Python/IR roundtrip subset.""" + +from __future__ import annotations + +import pytest + +from morphir_tools import python as api +from scripts.benchmark_extension import sources + + +@pytest.mark.parametrize("version", tuple(api.IrVersion)) +@pytest.mark.parametrize("records,functions", [(1, 1), (200, 100)]) +@pytest.mark.parametrize("with_values", [False, True]) +def test_benchmark_fixtures_roundtrip_in_both_ir_versions( + version: api.IrVersion, records: int, functions: int, with_values: bool +) -> None: + """Numeric suffixes previously caused PY003 in classic v3 word arrays.""" + types, values = sources(records, functions) + compiled = api.compile_sources( + [api.SourceDocument("models.py", values if with_values else types)], + package="acme", + ir_version=version, + ) + assert isinstance(compiled, (api.CompiledV3, api.CompiledV4)), compiled + generated = api.generate_python(compiled.distribution) + assert isinstance(generated, api.Generated), generated + again = api.compile_sources( + [api.SourceDocument(item.path, item.text) for item in generated.artifacts], + package="acme", + ir_version=version, + ) + assert isinstance(again, (api.CompiledV3, api.CompiledV4)), again + assert again.distribution == compiled.distribution diff --git a/tools/extension-qualification/Cargo.lock b/tools/extension-qualification/Cargo.lock new file mode 100644 index 00000000..8bbfc1aa --- /dev/null +++ b/tools/extension-qualification/Cargo.lock @@ -0,0 +1,5309 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.1", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object 0.39.1", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint 0.4.8", + "num-integer", + "num-traits", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "cap-fs-ext" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476f0d0003a760918ed4b1e039a59e11769030416f79c8222551d22785f7f70d" +dependencies = [ + "cap-primitives 3.4.6", + "cap-std 3.4.6", + "io-lifetimes 2.0.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-fs-ext" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ff379b70af8e08307a8f65e7040c7301cb4a572538ade16b4984f0da77847f" +dependencies = [ + "cap-primitives 4.0.3", + "cap-std 4.0.3", + "io-lifetimes 3.0.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "cap-primitives" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0bf07d379916947be6c4a07f43684153d710a2896c31f9e97781362895596c" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras 0.18.4", + "io-lifetimes 2.0.4", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras 0.19.0", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ec6a5b75f54547c579a6b117c6fdd5f04f4ab7598de747b9f440a53592b3a4a" +dependencies = [ + "ambient-authority", + "rand 0.8.8", +] + +[[package]] +name = "cap-std" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59e59fa26472d29680ece6a9f8ee8b0551a719a33df2f5240bde065ecbddfd7" +dependencies = [ + "cap-primitives 3.4.6", + "io-extras 0.18.4", + "io-lifetimes 2.0.4", + "rustix 1.1.4", +] + +[[package]] +name = "cap-std" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8" +dependencies = [ + "cap-primitives 4.0.3", + "io-extras 0.19.0", + "io-lifetimes 3.0.1", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54c289326c70f1c697ebf0a31842a480932e5942b5fac92fcc46e87286b48e2" +dependencies = [ + "ambient-authority", + "cap-primitives 3.4.6", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cbindgen" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20" +dependencies = [ + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_str" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc822414b18d1f5b1b33ce1441534e311e62fef86ebb5b9d382af857d0272c9" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c646808b06f4532478d8d6057d74f15c3322f10d995d9486e7dcea405bf521a" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73d1e372730b5f64ed1a2bd9f01fe4686c8ec14a28034e3084e530c8d951878" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.16.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0319c18165e93dc1ebf78946a8da0b1c341c95b4a39729a69574671639bdb5f" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9195cd8aeecb55e401aa96b2eaa55921636e8246c127ed7908f7ef7e0d40f270" + +[[package]] +name = "cranelift-control" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8976c2154b74136322befc74222ab5c7249edd7e2604f8cbef2b94975541ffb9" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbd294abe236e23cc3d907b0936226b6a8342db7636daa9c7c72be1e323420e" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a90b6ed3aba84189352a87badeb93b2126d3724225a42dc67fdce53d1b139c" + +[[package]] +name = "cranelift-native" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ec0cc1a54e22925eacf4fc3dc815f907734d3b377899d19d52bec04863e853" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948865622f87f30907bb46fbb081b235ae63c1896a99a83c26a003305c1fa82d" + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "delegate" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e018fccbeeb50ff26562ece792ed06659b9c2dae79ece77c4456bb10d9bf79b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", + "zeroize", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "extism" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b66cd9ac5c64b49c9bac69db3d1b10d8f9386e7caab73489c2f197ba43d5e05" +dependencies = [ + "anyhow", + "async-trait", + "cbindgen", + "extism-convert", + "extism-manifest", + "glob", + "libc", + "serde", + "serde_json", + "sha2 0.10.9", + "toml 0.9.12+spec-1.1.0", + "tracing", + "tracing-subscriber", + "ureq", + "url", + "uuid", + "wasi-common", + "wasmtime", + "wiggle", +] + +[[package]] +name = "extism-convert" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19858c4c462309a8f3a20abec53e8603bda1eefda26c8bfab51d5516b40cbb" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytemuck", + "extism-convert-macros", + "prost", + "rmp-serde", + "serde", + "serde_json", +] + +[[package]] +name = "extism-convert-macros" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2932799f6d9f9646f97b65287f6bb2addc75a0ee61e40fb24559a7540dd928" +dependencies = [ + "manyhow", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "extism-manifest" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f59c8dadb5e0bde9a48c6ed45312e6ef625cbcd5f67c28459dbc8fe8bc0383" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f" +dependencies = [ + "anyhow", + "base64 0.22.1", + "extism-convert", + "extism-manifest", + "extism-pdk-derive", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags 2.13.1", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ice_code" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6524844f553e8da5999f3000cf11d3f1ff926bb03fc087441c7b86dee4a7d48" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes 2.0.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ion-rs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "285b4429808446f30da668d210beaeb7ab82defb6045e261c6cdfaeafe565552" +dependencies = [ + "arrayvec", + "base64 0.12.3", + "bumpalo", + "chrono", + "compact_str", + "delegate", + "generic-array", + "ice_code", + "memchr", + "num-bigint 0.4.8", + "num-integer", + "num-traits", + "phf", + "rustc-hash", + "smallvec", + "thiserror 1.0.69", + "visibility", + "winnow 0.6.24", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jupyter-protocol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670197bfdc3b171334f450d9a3ae47999e8aec048579cc3c52f752cc8ff59843" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures", + "serde", + "serde_json", + "thiserror 2.0.21", + "uuid", +] + +[[package]] +name = "lasso" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb" +dependencies = [ + "dashmap", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rust2" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" +dependencies = [ + "sha2 0.11.0", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "morphir-common" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "anyhow", + "cap-fs-ext 4.0.3", + "cap-std 4.0.3", + "dirs", + "flate2", + "fs2", + "glob", + "indexmap", + "ion-rs", + "libc", + "morphir-config", + "morphir-core", + "morphir-workspace", + "nbformat", + "reqwest", + "rustix 1.1.4", + "same-file", + "secrecy", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "stacker", + "tar", + "tempfile", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", + "tracing", + "unicode-casefold", + "unicode-normalization", + "uuid", + "vfs", + "windows-sys 0.61.2", + "zip", +] + +[[package]] +name = "morphir-config" +version = "0.0.1" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "secrecy", + "serde", + "serde-saphyr", + "serde_json", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", +] + +[[package]] +name = "morphir-core" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "anyhow", + "bigdecimal", + "granit-parser", + "indexmap", + "lasso", + "morphir-json", + "num-bigint 0.5.1", + "schemars", + "serde", + "serde_json", + "sha2 0.11.0", + "stacker", + "thiserror 2.0.21", +] + +[[package]] +name = "morphir-distribution" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "flate2", + "fs2", + "morphir-common", + "morphir-extension-sdk", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "tar", + "tempfile", + "thiserror 2.0.21", + "tough", + "tracing", + "unicode-casefold", + "unicode-normalization", + "url", + "zip", +] + +[[package]] +name = "morphir-extension-sdk" +version = "0.4.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "base64 0.22.1", + "extism-pdk", + "ion-rs", + "morphir-json", + "morphir-workspace", + "percent-encoding", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "morphir-host" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "async-lock", + "async-trait", + "morphir-core", + "morphir-extension-sdk", + "morphir-workspace", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "morphir-host-native" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "async-trait", + "base64 0.23.1", + "extism", + "morphir-common", + "morphir-core", + "morphir-distribution", + "morphir-extension-sdk", + "morphir-host", + "morphir-workspace", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.21", + "tokio", + "tracing", + "unicode-casefold", + "unicode-normalization", +] + +[[package]] +name = "morphir-json" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "morphir-python-host-qualification" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "hex", + "morphir-extension-sdk", + "morphir-host", + "morphir-host-native", + "serde_json", + "sha2 0.11.0", + "tempfile", + "tokio", +] + +[[package]] +name = "morphir-workspace" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "globset", + "morphir-config", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "nbformat" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c06543f305caee515923b125a737c4d58079c063e25701f93f45077bb8d622" +dependencies = [ + "anyhow", + "chrono", + "jupyter-protocol", + "serde", + "serde_json", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "hashbrown 0.16.1", + "indexmap", + "memchr", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "olpc-cjson" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" +dependencies = [ + "serde", + "serde_json", + "unicode-normalization", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.8", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pulley-interpreter" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec12fe19a9588315a49fe5704502a9c02d6a198303314b0c7c86123b06d29e5" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f7d5ef31ebf1b46cd7e722ffef934e670d7e462f49aa01cde07b9b76dca580" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.6", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "futures-core", + "pin-project", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags 2.13.1", + "cap-fs-ext 3.4.6", + "cap-std 3.4.6", + "fd-lock", + "io-lifetimes 2.0.4", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tough" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b378d98765c2ae9cdc3e9963ea7e670da8cdd9ee39611b8d722083c7f1ac11" +dependencies = [ + "async-recursion", + "async-trait", + "aws-lc-rs", + "bytes", + "dyn-clone", + "futures", + "futures-core", + "globset", + "hex", + "jiff", + "log", + "olpc-cjson", + "pem", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "serde_plain", + "snafu", + "tempfile", + "tokio", + "tokio-util", + "typed-path 0.9.3", + "untrusted 0.7.1", + "url", + "walkdir", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-path" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82205ffd44a9697e34fc145491aa47310f9871540bb7909eaa9365e0a9a46607" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-casefold" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f66b1c8f8caa2ab31dc6d3f35386f16efdab89668f93411e565ac368908e8f" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +dependencies = [ + "base64 0.23.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vfs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d33cbe2ac48f2a446f04c33aef4906078ffd224888df5ae60b7ed401ded771" +dependencies = [ + "filetime", +] + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi-common" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46137f5bcc41a0f002ed14688e463665388a6f3a6662a12a8c315d4b8849791c" +dependencies = [ + "async-trait", + "bitflags 2.13.1", + "cap-fs-ext 3.4.6", + "cap-rand", + "cap-std 3.4.6", + "cap-time-ext", + "fs-set-times", + "io-extras 0.18.4", + "io-lifetimes 2.0.4", + "log", + "rustix 1.1.4", + "system-interface", + "thiserror 2.0.21", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" +dependencies = [ + "anyhow", + "heck", + "im-rc", + "indexmap", + "log", + "petgraph", + "serde", + "serde_derive", + "serde_yaml", + "smallvec", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" +dependencies = [ + "leb128fmt", + "wasmparser 0.259.0", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.16.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" +dependencies = [ + "bitflags 2.13.1", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasmtime" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf" +dependencies = [ + "addr2line", + "async-trait", + "bitflags 2.13.1", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object 0.38.1", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.16.1", + "indexmap", + "log", + "object 0.38.1", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed398988226d7aa0505ac6bb576e09532ad722d702ec4e66365d78ed695c95f" +dependencies = [ + "base64 0.22.1", + "directories-next", + "log", + "postcard", + "rustix 1.1.4", + "serde", + "serde_derive", + "sha2 0.10.9", + "toml 0.9.12+spec-1.1.0", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae5ec9fff073ff13b81732d56a9515d761c245750bcda09093827f84130ebc25" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935d9ab293ba27d1ec9aa7bc1b3a43993dbe961af2a8f23f90a11e1331b4c13f" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1679d205caf9766c6aa309d45bb3e7c634d7725e3164404df33824b9f7c4fb7" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object 0.38.1", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.21", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e505254058be5b0df458d670ee42d9eafe2349d04c1296e9dc01071dc20a85" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2e05b345f1773e59c20e6ad7298fd6857cdea245023d88bb659c96d8f0ea72" +dependencies = [ + "cc", + "object 0.38.1", + "rustix 1.1.4", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86701b234a4643e3f111869aa792b3a05a06e02d486ee9cb6c04dae16b52dab" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63558d801beb83dde9b336eb4ae049019aee26627926edb32cd119d7e4c83cd" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object 0.38.1", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737c4d956fc3a848541a064afb683dd2771132a6b125be5baaf95c4379aa47df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f599b79545e3bba0b7913406055ebede5bb0dabee9ba2015ef25a9f4c9f47807" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object 0.38.1", + "target-lexicon", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2192a77a00b9a67800c2b4e1c70fb6abca79d6b529e53a2ef9dcdcc36090330d" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "heck", + "indexmap", + "wit-parser", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "259.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c69beba8d9da07af9a0971b559149a2ac4729895b55144de4fddbf5a02660648" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.259.0", +] + +[[package]] +name = "wat" +version = "1.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6eec44b0c80391b20fb7ad9ea440c72c5d382385bdf74444bb18321c425017e" +dependencies = [ + "wast 259.0.0", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wiggle" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8cfd3db2f05619c6f36f257d84327c11546e28d61e3a1c1220aaad553bc4b0" +dependencies = [ + "bitflags 2.13.1", + "thiserror 2.0.21", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", + "witx", +] + +[[package]] +name = "wiggle-generate" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd7a197903e5b4ff5e13aef9c891960d71e92073600ecf4c86c7e795ac1c803" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6410b86fcec207070d9372b215d3470bad67215e6bbac46981a16999c4abbc28" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "wiggle-generate", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winch-codegen" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dbb0cf07b0dfe7b7a1ca8efb8f94ba98bd0fb144c411ea1665c78f0449e958" +dependencies = [ + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.21", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.13.1", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-parser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.245.1", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "aes", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.4.3", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "typed-path 0.12.3", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/tools/extension-qualification/Cargo.toml b/tools/extension-qualification/Cargo.toml new file mode 100644 index 00000000..e4668d10 --- /dev/null +++ b/tools/extension-qualification/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "morphir-python-host-qualification" +version = "0.1.0" +edition = "2024" +rust-version = "1.98" +license = "Apache-2.0" +publish = false + +# Independent developer tooling, not part of the compiler/Python dependency tree. +[workspace] + +[dependencies] +anyhow = "1" +async-trait = "0.1" +hex = "0.4" +morphir-extension-sdk = { git = "https://github.com/finos/morphir-rust.git", rev = "d51daa8aea7ae09ae295ff254d72b27770c64104" } +morphir-host = { git = "https://github.com/finos/morphir-rust.git", rev = "d51daa8aea7ae09ae295ff254d72b27770c64104" } +morphir-host-native = { git = "https://github.com/finos/morphir-rust.git", rev = "d51daa8aea7ae09ae295ff254d72b27770c64104" } +serde_json = { version = "1", features = ["arbitrary_precision"] } +sha2 = "0.11" +tempfile = "3" +tokio = { version = "1", features = ["rt", "macros", "sync", "time"] } diff --git a/tools/extension-qualification/README.md b/tools/extension-qualification/README.md new file mode 100644 index 00000000..25208ca2 --- /dev/null +++ b/tools/extension-qualification/README.md @@ -0,0 +1,66 @@ +# Source-pinned Rust host qualification + +This developer-only executable exercises the actual WASM and native process +guest bytes through `morphir-host` and `morphir-host-native` at +`d51daa8aea7ae09ae295ff254d72b27770c64104`. It uses the upstream Extism and +process channels directly. Process launches have an empty environment and do +not use Python or a Rust launcher. + +The Git dependencies are isolated in this tool's Cargo workspace. The compiler +and Python packages continue to depend on published crates. A source-built host +does not remove the compiler's published SDK prerequisite. + +Build once, then supply the exact candidate artifacts: + +```bash +cargo build --locked --release --manifest-path tools/extension-qualification/Cargo.toml \ + --target-dir .dev/host-qualification-target + +.dev/host-qualification-target/release/morphir-python-host-qualification \ + path/to/guest.wasm path/to/morphir-python-extension \ + .dev/qualification/rust-host.json +``` + +The checker owns temporary copies, verifies their hashes, and restores only +the process copy's executable permission if artifact transfer removed it. It +initializes each runtime using legacy MEP `0.1`, then checks IR v3/v4 compilation, +generation, recompilation, unsupported-source diagnostics, matching results, +and orderly shutdown. The process route must leave no running child, unread +stdout, or stderr. An actual guest request with a 1 ms host deadline must fail +with a timeout and leave the child reaped. The timeout can occur during +initialization or during the intentionally large compile request. + +The JSON result binds the checker binary, artifacts, inputs, Rust toolchain and +host revision. This legacy interoperability check is separate from the pinned +executable MCK guest suite. It cannot establish the newer MEP draft, negotiated +model transport, canonical conformance, installed-byte integrity or platform +coverage. Those have their own required checks. + +## Repeat timing observations + +```bash +python3 -m scripts.benchmark_extension \ + --checker .dev/host-qualification-target/release/morphir-python-host-qualification \ + --wasm path/to/guest.wasm --process path/to/morphir-python-extension \ + --output .dev/qualification/runtime-timings --samples 5 +``` + +The destination must be new. The runner retains both Python inputs, every raw +checker result and log, hashes, the machine description, and nanosecond samples +with min/median/max in `timings.json`. Workloads contain one record/one function +and 200 records/100 typed conditional functions. Alphabetic identifiers fit +both supported IR naming formats. Every sample still verifies semantic and +diagnostic parity and process cleanup. + +Each sample starts a fresh checker process and fresh guests. Guest startup +includes initialization and, for WASM, module compilation. OS caches are not +cleared. Operations run in a fixed session order: v3 types, v3 values, v4 types, +v4 values. Later operations reuse the guest session. WASM runs before process, +so the report records that order and makes no general speed claim. Whole-checker +wall time includes both runtimes, validation, timeout cleanup and report I/O; +it is not a per-runtime startup measurement. + +The checker also accepts two optional source paths after `REPORT`, for the +type-only and type/value cases respectively. The benchmark uses these to keep +the measured inputs reproducible. Inputs must compile and roundtrip in both +advertised IR versions. diff --git a/tools/extension-qualification/build.rs b/tools/extension-qualification/build.rs new file mode 100644 index 00000000..abd49eff --- /dev/null +++ b/tools/extension-qualification/build.rs @@ -0,0 +1,17 @@ +fn main() { + let rustc = std::env::var("RUSTC").expect("Cargo identifies its compiler"); + let output = std::process::Command::new(rustc) + .arg("-vV") + .output() + .expect("read compiler identity"); + assert!(output.status.success()); + let version = String::from_utf8(output.stdout) + .expect("compiler identity is UTF-8") + .replace('\n', " | "); + println!("cargo:rustc-env=QUALIFICATION_RUSTC={version}"); + for variable in ["PROFILE", "OPT_LEVEL", "DEBUG"] { + let value = std::env::var(variable).expect("Cargo identifies its build settings"); + println!("cargo:rustc-env=QUALIFICATION_{variable}={value}"); + } + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/tools/extension-qualification/src/main.rs b/tools/extension-qualification/src/main.rs new file mode 100644 index 00000000..535097d9 --- /dev/null +++ b/tools/extension-qualification/src/main.rs @@ -0,0 +1,383 @@ +//! Independent source-pinned Rust host qualification of actual guest bytes. +//! This verifies legacy interoperability; it is not the canonical MCK guest kit. +use anyhow::{Context, Result, bail, ensure}; +use async_trait::async_trait; +use morphir_extension_sdk::{ + CompileOptions, CompilePackage, CompileRequest, GenerateRequest, SourceDocument, SourceSet, + protocol::{ExtensionResponse, PeerInfo}, +}; +use morphir_host::{ + Channel, ChannelError, ChannelState, ExpectedChecks, ExpectedExtension, HostConfig, + JsonRpcConnection, Outgoing, Session, +}; +use morphir_host_native::{ + CheckedConnection, + extism::{ExtensionContainer, ExtismChannel, MorphirHostFunctions}, + process::{ProcessChannel, ProcessLaunch}, +}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::{ + fs, + io::Read, + path::{Path, PathBuf}, + sync::Arc, + time::{Duration, Instant}, +}; +use tokio::sync::Mutex; + +const HOST_SOURCE: &str = "d51daa8aea7ae09ae295ff254d72b27770c64104"; +const ID: &str = "morphir-python"; +const TYPES_SOURCE: &str = + "from dataclasses import dataclass\n@dataclass(frozen=True)\nclass Box:\n value: int\n"; +const FUNCTION_SOURCE: &str = "\ndef choose(flag: bool, first: int, second: int) -> int:\n return first if flag else second\n"; + +#[derive(Clone)] +struct SharedProcess(Arc>); + +#[async_trait] +impl Channel for SharedProcess { + async fn send(&mut self, message: Outgoing) -> Result<(), ChannelError> { + self.0.lock().await.send(message).await + } + async fn receive(&mut self) -> Result { + self.0.lock().await.receive().await + } + async fn close(&mut self) -> Result { + self.0.lock().await.close().await + } + async fn abort(&mut self) -> Result { + self.0.lock().await.abort().await + } +} + +fn host_config() -> HostConfig { + HostConfig::new(PeerInfo { + name: "morphir-python-qualification".into(), + version: env!("CARGO_PKG_VERSION").into(), + kind: Default::default(), + }) +} + +fn request(version: &str, types_only: bool) -> CompileRequest { + CompileRequest { + language_id: "python".into(), + sources: SourceSet { + root: None, + documents: vec![SourceDocument { + uri: "models.py".into(), + language_id: "python".into(), + version: 1, + text: if types_only { + TYPES_SOURCE.into() + } else { + format!("{TYPES_SOURCE}{FUNCTION_SOURCE}") + }, + }], + }, + package: CompilePackage { + name: "acme".into(), + exposed_modules: None, + }, + dependencies: vec![], + options: CompileOptions { + ir_version: version.into(), + types_only, + ..Default::default() + }, + baseline: None, + } +} + +fn hash_file(path: &Path) -> Result { + let mut file = fs::File::open(path)?; + let mut digest = Sha256::new(); + let mut buffer = [0_u8; 65536]; + loop { + let size = file.read(&mut buffer)?; + if size == 0 { + break; + } + digest.update(&buffer[..size]); + } + Ok(hex::encode(digest.finalize())) +} + +fn canonical_digest(value: &Value) -> Result { + let mut canonical = value.clone(); + canonical.sort_all_objects(); + Ok(hex::encode(Sha256::digest(serde_json::to_vec(&canonical)?))) +} + +async fn process_session( + executable: &Path, + cwd: &Path, + timeout: Duration, +) -> Result<(Session, SharedProcess)> { + // ProcessLaunch inherits no user environment. No Python or Rust launcher. + let launch = ProcessLaunch::new(ID, executable, cwd).request_timeout(timeout); + let channel = ProcessChannel::spawn(launch).await?; + let expected = channel.expectation(); + let shared = SharedProcess(Arc::new(Mutex::new(channel))); + let connection = CheckedConnection::new(JsonRpcConnection::new( + shared.clone(), + ExpectedChecks::new(expected), + )); + let session = Session::open(connection, &host_config()).await?; + Ok((session, shared)) +} + +async fn qualify_runtime( + wasm: &Path, + process: &Path, + runtime: &str, + types_source: &str, + values_source: &str, +) -> Result { + let work = tempfile::tempdir()?; + let started = Instant::now(); + let (mut session, child) = if runtime == "process" { + let (session, child) = + process_session(process, work.path(), Duration::from_secs(30)).await?; + (session, Some(child)) + } else { + let bytes = fs::read(wasm)?; + let container = + ExtensionContainer::from_bytes(ID, &bytes, MorphirHostFunctions::default())?; + let channel = ExtismChannel::new(container, ExpectedExtension::identified(ID)); + let expected = channel.expectation(); + let connection = CheckedConnection::new(JsonRpcConnection::new( + channel, + ExpectedChecks::new(expected), + )); + (Session::open(connection, &host_config()).await?, None) + }; + let startup_ns = started.elapsed().as_nanos(); + ensure!(session.negotiated().protocol_version() == "0.1"); + let mut operations = Vec::new(); + for version in ["3", "4"] { + for types_only in [true, false] { + let compile_start = Instant::now(); + let mut input = request(version, types_only); + input.sources.documents[0].text = if types_only { + types_source.into() + } else { + values_source.into() + }; + let compiled = session.compile(input).await?; + let compile_ns = compile_start.elapsed().as_nanos(); + ensure!(compiled.success, "compile: {:?}", compiled.diagnostics); + let ir = compiled.ir.context("successful compile has IR")?; + let generate_start = Instant::now(); + let generated = session + .generate(GenerateRequest { + ir: ir.clone(), + target: "python".into(), + options: Default::default(), + }) + .await?; + let generate_ns = generate_start.elapsed().as_nanos(); + ensure!(generated.success, "generate: {:?}", generated.diagnostics); + let mut recompile = request(version, types_only); + recompile.sources.documents = generated + .artifacts + .iter() + .map(|artifact| SourceDocument { + uri: artifact.path.clone(), + language_id: "python".into(), + version: 1, + text: artifact.content.clone(), + }) + .collect(); + let again = session.compile(recompile).await?; + ensure!( + again.success && again.ir.as_ref() == Some(&ir), + "generated source changed IR" + ); + let mut invalid = request(version, types_only); + invalid.sources.documents[0].text = "import os\n".into(); + let refused = session.compile(invalid).await?; + ensure!(!refused.success && refused.ir.is_none() && !refused.diagnostics.is_empty()); + operations.push(json!({ + "irVersion":version, "typesOnly":types_only, + "irSha256":canonical_digest(&ir)?, + "artifacts":generated.artifacts, "diagnostics":refused.diagnostics, + "compileNs":compile_ns, "generateNs":generate_ns, + })); + } + } + session.close().await?; + if let Some(child) = child { + let mut channel = child.0.lock().await; + ensure!(!channel.is_running()?, "closed guest is still running"); + ensure!( + channel.stdout_is_exhausted().await?, + "stdout contains unread bytes" + ); + ensure!( + channel.stderr_output().is_empty(), + "unexpected guest diagnostics" + ); + } + Ok( + json!({"runtime":runtime,"protocolVersion":"0.1","startupNs":startup_ns,"operations":operations,"orderlyClose":true}), + ) +} + +async fn timeout_cleanup(process: &Path) -> Result { + let work = tempfile::tempdir()?; + let channel = ProcessChannel::spawn( + ProcessLaunch::new(ID, process, work.path()).request_timeout(Duration::from_millis(1)), + ) + .await?; + let expected = channel.expectation(); + let shared = SharedProcess(Arc::new(Mutex::new(channel))); + let connection = CheckedConnection::new(JsonRpcConnection::new( + shared.clone(), + ExpectedChecks::new(expected), + )); + let (phase, reason) = match Session::open(connection, &host_config()).await { + Err(error) => ("initialize", error.to_string()), + Ok(mut session) => { + let mut large = request("4", true); + let mut source = "from dataclasses import dataclass\n".to_owned(); + for index in 0..20000 { + source.push_str(&format!( + "@dataclass(frozen=True)\nclass Item{index}:\n value: int\n" + )); + } + large.sources.documents[0].text = source; + let error = session + .compile(large) + .await + .expect_err("large request should exceed 1ms deadline"); + ("compile", error.to_string()) + } + }; + ensure!( + reason.contains("timed out") || reason.contains("timeout"), + "{reason}" + ); + ensure!( + !shared.0.lock().await.is_running()?, + "timed-out child was not reaped" + ); + Ok(json!({"deadlineMs":1,"phase":phase,"error":reason,"childRunning":false})) +} + +fn equivalent(first: &Value, second: &Value) -> bool { + let mut first = first.clone(); + let mut second = second.clone(); + for value in [&mut first, &mut second] { + for entry in value.as_array_mut().unwrap() { + entry.as_object_mut().unwrap().remove("compileNs"); + entry.as_object_mut().unwrap().remove("generateNs"); + } + } + first == second +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let mut arguments = std::env::args_os().skip(1); + let wasm = arguments + .next() + .map(PathBuf::from) + .context("usage: qualifier WASM PROCESS REPORT [TYPES_SOURCE VALUES_SOURCE]")?; + let process = arguments + .next() + .map(PathBuf::from) + .context("missing process executable")? + .canonicalize()?; + let report = arguments + .next() + .map(PathBuf::from) + .context("missing report path")?; + let sources = if let Some(types_path) = arguments.next() { + let values_path = arguments.next().context("missing values source path")?; + ( + fs::read_to_string(types_path)?, + fs::read_to_string(values_path)?, + ) + } else { + ( + TYPES_SOURCE.into(), + format!("{TYPES_SOURCE}{FUNCTION_SOURCE}"), + ) + }; + if arguments.next().is_some() { + bail!("unexpected arguments"); + } + let wasm = wasm.canonicalize()?; + let owned = tempfile::tempdir()?; + let wasm_copy = owned.path().join("guest.wasm"); + let process_copy = owned + .path() + .join(process.file_name().context("missing process filename")?); + fs::copy(&wasm, &wasm_copy)?; + fs::copy(&process, &process_copy)?; + ensure!(hash_file(&wasm)? == hash_file(&wasm_copy)?); + ensure!(hash_file(&process)? == hash_file(&process_copy)?); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&process_copy, fs::Permissions::from_mode(0o700))?; + } + let wasm = wasm_copy; + let process = process_copy; + let wasm_result = qualify_runtime(&wasm, &process, "wasm", &sources.0, &sources.1).await?; + let process_result = + qualify_runtime(&wasm, &process, "process", &sources.0, &sources.1).await?; + ensure!( + equivalent(&wasm_result["operations"], &process_result["operations"]), + "runtime results or diagnostics differ" + ); + let timeout = timeout_cleanup(&process).await?; + let result = json!({ + "scope":"source-pinned-legacy-host-interoperability", + "hostSource":HOST_SOURCE, "rustc":env!("QUALIFICATION_RUSTC"), + "build":{ + "profile":env!("QUALIFICATION_PROFILE"), + "optLevel":env!("QUALIFICATION_OPT_LEVEL"), + "debug":env!("QUALIFICATION_DEBUG"), + }, + "definitionInputs":{ + "Cargo.toml":hex::encode(Sha256::digest(include_bytes!("../Cargo.toml"))), + "Cargo.lock":hex::encode(Sha256::digest(include_bytes!("../Cargo.lock"))), + "build.rs":hex::encode(Sha256::digest(include_bytes!("../build.rs"))), + "src/main.rs":hex::encode(Sha256::digest(include_bytes!("main.rs"))), + }, + "checkerBinarySha256":hash_file(&std::env::current_exe()?)?, + "wasmSha256":hash_file(&wasm)?, "processSha256":hash_file(&process)?, + "inputs":{ + "typesOnlySha256":hex::encode(Sha256::digest(sources.0.as_bytes())), + "typesAndValuesSha256":hex::encode(Sha256::digest(sources.1.as_bytes())), + }, + "results":[wasm_result,process_result],"timeout":timeout, + }); + if let Some(parent) = report.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&report, serde_json::to_vec_pretty(&result)?)?; + println!( + "host interoperability qualified; report {}", + report.display() + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parity_ignores_observation_times_but_preserves_diagnostics_and_ir() { + let first = json!([{"compileNs":1,"generateNs":2,"irSha256":"abc","diagnostics":[{"code":"PY004"}]}]); + let second = json!([{"compileNs":3,"generateNs":4,"irSha256":"abc","diagnostics":[{"code":"PY004"}]}]); + assert!(equivalent(&first, &second)); + let different_diagnostic = json!([{"compileNs":3,"generateNs":4,"irSha256":"abc","diagnostics":[{"code":"PY003"}]}]); + assert!(!equivalent(&first, &different_diagnostic)); + let different_ir = json!([{"compileNs":3,"generateNs":4,"irSha256":"def","diagnostics":[{"code":"PY004"}]}]); + assert!(!equivalent(&first, &different_ir)); + } +} From dfdca796d5f5d923973fce48e41a7379b9eb978b Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:57:26 -0500 Subject: [PATCH 13/26] test(extension): require platform diagnostic parity --- .../add-process-mep-extension/tasks.md | 1 + scripts/process_diagnostics.py | 203 ++++++++++++++++++ scripts/process_extension.py | 5 + scripts/process_release.py | 30 +++ tests/unit/test_process_release.py | 51 ++++- 5 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 scripts/process_diagnostics.py diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 18172f96..0882633f 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -36,5 +36,6 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## Workflow follow-up - Implementation is in progress under the active apply goal. SDK publication and a separate upstream Python MCK profile decision are pending; the new draft remains unadvertised. +- Task 4.1 now requires a source/artifact-bound framed diagnostic receipt from every process producer. Actual macOS process bytes match the installed Python API's complete v3/v4 diagnostic records with a clean child environment. Missing, changed, stale or mismatched diagnostic proofs block assembly. The checkbox remains open until the replacement platform run supplies all five receipts. - Source-pinned legacy Rust host checks pass for both actual b38e00e runtime artifacts, including v3/v4 parity, timeout and cleanup. Five release-host timing samples per workload retain exact inputs and raw reports in `.dev/qualification/timings-b38-release/`; the manual/release candidate job owns reruns and artifact retention. MoonBit remains unsupported at the documented pin, with explicit v3/v4 envelope refusals reverified by run 40685569455. These checks do not substitute for task 5.1's canonical executable conformance. - All merges are approved. Tags and publication still require maintainer approval; sync/archive only after implementation and qualification are complete. diff --git a/scripts/process_diagnostics.py b/scripts/process_diagnostics.py new file mode 100644 index 00000000..cf9eaf92 --- /dev/null +++ b/scripts/process_diagnostics.py @@ -0,0 +1,203 @@ +"""Compare framed legacy process diagnostics with the exact installed Python API. + +This producer check supplements installed CLI checks, whose readable errors omit +language diagnostic codes. It is not canonical executable MEP conformance. +""" + +from __future__ import annotations + +import dataclasses +import json +import os +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle + +SOURCE = "import os\n" + + +def _frame(message: dict[str, Any]) -> bytes: + body = json.dumps(message, ensure_ascii=False).encode("utf-8") + return f"Content-Length: {len(body)}\r\n\r\n".encode() + body + + +def _responses(data: bytes) -> list[dict[str, Any]]: + responses: list[dict[str, Any]] = [] + offset = 0 + while offset < len(data): + end = data.find(b"\r\n\r\n", offset, offset + 8192) + if end < 0: + raise bundle.BundleError("process diagnostic stdout has no bounded header") + lengths: list[int] = [] + for line in data[offset:end].split(b"\r\n"): + name, separator, value = line.partition(b":") + if not separator: + raise bundle.BundleError("process diagnostic stdout is not framed") + if name.lower() == b"content-length": + value = value.strip() + if not value.isdigit(): + raise bundle.BundleError( + "process diagnostic frame length is invalid" + ) + lengths.append(int(value)) + if len(lengths) != 1 or not 0 < lengths[0] <= 16 * 1024 * 1024: + raise bundle.BundleError("process diagnostic frame length is not bounded") + offset = end + 4 + body_end = offset + lengths[0] + if body_end > len(data): + raise bundle.BundleError("process diagnostic frame is truncated") + message = json.loads(data[offset:body_end].decode("utf-8")) + if not isinstance(message, dict) or len(responses) == 4: + raise bundle.BundleError( + "process diagnostic stdout has unexpected messages" + ) + responses.append(message) + offset = body_end + return responses + + +def qualify(executable: Path) -> dict[str, Any]: + """Check a real clean-environment session and retain structured parity proof.""" + from morphir_tools.python import ( + CompileFailure, + IrVersion, + SourceDocument, + compile_sources, + native_compiler, + ) + + native = native_compiler() + if native.compiler_version != bundle.crate_version(): + raise bundle.BundleError("diagnostic API compiler does not match candidate") + messages: list[dict[str, Any]] = [ + { + "jsonrpc": "2.0", + "id": 1, + "method": "morphir.initialize", + "params": { + "protocolVersions": ["0.1"], + "host": {"name": "python-diagnostic-qualification", "version": "1"}, + }, + }, + {"jsonrpc": "2.0", "method": "morphir.initialized"}, + ] + expected: dict[str, list[dict[str, Any]]] = {} + for number, version in enumerate(IrVersion, 2): + compiled = compile_sources( + [SourceDocument("models.py", SOURCE)], package="acme", ir_version=version + ) + if not isinstance(compiled, CompileFailure): + raise bundle.BundleError("diagnostic fixture unexpectedly compiled") + expected[version.value] = [ + { + "code": item.code, + "severity": item.severity.value, + "message": item.message, + "location": None + if item.source is None + else { + "uri": item.source.path, + "range": { + "start": dataclasses.asdict(item.source.start), + "end": dataclasses.asdict(item.source.end), + }, + }, + } + for item in compiled.diagnostics + ] + messages.append( + { + "jsonrpc": "2.0", + "id": number, + "method": "morphir.frontend.compile", + "params": { + "languageId": "python", + "sources": { + "documents": [ + { + "uri": "models.py", + "languageId": "python", + "version": 1, + "text": SOURCE, + } + ] + }, + "package": {"name": "acme"}, + "dependencies": [], + "options": {"irVersion": version.value, "typesOnly": False}, + }, + } + ) + messages.extend( + [ + {"jsonrpc": "2.0", "id": 4, "method": "morphir.shutdown", "params": {}}, + {"jsonrpc": "2.0", "method": "morphir.exit"}, + ] + ) + digest = bundle.sha256(executable.read_bytes()) + with tempfile.TemporaryDirectory(prefix="python-process-diagnostics-") as name: + work = Path(name) + owned = work / executable.name + owned.write_bytes(executable.read_bytes()) + if bundle.sha256(owned.read_bytes()) != digest: + raise bundle.BundleError("diagnostic artifact copy differs") + if os.name != "nt": + owned.chmod(0o700) + result = subprocess.run( + [str(owned)], + input=b"".join(map(_frame, messages)), + cwd=work, + env={}, + capture_output=True, + timeout=60, + ) + if result.returncode or result.stderr: + raise bundle.BundleError(f"diagnostic guest failed: {result.stderr!r}") + replies = _responses(result.stdout) + if [item.get("id") for item in replies] != [1, 2, 3, 4] or any( + item.get("jsonrpc") != "2.0" or "error" in item for item in replies + ): + raise bundle.BundleError("diagnostic guest lifecycle/IDs differ") + initialized = replies[0]["result"] + if not ( + initialized["protocolVersion"] == "0.1" + and initialized["extension"]["id"] == "morphir-python" + and initialized["extension"]["version"] == native.compiler_version + ): + raise bundle.BundleError("diagnostic guest identity differs") + cases: dict[str, Any] = {} + for reply, version in zip(replies[1:3], IrVersion, strict=True): + value = reply["result"] + diagnostics = [ + {key: item.get(key) for key in ("code", "severity", "message", "location")} + for item in value["diagnostics"] + ] + if ( + value["success"] is not False + or value.get("ir") is not None + or not diagnostics + or diagnostics != expected[version.value] + ): + raise bundle.BundleError("process and Python API diagnostics differ") + cases[version.value] = { + "diagnostics": diagnostics, + "pythonApi": expected[version.value], + } + if bundle.sha256(executable.read_bytes()) != digest: + raise bundle.BundleError("diagnostic artifact changed during execution") + return { + "scope": "legacy-process-diagnostic-parity", + "artifactSha256": digest, + "checkerSha256": bundle.sha256(Path(__file__).read_bytes()), + "inputSha256": bundle.sha256(SOURCE.encode()), + "compilerVersion": native.compiler_version, + "nativeVersion": native.version, + "protocolVersion": "0.1", + "cleanEnvironment": True, + "exitCode": result.returncode, + "stderr": result.stderr.decode(), + "cases": cases, + } diff --git a/scripts/process_extension.py b/scripts/process_extension.py index 26df824a..5af1ef6b 100644 --- a/scripts/process_extension.py +++ b/scripts/process_extension.py @@ -167,6 +167,11 @@ def main(argv: list[str] | None = None) -> int: result = package(args.executable, args.output, args.commit) if args.host_check: result["installedHost"] = bundle.host_round_trips(args.output, result) + from scripts.process_diagnostics import qualify + + result["processDiagnostics"] = qualify( + args.output / artifact_name(bundle.crate_version(), bundle._target()) + ) args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, indent=2) + "\n") except (bundle.BundleError, OSError, subprocess.CalledProcessError) as error: diff --git a/scripts/process_release.py b/scripts/process_release.py index dc818ee3..cf6fba55 100644 --- a/scripts/process_release.py +++ b/scripts/process_release.py @@ -16,6 +16,7 @@ from typing import Any from scripts import extension_bundle as bundle +from scripts import process_diagnostics from scripts import process_extension as process from scripts.qualify_native import tools_pin from scripts.release_candidates import CandidateError, bundle_candidates @@ -61,6 +62,34 @@ def _windows_imports(root: Path, artifact_digest: str, commit: str) -> str: return bundle.sha256(path.read_bytes()) +def _diagnostics(proof: dict[str, Any], digest: str, version: str) -> None: + if not ( + proof["scope"] == "legacy-process-diagnostic-parity" + and proof["artifactSha256"] == digest + and proof["checkerSha256"] + == bundle.sha256(Path(process_diagnostics.__file__).read_bytes()) + and proof["inputSha256"] == bundle.sha256(process_diagnostics.SOURCE.encode()) + and proof["compilerVersion"] == version + and proof["nativeVersion"] == tools_pin() + and proof["protocolVersion"] == "0.1" + and proof["cleanEnvironment"] is True + and proof["exitCode"] == 0 + and proof["stderr"] == "" + and isinstance(proof["cases"], dict) + and set(proof["cases"]) == {"3", "4"} + ): + raise bundle.BundleError("process diagnostic proof identity differs") + for case in proof["cases"].values(): + diagnostics = case["diagnostics"] + if not ( + diagnostics == case["pythonApi"] + and len(diagnostics) == 1 + and diagnostics[0]["code"] == "PY004" + and diagnostics[0]["severity"] == "error" + ): + raise bundle.BundleError("process diagnostics differ from the Python API") + + def assemble( inputs: Path, output: Path, @@ -103,6 +132,7 @@ def assemble( root, checked["files"][name], commit ) try: + _diagnostics(report["processDiagnostics"], checked["files"][name], version) for field in ( "sourceCommit", "runtime", diff --git a/tests/unit/test_process_release.py b/tests/unit/test_process_release.py index d8d45852..362d5624 100644 --- a/tests/unit/test_process_release.py +++ b/tests/unit/test_process_release.py @@ -9,6 +9,7 @@ import pytest from scripts import extension_bundle as bundle +from scripts import process_diagnostics from scripts import process_extension as process from scripts import process_release as release from scripts.qualify_native import tools_pin @@ -72,6 +73,27 @@ def candidates(root: Path) -> tuple[dict[str, Any], dict[str, Any]]: receipt["cargoLockSha256"] = bundle.sha256( (bundle.ROOT / "rust/Cargo.lock").read_bytes() ) + receipt["processDiagnostics"] = { + "scope": "legacy-process-diagnostic-parity", + "artifactSha256": digest, + "checkerSha256": bundle.sha256( + (bundle.ROOT / "scripts/process_diagnostics.py").read_bytes() + ), + "inputSha256": bundle.sha256(process_diagnostics.SOURCE.encode()), + "compilerVersion": version, + "nativeVersion": tools_pin(), + "protocolVersion": "0.1", + "cleanEnvironment": True, + "exitCode": 0, + "stderr": "", + "cases": { + version: { + "diagnostics": [{"code": "PY004", "severity": "error"}], + "pythonApi": [{"code": "PY004", "severity": "error"}], + } + for version in ("3", "4") + }, + } receipt["installedHost"] = { **copy.deepcopy(receipt), "host": { @@ -162,6 +184,14 @@ def test_complete_process_set_keeps_canonical_per_platform_artifacts( "integrity-not-run", "integrity-cache", "integrity-reason", + "diagnostics-missing", + "diagnostics-code", + "diagnostics-api", + "diagnostics-checker", + "diagnostics-artifact", + "diagnostics-version", + "diagnostics-environment", + "diagnostics-v4", ], ) def test_mixed_missing_or_unqualified_process_sets_are_refused( @@ -173,7 +203,26 @@ def test_mixed_missing_or_unqualified_process_sets_are_refused( platform_root = inputs / f"morphir-python-process-{target}" receipt_path = platform_root / f".dev/qualification/process-{target}.json" receipt = json.loads(receipt_path.read_text()) - if change.startswith("integrity-"): + if change == "diagnostics-missing": + receipt.pop("processDiagnostics", None) + elif change.startswith("diagnostics-"): + proof = receipt["processDiagnostics"] + if change == "diagnostics-code": + proof["cases"]["3"]["diagnostics"][0]["code"] = "PY001" + proof["cases"]["3"]["pythonApi"][0]["code"] = "PY001" + elif change == "diagnostics-api": + proof["cases"]["3"]["pythonApi"][0]["code"] = "PY001" + elif change == "diagnostics-checker": + proof["checkerSha256"] = "c" * 64 + elif change == "diagnostics-artifact": + proof["artifactSha256"] = "c" * 64 + elif change == "diagnostics-version": + proof["compilerVersion"] = "0.0.0" + elif change == "diagnostics-environment": + proof["cleanEnvironment"] = False + elif change == "diagnostics-v4": + del proof["cases"]["4"] + elif change.startswith("integrity-"): proof = receipt["installedHost"]["installed_integrity"] if change == "integrity-digest": proof["expected_sha256"] = "c" * 64 From 8a9f0516798a06d2cb60a44bd568f77e1158df00 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:09:27 -0500 Subject: [PATCH 14/26] build(extension): seal process download archives --- .github/workflows/release.yml | 11 +- docs/python-extension.md | 21 ++++ .../add-process-mep-extension/design.html | 2 +- .../add-process-mep-extension/tasks.md | 1 + scripts/process_release.py | 111 ++++++++++++++++ tests/unit/test_process_release.py | 119 ++++++++++++++++++ 6 files changed, 261 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ccfc0aec..0d737635 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -319,13 +319,17 @@ jobs: --commit "$GITHUB_SHA" \ --wasm-bundle dist/wasm-check/.morphir/build/extensions/python \ --wasm-report dist/wasm-check/.dev/qualification/bundle-host.json \ - --report dist/process-assembly.json + --report dist/process-assembly.json \ + --assets-output dist/process-assets - name: Verify runtime switching, installed integrity and platform refusal run: | + (cd dist/process-assets && sha256sum --strict --check SHA256SUMS) + mkdir -p .dev/process-archive + tar -xzf dist/process-assets/morphir-python-extension-*.tar.gz -C .dev/process-archive python -m scripts.qualify_runtime \ --wasm dist/wasm-check/.morphir/build/extensions/python \ - --process dist/process-release \ + --process .dev/process-archive \ --report dist/runtime-choice.json # Source-pinned host assurance belongs to the existing manual/release @@ -357,7 +361,7 @@ jobs: python -m scripts.benchmark_extension \ --checker .dev/host-qualification-target/release/morphir-python-host-qualification \ --wasm "$wasm/morphir-python-binding-$version.wasm" \ - --process "dist/process-release/morphir-python-extension-$version-x86_64-unknown-linux-gnu" \ + --process ".dev/process-archive/morphir-python-extension-$version-x86_64-unknown-linux-gnu" \ --output dist/host-qualification --samples 5 - name: Upload the process-only candidate and its evidence @@ -366,6 +370,7 @@ jobs: name: process-release-candidate path: | dist/process-release/ + dist/process-assets/ dist/process-assembly.json dist/runtime-choice.json dist/host-qualification/ diff --git a/docs/python-extension.md b/docs/python-extension.md index 4ac41116..c5a88542 100644 --- a/docs/python-extension.md +++ b/docs/python-extension.md @@ -126,6 +126,27 @@ The canonical repository writer refuses a bundle mixing both runtimes. The process candidate includes Linux x86_64/ARM64, macOS x86_64/ARM64 and Windows x86_64. Installation chooses the matching platform and refuses an absent one. +The `process-release-candidate` workflow artifact also retains `process-assets/`: +five raw executables and checksums, a separately named process descriptor, +`morphir-python-extension-VERSION.tar.gz`, a source-bound packaging receipt and +`SHA256SUMS`. The archive contains the complete canonical process bundle, with +`release.json` and executable permissions. Its payloads match the raw assets. +These are development candidates; GitHub release publication remains pending +the SDK and independent conformance prerequisites. + +After downloading the candidate, verify and extract it in Bash: + +```bash +cd process-assets +shasum -a 256 -c SHA256SUMS +mkdir ../python-process +tar -xzf morphir-python-extension-*.tar.gz -C ../python-process +export PROCESS_BUNDLE="$(cd ../python-process && pwd)" +``` + +The qualification workflow runs the installation and runtime-switching checks +against this extracted archive. Its original WASM bundle remains separate. + Set `WASM_BUNDLE` and `PROCESS_BUNDLE` to those unmodified directories. With `MORPHIR_HOST` pointing at the pinned host described above, an isolated trial is: diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 5316a741..85d70f54 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -46,7 +46,7 @@

add-process-mep-extension

Operability and recovery102: candidate refusal tests pass; platform rerun/transfer and final promotion refusals pending.

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. Replacement source 820c9d0 passed all five producers and assembly in run 40685569455. Its Windows dependency proof admits only OS DLLs, with no VC or Python runtime imports. All eleven combined release files, the five producer receipts and the Windows checker proof were independently matched to the collected assembly hashes. Runtime switching, installed-byte tampering and foreign-platform refusal passed on the assembled Linux candidate.

Total withheld while 20 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

-

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

+

Download assets and transformed-byte checks

The assembly category stages all five raw executables with checksums, a separately named process descriptor, and one deterministic gzip/tar download containing the canonical eleven-file process bundle. Archive metadata uses fixed ownership/timestamps and explicit executable permissions; every copied and archived payload must retain its original digest. Corrupt transfers, mixed source/claims/platforms, symlinks and extra files are refused without leaving partial assets. The candidate receipt and final checksum list bind the download bytes to assembly. The existing runtime-selection and source-pinned host checks consume the extracted archive, preserving the build-once contract. Actual 820c9d0 archive bytes pass local offline switching, installed-byte refusal and unsupported-platform refusal. This stages development assets; it does not establish the pending executable MCK promotion gate or publish process releases.

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk 0.3.0: native.rs, traits.rs and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 0882633f..0ee539b8 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -37,5 +37,6 @@ This change is being applied. The published SDK prerequisite is tracked in morph - Implementation is in progress under the active apply goal. SDK publication and a separate upstream Python MCK profile decision are pending; the new draft remains unadvertised. - Task 4.1 now requires a source/artifact-bound framed diagnostic receipt from every process producer. Actual macOS process bytes match the installed Python API's complete v3/v4 diagnostic records with a clean child environment. Missing, changed, stale or mismatched diagnostic proofs block assembly. The checkbox remains open until the replacement platform run supplies all five receipts. +- Task 4.3 now stages deterministic download assets and checksums alongside assembly. The all-platform archive preserves exact bytes, canonical `release.json` and executable permissions; real runtime switching, installed integrity and platform refusal pass after extracting actual 820c9d0 candidate bytes. CI now tests that packaged form. Archive staging is separate from the still-pending independent MCK promotion gate and GitHub process publication. - Source-pinned legacy Rust host checks pass for both actual b38e00e runtime artifacts, including v3/v4 parity, timeout and cleanup. Five release-host timing samples per workload retain exact inputs and raw reports in `.dev/qualification/timings-b38-release/`; the manual/release candidate job owns reruns and artifact retention. MoonBit remains unsupported at the documented pin, with explicit v3/v4 envelope refusals reverified by run 40685569455. These checks do not substitute for task 5.1's canonical executable conformance. - All merges are approved. Tags and publication still require maintainer approval; sync/archive only after implementation and qualification are complete. diff --git a/scripts/process_release.py b/scripts/process_release.py index cf6fba55..16d26514 100644 --- a/scripts/process_release.py +++ b/scripts/process_release.py @@ -9,9 +9,12 @@ from __future__ import annotations import argparse +import gzip +import io import json import re import shutil +import tarfile from pathlib import Path from typing import Any @@ -19,6 +22,7 @@ from scripts import process_diagnostics from scripts import process_extension as process from scripts.qualify_native import tools_pin +from scripts.qualify_runtime import _descriptor from scripts.release_candidates import CandidateError, bundle_candidates @@ -226,6 +230,104 @@ def assemble( } +def archive_bundle( + directory: Path, assembly: Path, wasm: Path, output: Path, commit: str +) -> dict[str, Any]: + """Seal a portable download from verified candidate bytes, without publishing. + + This packaging receipt does not replace host assurance or the independent + executable MCK promotion gate. The current workflow retains candidates only. + """ + version = bundle.crate_version() + expected = {"release.json"} + for target in process.PLATFORMS: + name = process.artifact_name(version, target) + expected.update((name, f"{name}.sha256")) + if {path.name for path in directory.iterdir()} != expected or any( + path.is_symlink() or not path.is_file() for path in directory.iterdir() + ): + raise bundle.BundleError("archive input is not the exact regular process set") + descriptor = _descriptor(directory, "process") + reference = _descriptor(wasm, "wasm") + receipt = _read(assembly) + files = { + name: bundle.sha256((directory / name).read_bytes()) + for name in sorted(expected) + } + if not ( + receipt["kind"] == "process-release-assembly" + and receipt["sourceCommit"] == commit + and receipt["version"] == version + and receipt["platforms"] == list(process.PLATFORMS) + and receipt["files"] == files + and descriptor["extensionId"] == reference["extensionId"] == "morphir-python" + and descriptor["version"] == reference["version"] == version + and descriptor["gitCommit"] == reference["gitCommit"] == commit + and len(reference["artifacts"]) == 1 + and len(descriptor["artifacts"]) == len(process.PLATFORMS) + and {entry["platform"] for entry in descriptor["artifacts"]} + == set(process.PLATFORMS) + and all( + entry["filename"] == process.artifact_name(version, entry["platform"]) + and entry["claims"] == reference["artifacts"][0]["claims"] + for entry in descriptor["artifacts"] + ) + ): + raise bundle.BundleError("archive source, bytes or runtime claims differ") + if output.exists() or output.is_symlink(): + raise FileExistsError(output) + if output.resolve().is_relative_to( + directory.resolve() + ) or output.resolve().is_relative_to(wasm.resolve()): + raise bundle.BundleError("archive assets must remain outside canonical bundles") + output.mkdir() + try: + descriptor_name = f"morphir-python-extension-{version}.release.json" + for name in sorted(expected): + destination = output / (descriptor_name if name == "release.json" else name) + shutil.copyfile(directory / name, destination) + if bundle.sha256(destination.read_bytes()) != files[name]: + raise bundle.BundleError("archive asset transfer changed bytes") + destination.chmod(0o644 if name.endswith((".json", ".sha256")) else 0o700) + archive_path = output / f"morphir-python-extension-{version}.tar.gz" + # Normalize only the container metadata; preserve each original payload. + with ( + archive_path.open("xb") as raw, + gzip.GzipFile(filename="", fileobj=raw, mode="wb", mtime=0) as compressed, + tarfile.open(fileobj=compressed, mode="w") as archive, + ): + for name in sorted(expected): + data = (directory / name).read_bytes() + if bundle.sha256(data) != files[name]: + raise bundle.BundleError("archive payload changed during packaging") + info = tarfile.TarInfo(name) + info.size = len(data) + info.mode = 0o644 if name.endswith((".json", ".sha256")) else 0o700 + archive.addfile(info, io.BytesIO(data)) + result = { + "kind": "process-download-assets", + "sourceCommit": commit, + "version": version, + "assemblySha256": bundle.sha256(assembly.read_bytes()), + "bundleFiles": files, + "files": { + path.name: bundle.sha256(path.read_bytes()) + for path in sorted(output.iterdir()) + }, + } + (output / "process-assets.json").write_text(json.dumps(result, indent=2) + "\n") + (output / "SHA256SUMS").write_text( + "".join( + f"{bundle.sha256(path.read_bytes())} {path.name}\n" + for path in sorted(output.iterdir()) + ) + ) + except BaseException: + shutil.rmtree(output) + raise + return result + + def main(argv: list[str] | None = None) -> int: """Assemble a process candidate against source-bound WASM reference bytes.""" parser = argparse.ArgumentParser(description=__doc__) @@ -235,6 +337,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--wasm-bundle", type=Path, required=True) parser.add_argument("--wasm-report", type=Path, required=True) parser.add_argument("--report", type=Path, required=True) + parser.add_argument("--assets-output", type=Path) args = parser.parse_args(argv) if args.report.resolve().is_relative_to(args.output.resolve()): parser.error("assembly report must remain outside the canonical bundle") @@ -271,6 +374,14 @@ def main(argv: list[str] | None = None) -> int: ) args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, indent=2) + "\n") + if args.assets_output is not None: + archive_bundle( + args.output, + args.report, + args.wasm_bundle, + args.assets_output, + args.commit, + ) except ( bundle.BundleError, CandidateError, diff --git a/tests/unit/test_process_release.py b/tests/unit/test_process_release.py index 362d5624..8b7d3ba2 100644 --- a/tests/unit/test_process_release.py +++ b/tests/unit/test_process_release.py @@ -4,6 +4,7 @@ import copy import json +import tarfile from typing import TYPE_CHECKING, Any import pytest @@ -160,6 +161,124 @@ def test_complete_process_set_keeps_canonical_per_platform_artifacts( assert all(artifact["claims"] == claims for artifact in descriptor["artifacts"]) +def archive_candidate(tmp_path: Path) -> tuple[Path, Path, Path]: + inputs, directory = tmp_path / "inputs", tmp_path / "canonical" + claims, results = candidates(inputs) + report = release.assemble(inputs, directory, COMMIT, claims, results) + wasm = tmp_path / "wasm" + wasm.mkdir() + version = bundle.crate_version() + name = f"morphir-python-binding-{version}.wasm" + digest = bundle.sha256(b"WASM fixture") + (wasm / name).write_bytes(b"WASM fixture") + (wasm / f"{name}.sha256").write_text(f"{digest} {name}\n") + (wasm / "release.json").write_text( + json.dumps( + { + "extensionId": "morphir-python", + "version": version, + "gitCommit": COMMIT, + "artifacts": [ + { + "runtime": "wasm", + "filename": name, + "sha256": digest, + "claims": claims, + } + ], + } + ) + ) + assembled = tmp_path / "assembly.json" + assembled.write_text(json.dumps(report)) + return directory, assembled, wasm + + +def test_download_archive_keeps_exact_bytes_modes_and_canonical_descriptor( + tmp_path: Path, +) -> None: + directory, assembled, wasm = archive_candidate(tmp_path) + version = bundle.crate_version() + report = json.loads(assembled.read_text()) + first = release.archive_bundle( + directory, assembled, wasm, tmp_path / "assets", COMMIT + ) + second = release.archive_bundle( + directory, assembled, wasm, tmp_path / "again", COMMIT + ) + assert first == second + assert first["sourceCommit"] == COMMIT + assert len(list((tmp_path / "assets").iterdir())) == 14 + with tarfile.open( + tmp_path / "assets" / f"morphir-python-extension-{version}.tar.gz" + ) as archive: + assert set(archive.getnames()) == set(report["files"]) + for member in archive.getmembers(): + stream = archive.extractfile(member) + assert stream is not None + assert bundle.sha256(stream.read()) == report["files"][member.name] + assert member.isfile() + assert member.uid == member.gid == member.mtime == 0 + assert member.mode == ( + 0o644 if member.name.endswith((".json", ".sha256")) else 0o700 + ) + lines = (tmp_path / "assets/SHA256SUMS").read_text().splitlines() + assert len(lines) == 13 + for line in lines: + checksum, filename = line.split(" ") + assert checksum == bundle.sha256((tmp_path / "assets" / filename).read_bytes()) + + +@pytest.mark.parametrize( + "change", + ["bytes", "receipt", "source", "claims", "platform", "extra", "symlink", "copy"], +) +def test_archive_refuses_changed_inputs_or_transfers_without_partial_assets( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, change: str +) -> None: + directory, assembled, wasm = archive_candidate(tmp_path) + executable = directory / process.artifact_name( + bundle.crate_version(), process.PLATFORMS[0] + ) + report = json.loads(assembled.read_text()) + descriptor = json.loads((directory / "release.json").read_text()) + if change == "bytes": + executable.write_bytes(b"changed bytes") + elif change == "receipt": + report["files"][executable.name] = "c" * 64 + elif change == "source": + report["sourceCommit"] = "c" * 40 + elif change in ("claims", "platform"): + if change == "claims": + descriptor["artifacts"][0]["claims"]["capabilities"]["streaming"] = True + else: + descriptor["artifacts"][0]["platform"] = descriptor["artifacts"][1][ + "platform" + ] + (directory / "release.json").write_text(json.dumps(descriptor)) + report["files"]["release.json"] = bundle.sha256( + (directory / "release.json").read_bytes() + ) + elif change == "extra": + (directory / "unqualified").write_bytes(b"extra") + elif change == "symlink": + target = tmp_path / "outside" + executable.rename(target) + executable.symlink_to(target) + elif change == "copy": + original = release.shutil.copyfile + + def changed_copy(source: Path, destination: Path) -> None: + original(source, destination) + destination.write_bytes(b"changed transfer") + + monkeypatch.setattr(release.shutil, "copyfile", changed_copy) + assembled.write_text(json.dumps(report)) + with pytest.raises((bundle.BundleError, OSError)): + release.archive_bundle(directory, assembled, wasm, tmp_path / "assets", COMMIT) + assert not (tmp_path / "assets").exists() + + @pytest.mark.parametrize( "change", [ From fb77200a47c8a90732a4ee967ce6cf9c623e1ad7 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:20:46 -0500 Subject: [PATCH 15/26] feat(extension)!: support model MEP sessions Rust consumers must use published morphir-extension-sdk =0.4.0 with binding 0.5.0. Pair CLI 0.1.1 with native 0.1.2; bridge API stays 1. Both real runtimes pass the independent Python JSON MCK profile and saved canonical rechecks, including exact IDs and negotiated limits. --- .config/mise/tasks/rust/consumer | 2 +- .gitattributes | 1 + docs/python-extension.md | 58 ++++++-- .../add-process-mep-extension/design.html | 12 +- .../add-process-mep-extension/proposal.md | 1 + .../add-process-mep-extension/tasks.md | 12 +- packages/morphir-tools/README.md | 2 +- packages/morphir-tools/pyproject.toml | 4 +- .../src/morphir_tools/__init__.py | 2 +- .../src/morphir_tools/python/_bridge.py | 2 +- rust/Cargo.lock | 8 +- rust/Cargo.toml | 2 +- rust/crates/morphir-python-binding/Cargo.toml | 2 +- rust/crates/morphir-python-binding/README.md | 2 +- rust/crates/morphir-python-binding/src/lib.rs | 1 + .../tests/model_session.rs | 89 +++++++++++++ .../tests/support/native.rs | 7 +- .../morphir-python-extension/Cargo.toml | 2 +- .../morphir-python-extension/tests/session.rs | 50 ++++++- rust/crates/morphir-python-native/Cargo.toml | 2 +- rust/extension.toml | 2 +- tests/tools/test_tools.py | 2 +- tests/unit/test_process_extension.py | 4 +- tools/extension-qualification/Cargo.lock | 1 + tools/extension-qualification/Cargo.toml | 1 + tools/extension-qualification/README.md | 32 +++++ .../src/bin/mep_wasm_host.rs | 126 ++++++++++++++++++ uv.lock | 2 +- 28 files changed, 388 insertions(+), 43 deletions(-) create mode 100644 rust/crates/morphir-python-binding/tests/model_session.rs create mode 100644 tools/extension-qualification/src/bin/mep_wasm_host.rs diff --git a/.config/mise/tasks/rust/consumer b/.config/mise/tasks/rust/consumer index cf7ecbd8..03b9c211 100755 --- a/.config/mise/tasks/rust/consumer +++ b/.config/mise/tasks/rust/consumer @@ -10,7 +10,7 @@ trap 'rm -rf "$(dirname "$consumer")"' EXIT cargo new --quiet "$consumer" cat >>"$consumer/Cargo.toml" <add-process-mep-extension

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Applying; published SDK prerequisite in progress

-

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The published SDK 0.3.0 exposes NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type still uses a numeric u64 ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The Python implementation does not opt into model transport and its manifest declares MEP 0.1 only. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

+

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The baseline published SDK 0.3.0 exposed NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type used a numeric ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The baseline Python0.4.2 implementation declared legacy MEP0.1 only. Candidate0.5.0 uses publishedSDK0.4.0 and opts into the exact JSON model draft after native/WASM executable qualification. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

-

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. Crate-tag publication approval is pending; the SDK is not yet published. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with canonical empty arguments selecting stdio by default. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

-

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. Approval is pending for a separate upstream Python JSON profile; no existing MoonBit check will be relaxed and no unit-test result substitutes for executable qualification.

-

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. Protocol discovery remains legacy-only until the published SDK prerequisite and candidate-bound conformance pass. All five task groups remain incomplete.

+

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. The maintainer approved publication at that exact merge. Tag crates/morphir-extension-sdk/v0.4.0 triggered successful workflow40695514380; Cargo independently downloaded the published0.4.0 crate. The compiler now pins registry dependency =0.4.0, without a sibling or Git SDK dependency. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with canonical empty arguments selecting stdio by default. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

+

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. The maintainer approved a separate Python JSON profile, implemented in upstreamPR1109 and tracked by morphir-av8s. Existing MoonBit rich-Ion checks remain unchanged. The clean committed328c0216 shared checker independently qualified all91observations for both actual release-native and WASM0.5.0 bytes, and saved report-check passed for both. Review corrected two non-isolated negative vectors; reports now reach invalid_base64 and unsupported_ir_version. The WASM bridge passes raw request/response bytes; notification silence and exit qualify the adapter/guest route because Extism exposes a requests-only handle ABI. Final merged-source pin and release evidence consumption remain integration work; no unit-test result substitutes for the executable suite.

+

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. The0.5.0 candidate advertises exact0.2.0-draft.1 and legacy0.1 after publishedSDK adoption and passing independent native/WASM reports. Extension0.4.2 remains the published legacy release. Candidate platform and promotion assurance remain in progress.

Build and release assessment

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. A dependent read-only assembly job requires all five byte-bound receipts and their agreement with the WASM reference; it uploads a process-only candidate and does not publish it. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

- +
Risk and deadlineExecution and ownerEvidence and failure response
Incorrect shared compiler or process bytes, before mergeExisting Linux PR bundle category, repository maintainersCanonical bundle probe, isolated installation, deleted source repository, offline v3/v4 comparison with the exact native wheel; failure blocks the selected aggregate.
Platform-specific executable or wheel failure, before promotionExisting five-platform release matrix, explicit manual qualification and extension tag eventsNative target and compiler/toolchain identity, exact digest/claims/source receipts and installed host comparisons. Missing, failed or skipped selected producers block extension publication through its needs graph.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteThe Python MCK profile and published SDK 0.4.0 remain pending. Platform packaging does not establish draft qualification. Promotion assurance must consume and recheck the candidate-bound executable MCK reports before the change is complete.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteSDK0.4.0 is published and exact-pinned. The Python profile inPR1109 passes actual native/WASM qualification; final merged-source pin and producer/promotion consumption remain pending. Platform packaging does not establish draft qualification. Promotion assurance must consume and recheck the candidate-bound executable MCK reports before the change is complete.

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Maturin builds the CI wheel from an isolated source archive, so a hook alone did not share registry dependency objects. Run 40672914229 showed two compilations: 3m07s for the process guest and 2m57s for the archive wheel; the whole producer took 7m07s, compared with 2m57s for earlier job 122760321213. An explicit Cargo target directory now allows compatible registry objects to be reused across those source paths while retaining the independent archive build. The replacement b38e00e Linux producer took 5m01s versus 7m07s, and the two reported compile durations changed from 188/194 seconds to 188/58 seconds. This single observation does not establish a universal speedup. Local path-dependent compiler objects may still rebuild because their source identity differs. macOS and Windows use the same native target and deployment configuration as their wheel producer. Cached CLI bytes are checked against the digest-verified archive before execution. Host receipts retain normalized IR and generated artifact digests, actual CLI and qualification-script identity. Byte-bound lockfiles and script sources use LF on every checkout platform. Artifact ZIP transfer restores only the declared executable mode; payload digests and the original producer mode remain checked. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

@@ -47,7 +47,7 @@

add-process-mep-extension

CriterionWeightCurrent assessment

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. Replacement source 820c9d0 passed all five producers and assembly in run 40685569455. Its Windows dependency proof admits only OS DLLs, with no VC or Python runtime imports. All eleven combined release files, the five producer receipts and the Windows checker proof were independently matched to the collected assembly hashes. Runtime switching, installed-byte tampering and foreign-platform refusal passed on the assembled Linux candidate.

Total withheld while 20 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

Download assets and transformed-byte checks

The assembly category stages all five raw executables with checksums, a separately named process descriptor, and one deterministic gzip/tar download containing the canonical eleven-file process bundle. Archive metadata uses fixed ownership/timestamps and explicit executable permissions; every copied and archived payload must retain its original digest. Corrupt transfers, mixed source/claims/platforms, symlinks and extra files are refused without leaving partial assets. The candidate receipt and final checksum list bind the download bytes to assembly. The existing runtime-selection and source-pinned host checks consume the extracted archive, preserving the build-once contract. Actual 820c9d0 archive bytes pass local offline switching, installed-byte refusal and unsupported-platform refusal. This stages development assets; it does not establish the pending executable MCK promotion gate or publish process releases.

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

-

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk 0.3.0: native.rs, traits.rs and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

+

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk0.4.0: canonical request IDs, native session dispatch and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/openspec/changes/add-process-mep-extension/proposal.md b/openspec/changes/add-process-mep-extension/proposal.md index 6275950b..5702107f 100644 --- a/openspec/changes/add-process-mep-extension/proposal.md +++ b/openspec/changes/add-process-mep-extension/proposal.md @@ -25,6 +25,7 @@ The Python compiler currently exposes a WASM MEP guest advertising legacy `0.1`, ## Beads - **Tracked by**: `morphir-5hxy` +- **Prerequisites**: `morphir-4wrx` (published SDK0.4.0, completed), `morphir-av8s` (approved separate upstream Python JSON executable MCK profile, finos/morphir#1109). - **Related**: `morphir-bj9r` (mise/pipx CLI installation and release assurance), `morphir-ukvn.5.98` (MoonBit Extism qualification context). ## Impact diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 0ee539b8..4ccbe1a6 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -4,9 +4,9 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## 1. Negotiate current and legacy MEP (morphir-5hxy.1) -- [ ] 1.1 Resolve the SDK 0.3.0 numeric-only wire ID gap with a qualified published SDK pin, linking an upstream prerequisite if needed; add explicit legacy `0.1` and exact `0.2.0-draft.1` offers using the pinned SDK's session helpers; verify numeric/string ID preservation, both handshakes, incompatible drafts and unchanged legacy compile/generate behavior. -- [ ] 1.2 Add bilateral complete-tuple and limit negotiation without widening static claims; verify cross-product, unadvertised method, unsupported guarantee and legacy-session refusals. -- [ ] 1.3 Document independent MEP, envelope, IR, claims and package versions; verify the capability reference matches live discovery and initialized sessions. +- [x] 1.1 Resolve the SDK 0.3.0 numeric-only wire ID gap with a qualified published SDK pin, linking an upstream prerequisite if needed; add explicit legacy `0.1` and exact `0.2.0-draft.1` offers using the pinned SDK's session helpers; verify numeric/string ID preservation, both handshakes, incompatible drafts and unchanged legacy compile/generate behavior. +- [x] 1.2 Add bilateral complete-tuple and limit negotiation without widening static claims; verify cross-product, unadvertised method, unsupported guarantee and legacy-session refusals. +- [x] 1.3 Document independent MEP, envelope, IR, claims and package versions; verify the capability reference matches live discovery and initialized sessions. ## 2. Adapt supported model envelopes (morphir-5hxy.2) @@ -18,7 +18,7 @@ This change is being applied. The published SDK prerequisite is tracked in morph - [x] 3.1 Add the thin Rust process guest and documented `morphir-python-extension --stdio` entry point; verify it builds and executes without CPython/PyO3 or a user Rust toolchain. - [x] 3.2 Implement bounded Content-Length framing around the SDK session; test Unicode byte lengths, partial/truncated messages, duplicate/invalid lengths, overflow, invalid UTF-8/JSON and header/body limits. -- [ ] 3.3 Preserve request IDs, notification silence, fixed session capabilities, shutdown/exit and protocol-only stdout; verify a real multi-request process test and document stderr and supervision behavior. +- [x] 3.3 Preserve request IDs, notification silence, fixed session capabilities, shutdown/exit and protocol-only stdout; verify a real multi-request process test and document stderr and supervision behavior. ## 4. Distribute both runtime choices (morphir-5hxy.4) @@ -35,8 +35,8 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## Workflow follow-up -- Implementation is in progress under the active apply goal. SDK publication and a separate upstream Python MCK profile decision are pending; the new draft remains unadvertised. +- Implementation is in progress under the active apply goal. The maintainer approved both prerequisites. SDK0.4.0 is published and registry-pinned. Upstream Python JSON MCK profilePR1109 preserves MoonBit rich-Ion checks; independent actual release-native/WASM0.5.0 runs and saved rechecks pass91observations each. Final merged-source pin and producer/promotion integration remain pending. - Task 4.1 now requires a source/artifact-bound framed diagnostic receipt from every process producer. Actual macOS process bytes match the installed Python API's complete v3/v4 diagnostic records with a clean child environment. Missing, changed, stale or mismatched diagnostic proofs block assembly. The checkbox remains open until the replacement platform run supplies all five receipts. - Task 4.3 now stages deterministic download assets and checksums alongside assembly. The all-platform archive preserves exact bytes, canonical `release.json` and executable permissions; real runtime switching, installed integrity and platform refusal pass after extracting actual 820c9d0 candidate bytes. CI now tests that packaged form. Archive staging is separate from the still-pending independent MCK promotion gate and GitHub process publication. - Source-pinned legacy Rust host checks pass for both actual b38e00e runtime artifacts, including v3/v4 parity, timeout and cleanup. Five release-host timing samples per workload retain exact inputs and raw reports in `.dev/qualification/timings-b38-release/`; the manual/release candidate job owns reruns and artifact retention. MoonBit remains unsupported at the documented pin, with explicit v3/v4 envelope refusals reverified by run 40685569455. These checks do not substitute for task 5.1's canonical executable conformance. -- All merges are approved. Tags and publication still require maintainer approval; sync/archive only after implementation and qualification are complete. +- All merges are approved. Exact SDK0.4.0 tag publication was separately approved and completed; Python/native/extension release tags still require maintainer approval; sync/archive only after implementation and qualification are complete. diff --git a/packages/morphir-tools/README.md b/packages/morphir-tools/README.md index 2fa1c872..5ca42248 100644 --- a/packages/morphir-tools/README.md +++ b/packages/morphir-tools/README.md @@ -75,7 +75,7 @@ format-version rules and the draft v4 status. ## Requirements - Python 3.14+ -- `morphir>=0.1.0` and `morphir-python-native==0.1.1` (installed as dependencies) +- `morphir>=0.1.0` and `morphir-python-native==0.1.2` (installed as dependencies) ## License diff --git a/packages/morphir-tools/pyproject.toml b/packages/morphir-tools/pyproject.toml index f8c7de40..b879f22e 100644 --- a/packages/morphir-tools/pyproject.toml +++ b/packages/morphir-tools/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "morphir-python" -version = "0.1.0" +version = "0.1.1" description = "CLI tools and extensions for Morphir Python" readme = "README.md" license = "Apache-2.0" @@ -24,7 +24,7 @@ authors = [ dependencies = [ "morphir>=0.1.0", # Exact qualified native compiler; checked again when the compiler first loads. - "morphir-python-native==0.1.1", + "morphir-python-native==0.1.2", "typer>=0.15", # Rich uses Pygments; older versions are affected by CVE-2026-4539. "pygments>=2.20.0", diff --git a/packages/morphir-tools/src/morphir_tools/__init__.py b/packages/morphir-tools/src/morphir_tools/__init__.py index 22222dd3..5b33636e 100644 --- a/packages/morphir-tools/src/morphir_tools/__init__.py +++ b/packages/morphir-tools/src/morphir_tools/__init__.py @@ -9,5 +9,5 @@ '0.1.0' """ -__version__ = "0.1.0" +__version__ = "0.1.1" __all__ = ["__version__"] diff --git a/packages/morphir-tools/src/morphir_tools/python/_bridge.py b/packages/morphir-tools/src/morphir_tools/python/_bridge.py index e23a4bd1..5a364ea8 100644 --- a/packages/morphir-tools/src/morphir_tools/python/_bridge.py +++ b/packages/morphir-tools/src/morphir_tools/python/_bridge.py @@ -8,7 +8,7 @@ from ._types import NativeCompilerInfo, NativeCompilerUnavailable -EXPECTED_NATIVE_VERSION = "0.1.1" +EXPECTED_NATIVE_VERSION = "0.1.2" """The exact ``morphir-python-native`` version this release was qualified with. It must equal the pin in this distribution's ``pyproject.toml``. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5fbc6a77..fbaeac02 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1430,9 +1430,9 @@ dependencies = [ [[package]] name = "morphir-extension-sdk" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcb8913b6cb5d45fc20aadd638ddb0c1d83efac9868b8409937e4cfca6babc0" +checksum = "56820fd793c213eb0cd571cc6af6e24196171dbb40e4ab5f65c3838ca6c73dd8" dependencies = [ "base64 0.22.1", "extism-pdk", @@ -1460,7 +1460,7 @@ dependencies = [ [[package]] name = "morphir-python-binding" -version = "0.4.2" +version = "0.5.0" dependencies = [ "cucumber", "morphir-core", @@ -1488,7 +1488,7 @@ dependencies = [ [[package]] name = "morphir-python-native" -version = "0.1.1" +version = "0.1.2" dependencies = [ "morphir-extension-sdk", "morphir-python-binding", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6c1c135c..c729b844 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -14,7 +14,7 @@ homepage = "https://github.com/finos/morphir-python" # Published Morphir crates. Never use sibling-checkout paths here. morphir-core = "=0.2.0" morphir-json = "=0.2.0" -morphir-extension-sdk = "=0.3.0" +morphir-extension-sdk = "=0.4.0" num-bigint = "0.5" # Ruff component crates evolve together; update this group as one unit. ruff_python_parser = "=0.0.15" diff --git a/rust/crates/morphir-python-binding/Cargo.toml b/rust/crates/morphir-python-binding/Cargo.toml index 5e04cc85..1be1816c 100644 --- a/rust/crates/morphir-python-binding/Cargo.toml +++ b/rust/crates/morphir-python-binding/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morphir-python-binding" -version = "0.4.2" +version = "0.5.0" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/rust/crates/morphir-python-binding/README.md b/rust/crates/morphir-python-binding/README.md index e52d8f7f..ad11b4ec 100644 --- a/rust/crates/morphir-python-binding/README.md +++ b/rust/crates/morphir-python-binding/README.md @@ -582,7 +582,7 @@ workspace. Use the same published SDK version as this crate: ```toml [dependencies] morphir-python-binding = { git = "https://github.com/finos/morphir-python", rev = "" } -morphir-extension-sdk = "=0.3.0" +morphir-extension-sdk = "=0.4.0" serde_json = "1" ``` diff --git a/rust/crates/morphir-python-binding/src/lib.rs b/rust/crates/morphir-python-binding/src/lib.rs index 46f3cbfd..bc55b7cb 100644 --- a/rust/crates/morphir-python-binding/src/lib.rs +++ b/rust/crates/morphir-python-binding/src/lib.rs @@ -67,6 +67,7 @@ impl Extension for PythonExtension { ir_versions: vec!["3".into(), "4".into()], generate: true, }), + model_transport: Some(model_transport::support()), ..Default::default() } } diff --git a/rust/crates/morphir-python-binding/tests/model_session.rs b/rust/crates/morphir-python-binding/tests/model_session.rs new file mode 100644 index 00000000..f9a37c79 --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/model_session.rs @@ -0,0 +1,89 @@ +//! Real SDK sessions must admit only the negotiated model tuples and limits. +use morphir_extension_sdk::{NativeExtension, NativeProtocol}; +use morphir_python_binding::PythonExtension; +use serde_json::{Value, json}; + +fn send(protocol: &dyn NativeProtocol, request: Value) -> Value { + serde_json::to_value(protocol.handle(serde_json::from_value(request).unwrap())).unwrap() +} + +fn host_offer() -> Value { + json!({ + "tuples":[{"operation":"morphir.frontend.compileModel","profile":"morphir-ir", + "codec":"morphir-json","envelopeVersion":"0.1.0-draft.1","irVersion":"3.0.0"}], + "limits":{"maxEncodedBytes":65536,"maxDecodedBytes":49152,"maxDepth":64,"maxNodes":10000}, + "guarantees":[], + }) +} + +fn initialize(versions: Value, offer: Option) -> Value { + let mut params = + json!({"protocolVersions":versions,"host":{"name":"qualified-test","version":"1"}}); + if let Some(offer) = offer { + params["modelTransport"] = offer; + } + json!({"jsonrpc":"2.0","id":1,"method":"morphir.initialize","params":params}) +} + +fn compile_model(version: &str) -> Value { + json!({"jsonrpc":"2.0","id":2,"method":"morphir.frontend.compileModel","params":{ + "languageId":"python","sources":{"documents":[{"uri":"main.py","languageId":"python","version":1,"text":"def identity(flag: bool) -> bool:\n return flag\n"}]}, + "package":{"name":"mck/python"},"dependencies":[], + "options":{"irVersion":version,"typesOnly":false},"contextDigest":"a".repeat(64), + "output":{"operation":"morphir.frontend.compileModel","profile":"morphir-ir","codec":"morphir-json","envelopeVersion":"0.1.0-draft.1","irVersion":version}, + }}) +} + +#[test] +fn draft_session_selects_complete_tuples_and_each_lower_limit() { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let session = extension.open_protocol(); + let selected = send( + session.as_ref(), + initialize(json!(["0.2.0-draft.1", "0.1"]), Some(host_offer())), + ); + assert!(selected.get("error").is_none(), "{selected}"); + assert_eq!(selected["result"]["protocolVersion"], "0.2.0-draft.1"); + assert_eq!( + selected["result"]["capabilities"]["modelTransport"], + host_offer() + ); + let compiled = send(session.as_ref(), compile_model("3.0.0")); + assert!(compiled.get("error").is_none(), "{compiled}"); + assert_eq!(compiled["result"]["model"]["irVersion"], "3.0.0"); + let refused = send(session.as_ref(), compile_model("4.0.0")); + assert!( + refused.get("error").is_some(), + "unoffered tuple admitted: {refused}" + ); + let again = send(session.as_ref(), initialize(json!(["0.1"]), None)); + assert!(again.get("error").is_some(), "session was renegotiated"); + let still_refused = send(session.as_ref(), compile_model("4.0.0")); + assert!(still_refused.get("error").is_some()); +} + +#[test] +fn legacy_or_other_drafts_cannot_admit_model_operations() { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let legacy = extension.open_protocol(); + let selected = send( + legacy.as_ref(), + initialize(json!(["0.1"]), Some(host_offer())), + ); + assert!(selected.get("error").is_none(), "{selected}"); + assert_eq!(selected["result"]["protocolVersion"], "0.1"); + assert!(selected["result"]["capabilities"]["modelTransport"].is_null()); + assert!( + send(legacy.as_ref(), compile_model("3.0.0")) + .get("error") + .is_some() + ); + for (versions, offer) in [ + (json!(["0.2.0-draft.2"]), Some(host_offer())), + (json!(["0.2.0-draft.1"]), None), + ] { + let session = extension.open_protocol(); + let refused = send(session.as_ref(), initialize(versions, offer)); + assert_eq!(refused["error"]["code"], -32011); + } +} diff --git a/rust/crates/morphir-python-binding/tests/support/native.rs b/rust/crates/morphir-python-binding/tests/support/native.rs index cdcd69ee..0d86af27 100644 --- a/rust/crates/morphir-python-binding/tests/support/native.rs +++ b/rust/crates/morphir-python-binding/tests/support/native.rs @@ -25,7 +25,12 @@ pub fn an_initialized_extension() -> NativeExtension { .unwrap(), ); assert!(response.error.is_none(), "{:?}", response.error); - let expected = serde_json::to_value(PythonExtension::capabilities()).unwrap(); + let mut capabilities = PythonExtension::capabilities(); + assert!(capabilities.model_transport.is_some()); + // This fixture deliberately selects legacy0.1; model support is static + // inventory, but must be absent from the selected legacy session. + capabilities.model_transport = None; + let expected = serde_json::to_value(capabilities).unwrap(); assert_eq!(expected["frontend"]["multiDocument"], true); assert_eq!(response.result.unwrap()["capabilities"], expected); let response = extension diff --git a/rust/crates/morphir-python-extension/Cargo.toml b/rust/crates/morphir-python-extension/Cargo.toml index 84009521..9c806c4c 100644 --- a/rust/crates/morphir-python-extension/Cargo.toml +++ b/rust/crates/morphir-python-extension/Cargo.toml @@ -10,7 +10,7 @@ description = "Native stdio MEP guest for the Morphir Python compiler" publish = false [dependencies] -morphir-python-binding = { path = "../morphir-python-binding", version = "0.4.2" } +morphir-python-binding = { path = "../morphir-python-binding", version = "0.5.0" } morphir-extension-sdk.workspace = true serde.workspace = true serde_json = { workspace = true, features = ["raw_value"] } diff --git a/rust/crates/morphir-python-extension/tests/session.rs b/rust/crates/morphir-python-extension/tests/session.rs index e2cf3583..75085420 100644 --- a/rust/crates/morphir-python-extension/tests/session.rs +++ b/rust/crates/morphir-python-extension/tests/session.rs @@ -124,7 +124,7 @@ fn invalid_request_ids_do_not_become_notifications_or_poison_the_session() { json!(-1), json!(1.5), ] { - messages.push(json!({"jsonrpc":"2.0","id":id,"method":"morphir.ping"})); + messages.push(json!({"jsonrpc":"2.0","id":id,"method":"morphir.ping","params":{}})); } messages.push(json!({"jsonrpc":"2.0","id":77,"method":"morphir.ping","params":{}})); let mut output = Vec::new(); @@ -155,6 +155,49 @@ fn independent_stream_sessions_cannot_share_initialization_state() { assert_eq!(responses(second)[0]["error"]["code"], -32014); } +#[test] +fn canonical_numeric_and_string_ids_survive_requests_and_lifecycle() { + let ids = [json!(u64::MAX), json!("λ/request"), json!(""), json!(0)]; + let mut messages = vec![initialize()]; + for id in &ids { + messages.push(json!({"jsonrpc":"2.0","id":id,"method":"morphir.ping"})); + } + messages.extend([ + json!({"jsonrpc":"2.0","id":"shutdown","method":"morphir.shutdown","params":{}}), + json!({"jsonrpc":"2.0","method":"morphir.exit"}), + ]); + let mut output = Vec::new(); + serve(&mut Cursor::new(wire(&messages)), &mut output).unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 6); + for (reply, id) in replies[1..5].iter().zip(ids) { + assert_eq!(reply["id"], id); + assert_eq!(reply["result"]["ok"], true); + } + assert_eq!(replies[5]["id"], "shutdown"); + assert!(replies[5].get("error").is_none()); +} + +#[test] +fn canonical_empty_parameter_requests_need_no_params_member() { + let mut output = Vec::new(); + serve( + &mut Cursor::new(wire(&[ + initialize(), + json!({"jsonrpc":"2.0","id":2,"method":"morphir.ping"}), + json!({"jsonrpc":"2.0","id":3,"method":"morphir.shutdown"}), + json!({"jsonrpc":"2.0","method":"morphir.exit"}), + ])), + &mut output, + ) + .unwrap(); + let replies = responses(output); + assert_eq!(replies[1]["id"], 2); + assert_eq!(replies[1]["result"]["ok"], true); + assert_eq!(replies[2]["id"], 3); + assert!(replies[2].get("error").is_none()); +} + #[test] fn truncated_input_exits_with_diagnostics_only_on_stderr() { let mut child = Command::new(env!("CARGO_BIN_EXE_morphir-python-extension")) @@ -200,7 +243,10 @@ fn description_is_fixed_across_initialization_and_reinitialization_refusal() { .unwrap(); let replies = responses(output); assert_eq!(replies.len(), 5); - assert_eq!(replies[0]["result"]["protocolVersions"], json!(["0.1"])); + assert_eq!( + replies[0]["result"]["protocolVersions"], + json!(["0.2.0-draft.1", "0.1"]) + ); assert_eq!(replies[0]["result"], replies[2]["result"]); assert_eq!(replies[2]["result"], replies[4]["result"]); assert_eq!(replies[3]["error"]["code"], -32600); diff --git a/rust/crates/morphir-python-native/Cargo.toml b/rust/crates/morphir-python-native/Cargo.toml index c6b9735d..f3de8815 100644 --- a/rust/crates/morphir-python-native/Cargo.toml +++ b/rust/crates/morphir-python-native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morphir-python-native" -version = "0.1.1" +version = "0.1.2" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/rust/extension.toml b/rust/extension.toml index e978c300..60ed1a53 100644 --- a/rust/extension.toml +++ b/rust/extension.toml @@ -9,7 +9,7 @@ package = "morphir-python-binding" artifact = "morphir-python-binding" extension_id = "morphir-python" name = "Morphir Python" -mep_versions = ["0.1"] +mep_versions = ["0.2.0-draft.1", "0.1"] targets = ["python"] languages = [{ id = "python", file_extensions = [".py"] }] ir_versions = ["3", "4"] diff --git a/tests/tools/test_tools.py b/tests/tools/test_tools.py index d672818f..f48b3a16 100644 --- a/tests/tools/test_tools.py +++ b/tests/tools/test_tools.py @@ -14,7 +14,7 @@ def test_version_is_defined(self) -> None: def test_version_matches_expected(self) -> None: """The version should match the expected value.""" - assert morphir_tools.__version__ == "0.1.0" + assert morphir_tools.__version__ == "0.1.1" def test_installed_distribution_name(self) -> None: """The tools import belongs to the morphir-python distribution.""" diff --git a/tests/unit/test_process_extension.py b/tests/unit/test_process_extension.py index 39cdd087..19b2eb97 100644 --- a/tests/unit/test_process_extension.py +++ b/tests/unit/test_process_extension.py @@ -40,7 +40,9 @@ def candidate(directory: Path) -> str: "sha256": digest, "claimCheck": "unchecked", "claims": { - "protocolVersions": ["0.1"], + "protocolVersions": wasm.declaration()["extension"][ + "mep_versions" + ], "extension": {"id": "morphir-python", "version": version}, "capabilities": {}, }, diff --git a/tools/extension-qualification/Cargo.lock b/tools/extension-qualification/Cargo.lock index 8bbfc1aa..09159e55 100644 --- a/tools/extension-qualification/Cargo.lock +++ b/tools/extension-qualification/Cargo.lock @@ -2345,6 +2345,7 @@ dependencies = [ "morphir-extension-sdk", "morphir-host", "morphir-host-native", + "serde", "serde_json", "sha2 0.11.0", "tempfile", diff --git a/tools/extension-qualification/Cargo.toml b/tools/extension-qualification/Cargo.toml index e4668d10..3bf0b3bb 100644 --- a/tools/extension-qualification/Cargo.toml +++ b/tools/extension-qualification/Cargo.toml @@ -17,6 +17,7 @@ morphir-extension-sdk = { git = "https://github.com/finos/morphir-rust.git", rev morphir-host = { git = "https://github.com/finos/morphir-rust.git", rev = "d51daa8aea7ae09ae295ff254d72b27770c64104" } morphir-host-native = { git = "https://github.com/finos/morphir-rust.git", rev = "d51daa8aea7ae09ae295ff254d72b27770c64104" } serde_json = { version = "1", features = ["arbitrary_precision"] } +serde = { version = "1", features = ["derive"] } sha2 = "0.11" tempfile = "3" tokio = { version = "1", features = ["rt", "macros", "sync", "time"] } diff --git a/tools/extension-qualification/README.md b/tools/extension-qualification/README.md index 25208ca2..eea2af9d 100644 --- a/tools/extension-qualification/README.md +++ b/tools/extension-qualification/README.md @@ -64,3 +64,35 @@ The checker also accepts two optional source paths after `REPORT`, for the type-only and type/value cases respectively. The benchmark uses these to keep the measured inputs reproducible. Inputs must compile and roundtrip in both advertised IR versions. + +## Bridge the real WASM guest to the shared MCK + +The same build produces `mep_wasm_host`, a transport adapter for the upstream +`morphir mck mep guest-run` executable. It forwards request bodies unchanged to +the actual Extism guest's `handle` export and returns its exact response bytes. +The adapter contains no conformance expectations or adjudication logic. + +```bash +"$MCK" --no-banner mck mep guest-run --kit "$PYTHON_JSON_KIT" \ + --host "$PWD/.dev/host-qualification-target/release/mep_wasm_host" \ + --host-arg "$WASM_GUEST" --identity-file "$WASM_GUEST" \ + --identity-file "$PWD/rust/Cargo.lock" \ + --identity-file "$PWD/tools/extension-qualification/Cargo.lock" \ + --identity-file "$PWD/tools/extension-qualification/src/bin/mep_wasm_host.rs" \ + --report "$PWD/.dev/qualification/mck-wasm.json" +"$MCK" --no-banner mck mep report-check --kit "$PYTHON_JSON_KIT" \ + "$PWD/.dev/qualification/mck-wasm.json" +``` + +Use an explicitly pinned shared MCK source/binary and its matching +`python-json-library-v1` kit. Supply absolute guest paths. Process qualification +can pass the raw executable directly as `--host`, without this adapter. +The saved report records the actual host executable, arguments, guest bytes, +checker, corpus, identities and observations. Its canonical recheck requires +the same checker and all recorded identity paths while they are available. + +Extism exposes a requests-only `handle` ABI. As in the pinned upstream +`ExtismChannel`, notifications are not forwarded; an exit notification closes +the bridge and unloads the guest. Those lifecycle observations qualify this +adapter/guest pair, rather than claiming guest notification delivery. Process +session tests exercise actual native guest notification handling separately. diff --git a/tools/extension-qualification/src/bin/mep_wasm_host.rs b/tools/extension-qualification/src/bin/mep_wasm_host.rs new file mode 100644 index 00000000..e9b852ea --- /dev/null +++ b/tools/extension-qualification/src/bin/mep_wasm_host.rs @@ -0,0 +1,126 @@ +//! Thin Extism/stdio adapter for the shared executable MCK. +//! No expectations or conformance decisions live here; raw request/response +//! bodies pass through the actual guest's exported `handle` function. +use anyhow::{Context, Result, ensure}; +use morphir_host_native::extism::{ExtensionContainer, MorphirHostFunctions}; +use serde::{Deserialize, Deserializer}; +use serde_json::value::RawValue; +use std::{ + fs, + io::{BufRead, Write}, + path::PathBuf, +}; + +const MAX_HEADER: usize = 8192; +const MAX_BODY: usize = 16 * 1024 * 1024; + +#[derive(Deserialize)] +struct Head { + method: String, + #[serde(default, deserialize_with = "present_id")] + id: Option>, +} + +fn present_id<'de, D: Deserializer<'de>>(input: D) -> Result>, D::Error> { + Box::::deserialize(input).map(Some) +} + +fn read_frame(input: &mut impl BufRead) -> Result>> { + let mut header = Vec::new(); + loop { + let available = input.fill_buf()?; + if available.is_empty() { + ensure!(header.is_empty(), "truncated MCK adapter header"); + return Ok(None); + } + let byte = available[0]; + input.consume(1); + header.push(byte); + ensure!(header.len() <= MAX_HEADER, "MCK adapter header exceeds8KiB"); + if header.ends_with(b"\r\n\r\n") { + break; + } + } + let mut length = None; + for line in std::str::from_utf8(&header)?.trim_end().split("\r\n") { + let (name, value) = line.split_once(':').context("invalid MCK adapter header")?; + if name.eq_ignore_ascii_case("content-length") { + ensure!(length.is_none(), "duplicate Content-Length"); + let value = value.trim(); + ensure!(!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())); + let size: usize = value.parse()?; + ensure!(size <= MAX_BODY, "MCK adapter body exceeds16MiB"); + length = Some(size); + } + } + let mut body = vec![0; length.context("missing Content-Length")?]; + input.read_exact(&mut body)?; + Ok(Some(body)) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let mut args = std::env::args_os().skip(1); + let wasm = args + .next() + .map(PathBuf::from) + .context("usage: mep_wasm_host GUEST.wasm")?; + ensure!(args.next().is_none(), "unexpected adapter argument"); + // Use the same pinned concrete host container as interoperability checks. + // Notifications have no Extism request ABI, matching ExtismChannel policy. + let container = ExtensionContainer::from_bytes( + "morphir-python", + &fs::read(wasm)?, + MorphirHostFunctions::default(), + )?; + let mut input = std::io::stdin().lock(); + let mut output = std::io::stdout().lock(); + while let Some(body) = read_frame(&mut input)? { + let head: Head = serde_json::from_slice(&body)?; + if head.id.is_none() { + if head.method == "morphir.exit" { + break; + } + continue; + } + let response = container.call_raw("handle", &body).await?; + ensure!(response.len() <= MAX_BODY, "guest response exceeds16MiB"); + write!(output, "Content-Length: {}\r\n\r\n", response.len())?; + output.write_all(&response)?; + output.flush()?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn unicode_body_uses_byte_lengths_and_concatenated_frames_keep_boundaries() { + let body = "λ".as_bytes(); + let mut bytes = format!("Content-Length: {}\r\n\r\n", body.len()).into_bytes(); + bytes.extend(body); + bytes.extend(b"Content-Length: 2\r\n\r\n{}"); + let mut input = Cursor::new(bytes); + assert_eq!(read_frame(&mut input).unwrap().unwrap(), body); + assert_eq!(read_frame(&mut input).unwrap().unwrap(), b"{}"); + assert!(read_frame(&mut input).unwrap().is_none()); + } + + #[test] + fn invalid_truncated_duplicate_and_unbounded_frames_are_refused() { + for bytes in [ + b"Content-Length: 2\r\n\r\n{".as_slice(), + b"Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}", + b"Content-Length: -1\r\n\r\n", + b"Content-Length: 999999999999999999999999\r\n\r\n", + b"Content-Length: 16777217\r\n\r\n", + b"Content-Length: 2\r\n", + ] { + assert!(read_frame(&mut Cursor::new(bytes)).is_err()); + } + assert!(read_frame(&mut Cursor::new(vec![b'x'; MAX_HEADER + 1])).is_err()); + } +} diff --git a/uv.lock b/uv.lock index 3d9732bb..3881afac 100644 --- a/uv.lock +++ b/uv.lock @@ -201,7 +201,7 @@ source = { editable = "packages/morphir" } [[package]] name = "morphir-python" -version = "0.1.0" +version = "0.1.1" source = { editable = "packages/morphir-tools" } dependencies = [ { name = "morphir" }, From b3f55b14b33a5269fe0c3c51439894d6242d6831 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:39:30 -0500 Subject: [PATCH 16/26] build(extension): pin merged Python MCK profile --- .../changes/add-process-mep-extension/design.html | 4 ++-- .../changes/add-process-mep-extension/tasks.md | 2 +- rust/extension.toml | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index ebb7b89e..4e78936c 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -27,14 +27,14 @@

add-process-mep-extension

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The baseline published SDK 0.3.0 exposed NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type used a numeric ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The baseline Python0.4.2 implementation declared legacy MEP0.1 only. Candidate0.5.0 uses publishedSDK0.4.0 and opts into the exact JSON model draft after native/WASM executable qualification. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. The maintainer approved publication at that exact merge. Tag crates/morphir-extension-sdk/v0.4.0 triggered successful workflow40695514380; Cargo independently downloaded the published0.4.0 crate. The compiler now pins registry dependency =0.4.0, without a sibling or Git SDK dependency. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with canonical empty arguments selecting stdio by default. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

-

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. The maintainer approved a separate Python JSON profile, implemented in upstreamPR1109 and tracked by morphir-av8s. Existing MoonBit rich-Ion checks remain unchanged. The clean committed328c0216 shared checker independently qualified all91observations for both actual release-native and WASM0.5.0 bytes, and saved report-check passed for both. Review corrected two non-isolated negative vectors; reports now reach invalid_base64 and unsupported_ir_version. The WASM bridge passes raw request/response bytes; notification silence and exit qualify the adapter/guest route because Extism exposes a requests-only handle ABI. Final merged-source pin and release evidence consumption remain integration work; no unit-test result substitutes for the executable suite.

+

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. The maintainer approved a separate Python JSON profile, merged in upstreamPR1109 as85cf4352eeb517fefb85cda10961f749cce51004 and tracked by closed morphir-av8s. Its treec42c55ef3aa9d0190850251dac97bd094cebb4c4 equals the independently audited328c0216 source tree. The source, lockfile, corpus and complete91-case identity set are pinned in rust/extension.toml. Existing MoonBit rich-Ion checks remain unchanged. The clean committed328c0216 shared checker independently qualified all91observations for both actual release-native and WASM0.5.0 bytes, and saved report-check passed for both. Review corrected two non-isolated negative vectors; reports now reach invalid_base64 and unsupported_ir_version. The WASM bridge passes raw request/response bytes; notification silence and exit qualify the adapter/guest route because Extism exposes a requests-only handle ABI. Release producer/recheck and promotion evidence consumption remain integration work; no unit-test result substitutes for the executable suite.

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. The0.5.0 candidate advertises exact0.2.0-draft.1 and legacy0.1 after publishedSDK adoption and passing independent native/WASM reports. Extension0.4.2 remains the published legacy release. Candidate platform and promotion assurance remain in progress.

Build and release assessment

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. A dependent read-only assembly job requires all five byte-bound receipts and their agreement with the WASM reference; it uploads a process-only candidate and does not publish it. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

- +
Risk and deadlineExecution and ownerEvidence and failure response
Incorrect shared compiler or process bytes, before mergeExisting Linux PR bundle category, repository maintainersCanonical bundle probe, isolated installation, deleted source repository, offline v3/v4 comparison with the exact native wheel; failure blocks the selected aggregate.
Platform-specific executable or wheel failure, before promotionExisting five-platform release matrix, explicit manual qualification and extension tag eventsNative target and compiler/toolchain identity, exact digest/claims/source receipts and installed host comparisons. Missing, failed or skipped selected producers block extension publication through its needs graph.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteSDK0.4.0 is published and exact-pinned. The Python profile inPR1109 passes actual native/WASM qualification; final merged-source pin and producer/promotion consumption remain pending. Platform packaging does not establish draft qualification. Promotion assurance must consume and recheck the candidate-bound executable MCK reports before the change is complete.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteSDK0.4.0 is published and exact-pinned. The Python profile inPR1109 passes actual native/WASM qualification; merged-source pin is recorded; producer/promotion consumption remains pending. Platform packaging does not establish draft qualification. Promotion assurance must consume and recheck the candidate-bound executable MCK reports before the change is complete.

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Maturin builds the CI wheel from an isolated source archive, so a hook alone did not share registry dependency objects. Run 40672914229 showed two compilations: 3m07s for the process guest and 2m57s for the archive wheel; the whole producer took 7m07s, compared with 2m57s for earlier job 122760321213. An explicit Cargo target directory now allows compatible registry objects to be reused across those source paths while retaining the independent archive build. The replacement b38e00e Linux producer took 5m01s versus 7m07s, and the two reported compile durations changed from 188/194 seconds to 188/58 seconds. This single observation does not establish a universal speedup. Local path-dependent compiler objects may still rebuild because their source identity differs. macOS and Windows use the same native target and deployment configuration as their wheel producer. Cached CLI bytes are checked against the digest-verified archive before execution. Host receipts retain normalized IR and generated artifact digests, actual CLI and qualification-script identity. Byte-bound lockfiles and script sources use LF on every checkout platform. Artifact ZIP transfer restores only the declared executable mode; payload digests and the original producer mode remain checked. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 4ccbe1a6..bd706034 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -35,7 +35,7 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## Workflow follow-up -- Implementation is in progress under the active apply goal. The maintainer approved both prerequisites. SDK0.4.0 is published and registry-pinned. Upstream Python JSON MCK profilePR1109 preserves MoonBit rich-Ion checks; independent actual release-native/WASM0.5.0 runs and saved rechecks pass91observations each. Final merged-source pin and producer/promotion integration remain pending. +- Implementation is in progress under the active apply goal. The maintainer approved both prerequisites. SDK0.4.0 is published and registry-pinned. Upstream Python JSON MCK profilePR1109 merged as85cf4352 with audited treec42c55ef, pinned in rust/extension.toml, and preserves MoonBit rich-Ion checks; independent actual release-native/WASM0.5.0 runs and saved rechecks pass91observations each. Producer/recheck and promotion integration remain pending. - Task 4.1 now requires a source/artifact-bound framed diagnostic receipt from every process producer. Actual macOS process bytes match the installed Python API's complete v3/v4 diagnostic records with a clean child environment. Missing, changed, stale or mismatched diagnostic proofs block assembly. The checkbox remains open until the replacement platform run supplies all five receipts. - Task 4.3 now stages deterministic download assets and checksums alongside assembly. The all-platform archive preserves exact bytes, canonical `release.json` and executable permissions; real runtime switching, installed integrity and platform refusal pass after extracting actual 820c9d0 candidate bytes. CI now tests that packaged form. Archive staging is separate from the still-pending independent MCK promotion gate and GitHub process publication. - Source-pinned legacy Rust host checks pass for both actual b38e00e runtime artifacts, including v3/v4 parity, timeout and cleanup. Five release-host timing samples per workload retain exact inputs and raw reports in `.dev/qualification/timings-b38-release/`; the manual/release candidate job owns reruns and artifact retention. MoonBit remains unsupported at the documented pin, with explicit v3/v4 envelope refusals reverified by run 40685569455. These checks do not substitute for task 5.1's canonical executable conformance. diff --git a/rust/extension.toml b/rust/extension.toml index 60ed1a53..d2a48cad 100644 --- a/rust/extension.toml +++ b/rust/extension.toml @@ -34,3 +34,17 @@ release = "https://github.com/finos/morphir/releases/download/v0.4.0-beta.10" "x86_64-apple-darwin" = "4080dcfb04f6e1c902bce2780756c6af44320ac0dd6ff6bd953ca74a0da8e128" "x86_64-pc-windows-msvc" = "c178002d570f4cb8c9ceed4bf7b51f04d4ebec2458765935839c90ac1cc01176" + +# Shared executable conformance, independently authored in finos/morphir. +# This source is merged; the Python and MoonBit profiles remain separate. +[mep] +repository = "https://github.com/finos/morphir.git" +rev = "85cf4352eeb517fefb85cda10961f749cce51004" +tree = "c42c55ef3aa9d0190850251dac97bd094cebb4c4" +cargo_lock_sha256 = "b502b3c627b4b2b8f5835f5ad56d9f341ad24722471c75113f49688b39e56f9b" +runner_version = "0.4.0-beta.10" +kit = "spec/mep/mck/guest-python-json.json" +profile = "python-json-library-v1" +corpus_sha256 = "9b4b5079ec36275b6ab5f11e7029e40959bc6577dcae41c4659d4151fab45a9d" +required_cases = 91 +case_set_sha256 = "a1d451ed13bee322b574637d74548d36e3846205a88b4e1830a9a5bf708a7264" From f5cf244fee3f07b6f8e66fb7e0ded062209e3deb Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:11:12 -0500 Subject: [PATCH 17/26] ci(extension): gate dual-runtime releases with MCK Run the pinned shared checker and saved-report recheck on every producer. Bind transferred observations to exact source, artifact, checker, corpus and discovery identities before assembly and promotion. --- .github/workflows/ci.yml | 43 ++ .github/workflows/release.yml | 72 ++- docs/releasing.md | 35 +- .../add-process-mep-extension/design.html | 12 +- .../add-process-mep-extension/tasks.md | 4 +- scripts/mep_tool.py | 175 +++++++ scripts/process_release.py | 6 +- scripts/qualify_mep.py | 495 ++++++++++++++++++ tests/unit/test_mep_evidence.py | 423 +++++++++++++++ tests/unit/test_mep_tool.py | 31 ++ tools/extension-qualification/README.md | 7 + 11 files changed, 1283 insertions(+), 20 deletions(-) create mode 100644 scripts/mep_tool.py create mode 100644 scripts/qualify_mep.py create mode 100644 tests/unit/test_mep_evidence.py create mode 100644 tests/unit/test_mep_tool.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0fbabb31..f8e3367f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,10 @@ on: description: Package-specific tag that must match its project version type: string default: '' + qualify-mep: + description: Run the source-pinned canonical MCK before release promotion + type: boolean + default: false permissions: contents: read @@ -253,6 +257,44 @@ jobs: --report .dev/qualification/process-x86_64-unknown-linux-gnu.json \ --host-check + # Full executable conformance belongs to candidate qualification. The + # normal PR path already covers model negotiation and framing in Rust. + - name: Cache the source-pinned canonical MCK build + if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' + uses: actions/cache@v5 + with: + path: .dev/mep-tool/ + key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} + + - name: Cache the independent WASM adapter build + if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' + uses: Swatinem/rust-cache@v2 + with: + workspaces: tools/extension-qualification -> ../../.dev/host-qualification-target + + - name: Prepare the shared checker and transparent WASM adapter + if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/.dev/host-qualification-target + run: | + python -m scripts.mep_tool --output .dev/qualification/mep-tool.json + cargo build --locked --release --manifest-path tools/extension-qualification/Cargo.toml --bin mep_wasm_host + + - name: Run and independently recheck canonical MEP reports + if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' + run: | + python -m scripts.qualify_mep \ + --bundle .morphir/build/extensions/python-process --runtime process \ + --tool .dev/qualification/mep-tool.json --commit "$GITHUB_SHA" \ + --report .dev/qualification/mep-x86_64-unknown-linux-gnu-report.json \ + --receipt .dev/qualification/mep-x86_64-unknown-linux-gnu-recheck.json + python -m scripts.qualify_mep \ + --bundle .morphir/build/extensions/python --runtime wasm \ + --tool .dev/qualification/mep-tool.json --commit "$GITHUB_SHA" \ + --adapter .dev/host-qualification-target/release/mep_wasm_host \ + --report .dev/qualification/bundle-mep-report.json \ + --receipt .dev/qualification/bundle-mep-recheck.json + - name: Upload the qualified process bundle and receipt uses: actions/upload-artifact@v6 with: @@ -260,6 +302,7 @@ jobs: path: | .morphir/build/extensions/python-process/ .dev/qualification/process-x86_64-unknown-linux-gnu.json + .dev/qualification/mep-x86_64-unknown-linux-gnu-*.json include-hidden-files: true if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d737635..9deef98c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,7 @@ jobs: uses: ./.github/workflows/ci.yml with: release-tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }} + qualify-mep: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/extension/python/v') }} # Every advertised platform builds its own wheel once; the same job installs # that exact wheel, with the qualified core and tools wheels, into a clean @@ -217,6 +218,39 @@ jobs: --report .dev/qualification/process-${{ matrix.target }}.json \ --host-check + - name: Set up the canonical MCK toolchain + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + uses: jdx/mise-action@v3 + with: + install_args: rust python + + - name: Cache the source-pinned canonical MCK build + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + uses: actions/cache@v5 + with: + path: .dev/mep-tool/ + key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} + + - name: Run and independently recheck the canonical process MEP report + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + shell: bash + run: | + python -m scripts.mep_tool --output .dev/qualification/mep-tool.json + python -m scripts.qualify_mep \ + --bundle .morphir/build/extensions/python-process --runtime process \ + --tool .dev/qualification/mep-tool.json --commit "$GITHUB_SHA" \ + --report .dev/qualification/mep-${{ matrix.target }}-report.json \ + --receipt .dev/qualification/mep-${{ matrix.target }}-recheck.json + - name: Upload the qualified process bundle and receipt if: >- !matrix.reuse && @@ -228,6 +262,7 @@ jobs: path: | .morphir/build/extensions/python-process/ .dev/qualification/process-${{ matrix.target }}.json + .dev/qualification/mep-${{ matrix.target }}-*.json .dev/qualification/*-process-imports.json include-hidden-files: true if-no-files-found: error @@ -281,8 +316,8 @@ jobs: path: dist/candidates/ if-no-files-found: error - # This stages a process-only candidate. Publication additionally requires - # independent MEP guest qualification; no process release is published here. + # Assembly requires all five process and the portable WASM producer's + # canonical MCK run plus saved-report recheck for these exact artifact bytes. process-candidates: name: Verify the process release candidate set needs: [qualify, native-wheels] @@ -311,7 +346,7 @@ jobs: name: morphir-python-extension-bundle path: dist/wasm-check/ - - name: Refuse mixed sources, claims, bytes or installed results + - name: Require canonical MEP rechecks and matching installed results run: | python -m scripts.process_release \ --inputs dist/process-inputs \ @@ -364,7 +399,7 @@ jobs: --process ".dev/process-archive/morphir-python-extension-$version-x86_64-unknown-linux-gnu" \ --output dist/host-qualification --samples 5 - - name: Upload the process-only candidate and its evidence + - name: Upload the process candidate and its release evidence uses: actions/upload-artifact@v6 with: name: process-release-candidate @@ -375,6 +410,7 @@ jobs: dist/runtime-choice.json dist/host-qualification/ dist/process-inputs/*/.dev/qualification/*.json + dist/wasm-check/.dev/qualification/bundle-mep-*.json include-hidden-files: true if-no-files-found: error @@ -410,7 +446,7 @@ jobs: extension-candidate: name: Verify the extension bundle candidate needs: [qualify, native-wheels, process-candidates] - if: startsWith(github.ref, 'refs/tags/extension/python/v') + if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/extension/python/v') runs-on: ubuntu-latest steps: - name: Checkout repository @@ -425,17 +461,35 @@ jobs: - name: Refuse a bundle that is not from this tag's commit run: | bundle=dist/bundle-check/.morphir/build/extensions/python + version="$(python3 -c 'from scripts.extension_bundle import crate_version; print(crate_version())')" + tag="extension/python/v$version" + if [[ "${{ github.ref_type }}" == tag ]]; then tag="$GITHUB_REF_NAME"; fi python3 scripts/release_candidates.py bundle "$bundle" \ - --tag "${{ github.ref_name }}" --commit "$GITHUB_SHA" + --tag "$tag" --commit "$GITHUB_SHA" mkdir -p dist/bundle - version="${GITHUB_REF_NAME#extension/python/v}" # Release assets keep morphir-rust's descriptor name. cp "$bundle/morphir-python-binding-$version.wasm" \ "$bundle/morphir-python-binding-$version.wasm.sha256" dist/bundle/ cp "$bundle/release.json" "dist/bundle/morphir-python-binding-$version.release.json" cp dist/bundle-check/.dev/qualification/bundle*.json dist/bundle/ - # Seal the final asset names, including the renamed descriptor. - (cd dist/bundle && sha256sum -- morphir-python-binding-* bundle*.json > SHA256SUMS) + + - name: Download the qualified process assets and producer evidence + uses: actions/download-artifact@v7 + with: + name: process-release-candidate + path: dist/process-check/ + + - name: Stage both runtimes with their exact qualification evidence + run: | + (cd dist/process-check/process-assets && sha256sum --strict --check SHA256SUMS) + cp dist/process-check/process-assets/morphir-python-extension-* dist/bundle/ + cp dist/process-check/process-assets/process-assets.json dist/bundle/ + cp dist/process-check/process-assembly.json dist/process-check/runtime-choice.json dist/bundle/ + cp dist/process-check/process-inputs/*/.dev/qualification/*.json dist/bundle/ + version="$(python3 -c 'from scripts.extension_bundle import crate_version; print(crate_version())')" + tar -czf "dist/bundle/extension-host-qualification-$version.tar.gz" -C dist/process-check host-qualification + # Seal the final asset names, including both renamed descriptors. + (cd dist/bundle && sha256sum -- * > SHA256SUMS) - name: Upload the release set uses: actions/upload-artifact@v6 diff --git a/docs/releasing.md b/docs/releasing.md index 6bcdb77f..09adbb1f 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,4 +1,4 @@ -# Release Python packages and the WASM extension +# Release Python packages and extensions The package versions are independent. A maintainer approves tags and publication. The [Release workflow](https://github.com/finos/morphir-python/actions/workflows/release.yml) @@ -10,7 +10,7 @@ candidate, including the platform matrix, without publishing a release. | `morphir-v` | Core wheel and source archive on PyPI and GitHub Releases | Python package installer | | `native-v` | Five native compiler wheels and a source archive on PyPI | Exact dependency of the CLI | | `python-v` | CLI wheel and source archive on PyPI and GitHub Releases | mise `pypi:`/`pipx:`, uv or pip | -| `extension/python/v` | WASM, SHA-256 checksum, release descriptor, qualification reports and `SHA256SUMS` on GitHub Releases | Morphir extension host | +| `extension/python/v` | WASM and five process executables, separate runtime descriptors, process bundle archive, qualification evidence and `SHA256SUMS` on GitHub Releases | Morphir extension host | The CLI release is a Python distribution. mise installs it from PyPI into an isolated environment; GitHub release assets are distribution mirrors, not @@ -58,6 +58,37 @@ The WASM gate checks the tag's compiler version, source commit, descriptor diges and checksum contents. Publication rechecks `SHA256SUMS` for the final release asset names, including the renamed release descriptor. +## Qualify both extension runtimes + +The manual and extension-tag workflows run the shared executable MCK on each +process producer and once for the portable WASM guest. `rust/extension.toml` +pins the merged upstream source, tree, lockfile, Python JSON corpus and complete +91-case identity set. `scripts.mep_tool` verifies that source and builds its CLI; +it never replaces upstream expectations. Cargo may reuse compatible cached +objects. Each report names an immutable copy of the built checker. + +`scripts.qualify_mep` runs `morphir mck mep guest-run`, then `report-check` with +the original checker and all identity files still available. A failed run or +recheck prevents the producer from uploading a qualified candidate. The WASM +adapter forwards raw request/response bytes to the guest. Its notification and +exit checks cover the adapter/guest route, as documented in the +[extension guide](python-extension.md). + +The collector consumes only successful producers in the same workflow run. It +verifies source, artifact, checker definitions, lockfiles, corpus, all required +case identities, discovery claims, raw report hashes and the successful saved +recheck receipt. Windows and macOS paths remain unchanged. This is a trusted +producer handoff, not a second filesystem recheck on Linux. A standalone report +with `qualified: true` is insufficient. + +Missing, stale, failed or mismatched evidence blocks process assembly and the +dependent extension promotion job. The candidate also checks installed-byte +integrity, full diagnostic parity with the matching Python API, Windows runtime +imports, extracted-archive runtime switching, and real Rust host behavior. The +final release retains raw MCK reports and recheck receipts, assembly and runtime +receipts, and the host timing evidence. Publication verifies final checksums. +Historical WASM releases remain available with their original bytes. + ## Reproduce the installer checks Check the published configurations without changing your global mise setup: diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 4e78936c..06faf248 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -22,31 +22,31 @@

add-process-mep-extension

-

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Applying; published SDK prerequisite in progress

+

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Applying; SDK published and Python MCK profile merged

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The baseline published SDK 0.3.0 exposed NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type used a numeric ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The baseline Python0.4.2 implementation declared legacy MEP0.1 only. Candidate0.5.0 uses publishedSDK0.4.0 and opts into the exact JSON model draft after native/WASM executable qualification. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

CriterionWeightCurrent assessment
BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. The maintainer approved publication at that exact merge. Tag crates/morphir-extension-sdk/v0.4.0 triggered successful workflow40695514380; Cargo independently downloaded the published0.4.0 crate. The compiler now pins registry dependency =0.4.0, without a sibling or Git SDK dependency. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with canonical empty arguments selecting stdio by default. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

-

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The pinned executable MCK currently accepts only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. The maintainer approved a separate Python JSON profile, merged in upstreamPR1109 as85cf4352eeb517fefb85cda10961f749cce51004 and tracked by closed morphir-av8s. Its treec42c55ef3aa9d0190850251dac97bd094cebb4c4 equals the independently audited328c0216 source tree. The source, lockfile, corpus and complete91-case identity set are pinned in rust/extension.toml. Existing MoonBit rich-Ion checks remain unchanged. The clean committed328c0216 shared checker independently qualified all91observations for both actual release-native and WASM0.5.0 bytes, and saved report-check passed for both. Review corrected two non-isolated negative vectors; reports now reach invalid_base64 and unsupported_ir_version. The WASM bridge passes raw request/response bytes; notification silence and exit qualify the adapter/guest route because Extism exposes a requests-only handle ABI. Release producer/recheck and promotion evidence consumption remain integration work; no unit-test result substitutes for the executable suite.

+

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The baseline executable MCK accepted only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. The maintainer approved a separate Python JSON profile, merged in upstreamPR1109 as85cf4352eeb517fefb85cda10961f749cce51004 and tracked by closed morphir-av8s. Its treec42c55ef3aa9d0190850251dac97bd094cebb4c4 equals the independently audited328c0216 source tree. The source, lockfile, corpus and complete91-case identity set are pinned in rust/extension.toml. Existing MoonBit rich-Ion checks remain unchanged. The clean committed328c0216 shared checker independently qualified all91observations for both actual release-native and WASM0.5.0 bytes, and saved report-check passed for both. Review corrected two non-isolated negative vectors; reports now reach invalid_base64 and unsupported_ir_version. The WASM bridge passes raw request/response bytes; notification silence and exit qualify the adapter/guest route because Extism exposes a requests-only handle ABI. The merged85cf4352 checker also passes both actual runtime candidates and saved rechecks. Release producers now run the canonical suite and saved recheck before upload. No unit-test result substitutes for the executable suite; the replacement five-platform candidate run remains required.

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. The0.5.0 candidate advertises exact0.2.0-draft.1 and legacy0.1 after publishedSDK adoption and passing independent native/WASM reports. Extension0.4.2 remains the published legacy release. Candidate platform and promotion assurance remain in progress.

Build and release assessment

-

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. A dependent read-only assembly job requires all five byte-bound receipts and their agreement with the WASM reference; it uploads a process-only candidate and does not publish it. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

+

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. A dependent read-only assembly job requires all five byte-bound installed-host and executable MCK receipts plus the WASM reference and its MCK receipt. The existing extension promotion job stages both runtime asset sets and their evidence; only an approved extension tag triggers publication. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

- +
Risk and deadlineExecution and ownerEvidence and failure response
Incorrect shared compiler or process bytes, before mergeExisting Linux PR bundle category, repository maintainersCanonical bundle probe, isolated installation, deleted source repository, offline v3/v4 comparison with the exact native wheel; failure blocks the selected aggregate.
Platform-specific executable or wheel failure, before promotionExisting five-platform release matrix, explicit manual qualification and extension tag eventsNative target and compiler/toolchain identity, exact digest/claims/source receipts and installed host comparisons. Missing, failed or skipped selected producers block extension publication through its needs graph.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteSDK0.4.0 is published and exact-pinned. The Python profile inPR1109 passes actual native/WASM qualification; merged-source pin is recorded; producer/promotion consumption remains pending. Platform packaging does not establish draft qualification. Promotion assurance must consume and recheck the candidate-bound executable MCK reports before the change is complete.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteSDK0.4.0 is published and exact-pinned. The Python profile inPR1109 passes actual native/WASM qualification; merged-source pin is recorded; producers run canonical guest-run and saved report-check with all original identities available. Platform packaging does not establish draft qualification. Assembly verifies the successful same-workflow producer recheck, exact report/artifact/source/checker/lock/corpus identities, all91case identities and staged discovery claims. It preserves Windows/macOS paths and does not claim a Linux filesystem recheck of foreign paths. Missing, stale, failed or mismatched evidence blocks the dependent promotion job.

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Maturin builds the CI wheel from an isolated source archive, so a hook alone did not share registry dependency objects. Run 40672914229 showed two compilations: 3m07s for the process guest and 2m57s for the archive wheel; the whole producer took 7m07s, compared with 2m57s for earlier job 122760321213. An explicit Cargo target directory now allows compatible registry objects to be reused across those source paths while retaining the independent archive build. The replacement b38e00e Linux producer took 5m01s versus 7m07s, and the two reported compile durations changed from 188/194 seconds to 188/58 seconds. This single observation does not establish a universal speedup. Local path-dependent compiler objects may still rebuild because their source identity differs. macOS and Windows use the same native target and deployment configuration as their wheel producer. Cached CLI bytes are checked against the digest-verified archive before execution. Host receipts retain normalized IR and generated artifact digests, actual CLI and qualification-script identity. Byte-bound lockfiles and script sources use LF on every checkout platform. Artifact ZIP transfer restores only the declared executable mode; payload digests and the original producer mode remain checked. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

- +
CriterionWeightCurrent assessment
Risk and coverage fit253: focused refusals, installed macOS checks and the b38e00e five-platform matrix pass; replacement source 820c9d0 also passes Windows static-CRT and installed-integrity proof.
Feedback and placement203: one Linux PR path; broader work stays manual/release. Added CI latency not yet measured.
Release evidence and trust20U: current-draft MCK prerequisite and final promotion assembly pending.
Release evidence and trust20U: source-built canonical MCK and transfer refusals pass locally; final five-platform run and promotion assembly remain pending.
Execution efficiency153: compatible target reuse reduced one observed Linux producer from 7m07s to 5m01s; platform-specific CRT objects remain separate.
Ownership and structure104: existing bundle and native-wheel categories, explicit needs, repository maintainers own failures.
Operability and recovery102: candidate refusal tests pass; platform rerun/transfer and final promotion refusals pending.

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. Replacement source 820c9d0 passed all five producers and assembly in run 40685569455. Its Windows dependency proof admits only OS DLLs, with no VC or Python runtime imports. All eleven combined release files, the five producer receipts and the Windows checker proof were independently matched to the collected assembly hashes. Runtime switching, installed-byte tampering and foreign-platform refusal passed on the assembled Linux candidate.

Total withheld while 20 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

-

Download assets and transformed-byte checks

The assembly category stages all five raw executables with checksums, a separately named process descriptor, and one deterministic gzip/tar download containing the canonical eleven-file process bundle. Archive metadata uses fixed ownership/timestamps and explicit executable permissions; every copied and archived payload must retain its original digest. Corrupt transfers, mixed source/claims/platforms, symlinks and extra files are refused without leaving partial assets. The candidate receipt and final checksum list bind the download bytes to assembly. The existing runtime-selection and source-pinned host checks consume the extracted archive, preserving the build-once contract. Actual 820c9d0 archive bytes pass local offline switching, installed-byte refusal and unsupported-platform refusal. This stages development assets; it does not establish the pending executable MCK promotion gate or publish process releases.

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

+

Download assets and transformed-byte checks

The assembly category stages all five raw executables with checksums, a separately named process descriptor, and one deterministic gzip/tar download containing the canonical eleven-file process bundle. Archive metadata uses fixed ownership/timestamps and explicit executable permissions; every copied and archived payload must retain its original digest. Corrupt transfers, mixed source/claims/platforms, symlinks and extra files are refused without leaving partial assets. The candidate receipt and final checksum list bind the download bytes to assembly. The existing runtime-selection and source-pinned host checks consume the extracted archive, preserving the build-once contract. Actual 820c9d0 archive bytes pass local offline switching, installed-byte refusal and unsupported-platform refusal. The same manual/extension-tag workflow now requires canonical producer MCK rechecks before assembly. Final promotion copies verified raw process assets, their separate descriptor/archive, WASM assets and all producer evidence, then seals their final names. Manual runs stage this exact release set without publishing. An approved extension tag publishes it after all selected jobs pass.

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk0.4.0: canonical request IDs, native session dispatch and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index bd706034..3b2479e3 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -1,6 +1,6 @@ # Tasks -This change is being applied. The published SDK prerequisite is tracked in morphir-4wrx and finos/morphir-rust#316. Python process/model implementation tasks remain incomplete. +This change is being applied. SDK 0.4.0 is published and the separate upstream Python JSON MCK profile is merged. Final platform qualification and release promotion remain incomplete. ## 1. Negotiate current and legacy MEP (morphir-5hxy.1) @@ -35,7 +35,7 @@ This change is being applied. The published SDK prerequisite is tracked in morph ## Workflow follow-up -- Implementation is in progress under the active apply goal. The maintainer approved both prerequisites. SDK0.4.0 is published and registry-pinned. Upstream Python JSON MCK profilePR1109 merged as85cf4352 with audited treec42c55ef, pinned in rust/extension.toml, and preserves MoonBit rich-Ion checks; independent actual release-native/WASM0.5.0 runs and saved rechecks pass91observations each. Producer/recheck and promotion integration remain pending. +- Implementation is in progress under the active apply goal. The maintainer approved both prerequisites. SDK0.4.0 is published and registry-pinned. Upstream Python JSON MCK profilePR1109 merged as85cf4352 with audited treec42c55ef, pinned in rust/extension.toml, and preserves MoonBit rich-Ion checks; independent actual release-native/WASM0.5.0 runs and saved rechecks pass91observations each. Producer run/recheck and the same-workflow transfer gate are implemented; the final five-platform run remains pending. - Task 4.1 now requires a source/artifact-bound framed diagnostic receipt from every process producer. Actual macOS process bytes match the installed Python API's complete v3/v4 diagnostic records with a clean child environment. Missing, changed, stale or mismatched diagnostic proofs block assembly. The checkbox remains open until the replacement platform run supplies all five receipts. - Task 4.3 now stages deterministic download assets and checksums alongside assembly. The all-platform archive preserves exact bytes, canonical `release.json` and executable permissions; real runtime switching, installed integrity and platform refusal pass after extracting actual 820c9d0 candidate bytes. CI now tests that packaged form. Archive staging is separate from the still-pending independent MCK promotion gate and GitHub process publication. - Source-pinned legacy Rust host checks pass for both actual b38e00e runtime artifacts, including v3/v4 parity, timeout and cleanup. Five release-host timing samples per workload retain exact inputs and raw reports in `.dev/qualification/timings-b38-release/`; the manual/release candidate job owns reruns and artifact retention. MoonBit remains unsupported at the documented pin, with explicit v3/v4 envelope refusals reverified by run 40685569455. These checks do not substitute for task 5.1's canonical executable conformance. diff --git a/scripts/mep_tool.py b/scripts/mep_tool.py new file mode 100644 index 00000000..cd498edd --- /dev/null +++ b/scripts/mep_tool.py @@ -0,0 +1,175 @@ +"""Build the exact shared MCK source without changing its frozen expectations.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle + + +def sha(path: Path) -> str: + """Hash available file bytes without loading a checker binary into memory.""" + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def object_file(path: Path) -> dict[str, Any]: + """Read a required JSON object at the tool boundary.""" + value = json.loads(path.read_text()) + if not isinstance(value, dict): + raise bundle.BundleError(f"expected an object: {path}") + return value + + +def freeze_binary(executable: Path, cache: Path) -> Path: + """Keep report identities independent of mutable Cargo output filenames.""" + if executable.is_symlink() or not executable.is_file(): + raise bundle.BundleError("MCK build output must be a regular file") + digest = sha(executable) + frozen = cache / digest / executable.name + frozen.parent.mkdir(parents=True, exist_ok=True) + if frozen.exists() or frozen.is_symlink(): + if frozen.is_symlink() or not frozen.is_file() or sha(frozen) != digest: + raise bundle.BundleError("stored MCK checker integrity differs") + return frozen + created = False + try: + with frozen.open("xb") as destination, executable.open("rb") as source: + created = True + shutil.copyfileobj(source, destination) + if sha(frozen) != digest: + raise bundle.BundleError("MCK checker copy integrity differs") + if os.name != "nt": + frozen.chmod(0o700) + except BaseException: + if created: + frozen.unlink(missing_ok=True) + raise + return frozen + + +def command(arguments: list[str], *, cwd: Path | None = None) -> str: + """Run a bounded setup command, retaining diagnostics on failure.""" + result = subprocess.run( + arguments, + cwd=cwd, + text=True, + capture_output=True, + timeout=180, + ) + if result.returncode: + raise bundle.BundleError(f"{arguments[0]} failed: {result.stderr}") + return result.stdout.strip() + + +def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: + """Fetch source by Git identity and run Cargo even when a build cache exists. + + Cargo owns compatible object reuse. A cached receipt alone never authorizes + execution. Only the needed Rust submodule is initialized. + """ + pin = bundle.declaration()["mep"] + cache = bundle.ROOT / ".dev/mep-tool" / pin["rev"] + source = cache / "source" + source.mkdir(parents=True, exist_ok=True) + if not (source / ".git").is_dir(): + command(["git", "init", str(source)]) + command(["git", "remote", "add", "origin", pin["repository"]], cwd=source) + command(["git", "config", "core.autocrlf", "false"], cwd=source) + command(["git", "config", "core.longpaths", "true"], cwd=source) + if command(["git", "remote", "get-url", "origin"], cwd=source) != pin["repository"]: + raise bundle.BundleError("MCK source cache has another repository") + command(["git", "fetch", "--depth", "1", "origin", pin["rev"]], cwd=source) + # Refuse tracked/untracked modifications rather than resetting somebody's work. + if command(["git", "status", "--porcelain"], cwd=source): + raise bundle.BundleError("MCK source cache is dirty") + command(["git", "checkout", "--detach", pin["rev"]], cwd=source) + if command(["git", "rev-parse", "HEAD^{tree}"], cwd=source) != pin["tree"]: + raise bundle.BundleError("MCK source tree differs from the audited pin") + command( + [ + "git", + "-c", + "core.autocrlf=false", + "-c", + "core.longpaths=true", + "submodule", + "update", + "--init", + "--depth", + "1", + "ecosystem/morphir-rust", + ], + cwd=source, + ) + kit = source / pin["kit"] + if ( + sha(source / "Cargo.lock") != pin["cargo_lock_sha256"] + or sha(kit) != pin["corpus_sha256"] + ): + raise bundle.BundleError("MCK lockfile or frozen corpus differs") + command(["mise", "trust", str(source / ".config/mise/config.toml")]) + target = (target or cache / "target").resolve() + log = cache / "build.log" + # This is a correctness checker, not the implementation being benchmarked. + # A dev-profile build reduces setup cost and is identified in the receipt. + with log.open("w") as stream: + result = subprocess.run( + [ + "cargo", + "build", + "--locked", + "--manifest-path", + str(source / "Cargo.toml"), + "--target-dir", + str(target), + "-p", + "morphir", + ], + cwd=bundle.ROOT, + stdout=stream, + stderr=subprocess.STDOUT, + timeout=1800, + ) + if result.returncode: + raise bundle.BundleError(f"MCK build failed; see {log}") + if command(["git", "status", "--porcelain"], cwd=source): + raise bundle.BundleError("MCK build changed its pinned source") + executable = target / "debug" / ("morphir.exe" if os.name == "nt" else "morphir") + executable = freeze_binary(executable, cache / "bin") + receipt = { + "kind": "source-built-shared-mck", + "revision": pin["rev"], + "tree": pin["tree"], + "executable": str(executable), + "binarySha256": sha(executable), + "kit": str(kit), + "corpusSha256": sha(kit), + "cargoLockSha256": sha(source / "Cargo.lock"), + "profile": "dev", + "rustc": command(["rustc", "-vV"]), + "preparerSha256": sha(Path(__file__)), + } + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(receipt, indent=2) + "\n") + return receipt + + +def main() -> None: + """Prepare one compatible checker for explicit conformance consumers.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--target-dir", type=Path) + args = parser.parse_args() + prepare(args.output, args.target_dir) + + +if __name__ == "__main__": + main() diff --git a/scripts/process_release.py b/scripts/process_release.py index 16d26514..b79d4c2f 100644 --- a/scripts/process_release.py +++ b/scripts/process_release.py @@ -19,7 +19,7 @@ from typing import Any from scripts import extension_bundle as bundle -from scripts import process_diagnostics +from scripts import process_diagnostics, qualify_mep from scripts import process_extension as process from scripts.qualify_native import tools_pin from scripts.qualify_runtime import _descriptor @@ -365,6 +365,9 @@ def main(argv: list[str] | None = None) -> int: ): raise bundle.BundleError("WASM reference is not this qualified candidate") claims = _read(args.wasm_bundle / "release.json")["artifacts"][0]["claims"] + canonical_mep = qualify_mep.verify_release( + args.inputs, args.wasm_bundle, args.wasm_report.parent, args.commit + ) result = assemble( args.inputs, args.output, @@ -372,6 +375,7 @@ def main(argv: list[str] | None = None) -> int: claims, reference["offline_round_trips"], ) + result["canonicalMep"] = canonical_mep args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, indent=2) + "\n") if args.assets_output is not None: diff --git a/scripts/qualify_mep.py b/scripts/qualify_mep.py new file mode 100644 index 00000000..2d9e50cc --- /dev/null +++ b/scripts/qualify_mep.py @@ -0,0 +1,495 @@ +"""Run canonical MEP qualification and bind its producer recheck for transfer. + +Only the shared executable MCK adjudicates observations. This module verifies +tool/artifact identities and the trusted same-workflow producer handoff; it does +not replace canonical report-check or rewrite producer filesystem identities. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import ntpath +import os +import posixpath +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle +from scripts import mep_tool +from scripts.qualify_runtime import _descriptor + + +def argument_blob(value: str, *, windows: bool) -> dict[str, list[int]]: + """Match the canonical runner's lossless serde OsString representation.""" + if not windows: + return {"Unix": list(os.fsencode(value))} + encoded = value.encode("utf-16-le", errors="surrogatepass") + return { + "Windows": [ + int.from_bytes(encoded[index : index + 2], "little") + for index in range(0, len(encoded), 2) + ] + } + + +def _same_recorded_path(left: str, right: str, *, windows: bool) -> bool: + # Rust canonicalization adds the Windows extended-length prefix. Never + # resolve another producer's paths against the collector's filesystem. + if windows: + + def normalize(value: str) -> str: + value = value.replace("/", "\\") + if value.startswith("\\\\?\\UNC\\"): + value = "\\\\" + value[8:] + elif value.startswith("\\\\?\\"): + value = value[4:] + return ntpath.normcase(ntpath.normpath(value)) + + return normalize(left) == normalize(right) + return posixpath.normpath(left) == posixpath.normpath(right) + + +def _bind_roles( + roles: dict[str, dict[str, str]], identity: dict[str, Any] +) -> dict[str, dict[str, str]]: + """Bind live producer files to the runner's own canonical identities.""" + bound = {} + for name, expected in roles.items(): + matches = [ + record + for record in identity["files"] + if record["sha256"] == expected["sha256"] + and os.path.samefile(record["path"], expected["path"]) + ] + if len(matches) != 1: + raise bundle.BundleError(f"MCK file identity differs for {name}") + bound[name] = matches[0] + return bound + + +def definition_hashes() -> dict[str, str]: + """Identify the exact producer definition and pin configuration.""" + return { + name: mep_tool.sha(bundle.ROOT / name) + for name in ( + "scripts/qualify_mep.py", + "scripts/mep_tool.py", + "rust/extension.toml", + ) + } + + +def case_set_sha(records: list[dict[str, Any]]) -> str: + """Hash the complete required identity set, independently of record order.""" + identifiers = [record["caseId"] for record in records] + if any(not isinstance(value, str) for value in identifiers) or len( + set(identifiers) + ) != len(identifiers): + raise bundle.BundleError("MCK observation identities are invalid or duplicated") + return hashlib.sha256( + json.dumps(sorted(identifiers), separators=(",", ":")).encode() + ).hexdigest() + + +def _tool(path: Path) -> dict[str, Any]: + tool = mep_tool.object_file(path) + pin = bundle.declaration()["mep"] + if not ( + tool["kind"] == "source-built-shared-mck" + and tool["revision"] == pin["rev"] + and tool["tree"] == pin["tree"] + and tool["corpusSha256"] == pin["corpus_sha256"] + and tool["cargoLockSha256"] == pin["cargo_lock_sha256"] + and tool["preparerSha256"] == mep_tool.sha(Path(mep_tool.__file__)) + and mep_tool.sha(Path(tool["executable"])) == tool["binarySha256"] + and mep_tool.sha(Path(tool["kit"])) == pin["corpus_sha256"] + ): + raise bundle.BundleError("MCK tool receipt or available bytes differ") + return tool + + +def _verify_transfer( + receipt: dict[str, Any], + report_path: Path, + *, + commit: str, + runtime: str, + platform: str, + artifact_sha: str, + claims: dict[str, Any], +) -> dict[str, Any]: + """Verify exact evidence from successful producers in this workflow run. + + Canonical report-check already ran on the producer with the original + checker and identity paths. Those paths can be Windows/macOS-specific; + they are never rewritten or falsely rechecked on the Linux collector. + Workflow needs, source/checker definitions, complete records and byte-bound + receipts form the explicit transfer contract. This accepts no standalone + 'qualified' boolean as conformance evidence. + """ + pin = bundle.declaration()["mep"] + report = mep_tool.object_file(report_path) + identity = report["identity"] + invocation = receipt["invocation"] + windows = invocation["windows"] + roles = receipt["roles"] + expected_roles = {"guest", "compilerLock"} + if runtime == "wasm": + expected_roles |= {"adapter", "adapterLock", "adapterSource"} + if not ( + receipt["kind"] == "canonical-mep-producer-recheck" + and receipt["sourceCommit"] == commit + and receipt["runtime"] == runtime + and receipt["platform"] == platform + and receipt["artifactSha256"] == artifact_sha + and receipt["definitionInputs"] == definition_hashes() + and receipt["reportSha256"] == mep_tool.sha(report_path) + and receipt["reportFilename"] == report_path.name + and receipt["recheck"]["exitCode"] == 0 + and receipt["recheck"]["stdout"].strip() == "MEP guest report: qualified" + and receipt["recheck"]["stderr"] == "" + and report["contractVersion"] == "0.1.0-draft.1" + and report["suite"] == "mep-guest" + and report["profile"] == pin["profile"] + and report["corpusSha256"] == pin["corpus_sha256"] + and report["qualified"] is True + and report["error"] is None + and len(report["records"]) == pin["required_cases"] + and case_set_sha(report["records"]) == pin["case_set_sha256"] + and identity["runner"] + == { + "name": "morphir", + "version": pin["runner_version"], + "commit": pin["rev"], + "dirty": False, + } + and identity["runnerBinary"]["sha256"] == receipt["checker"]["binarySha256"] + and receipt["checker"]["revision"] == pin["rev"] + and receipt["checker"]["tree"] == pin["tree"] + and receipt["checker"]["cargoLockSha256"] == pin["cargo_lock_sha256"] + and identity == receipt["checkedIdentity"] + and set(roles) == expected_roles + and len(identity["files"]) == len(roles) + and all(role in identity["files"] for role in roles.values()) + and len({role["path"] for role in roles.values()}) == len(roles) + and isinstance(windows, bool) + and windows == platform.endswith("windows-msvc") + and identity["arguments"] + == [argument_blob(value, windows=windows) for value in invocation["arguments"]] + and _same_recorded_path( + invocation["host"], identity["host"]["path"], windows=windows + ) + and _same_recorded_path( + receipt["checker"]["executable"], + identity["runnerBinary"]["path"], + windows=windows, + ) + and receipt["roles"]["guest"] in identity["files"] + and receipt["roles"]["guest"]["sha256"] == artifact_sha + and receipt["roles"]["compilerLock"] in identity["files"] + and receipt["roles"]["compilerLock"]["sha256"] + == mep_tool.sha(bundle.ROOT / "rust/Cargo.lock") + ): + raise bundle.BundleError( + "MCK producer transfer is missing, stale or mismatched" + ) + describes = [ + record for record in report["records"] if record["caseId"] == "describe" + ] + if len(describes) != 1 or describes[0]["observed"]["result"] != claims: + raise bundle.BundleError("MCK discovery differs from staged artifact claims") + if runtime == "process": + if identity["host"] != receipt["roles"]["guest"] or identity["arguments"]: + raise bundle.BundleError( + "MCK process host is not the actual bare executable" + ) + elif runtime == "wasm": + if not ( + len(identity["arguments"]) == 1 + and _same_recorded_path( + invocation["arguments"][0], roles["guest"]["path"], windows=windows + ) + and identity["host"] == receipt["roles"]["adapter"] + and receipt["roles"]["adapterLock"] in identity["files"] + and receipt["roles"]["adapterLock"]["sha256"] + == mep_tool.sha(bundle.ROOT / "tools/extension-qualification/Cargo.lock") + and receipt["roles"]["adapterSource"] in identity["files"] + and receipt["roles"]["adapterSource"]["sha256"] + == mep_tool.sha( + bundle.ROOT / "tools/extension-qualification/src/bin/mep_wasm_host.rs" + ) + ): + raise bundle.BundleError("MCK WASM adapter identity differs") + else: + raise bundle.BundleError("unsupported MCK runtime") + return report + + +def verify_transfer( + receipt: dict[str, Any], + report_path: Path, + *, + commit: str, + runtime: str, + platform: str, + artifact_sha: str, + claims: dict[str, Any], +) -> dict[str, Any]: + """Reject incomplete evidence as well as mismatched evidence.""" + try: + return _verify_transfer( + receipt, + report_path, + commit=commit, + runtime=runtime, + platform=platform, + artifact_sha=artifact_sha, + claims=claims, + ) + except (KeyError, TypeError, ValueError, IndexError, OSError) as error: + raise bundle.BundleError( + "incomplete canonical MCK producer evidence" + ) from error + + +def qualify( + directory: Path, + runtime: str, + tool_path: Path, + output: Path, + receipt_path: Path, + commit: str, + adapter: Path | None = None, +) -> dict[str, Any]: + """Execute the canonical suite and saved recheck while all identities exist.""" + directory = directory.resolve() + if output.resolve().is_relative_to( + directory + ) or receipt_path.resolve().is_relative_to(directory): + raise bundle.BundleError("MCK evidence must remain outside canonical bundles") + descriptor = _descriptor(directory, runtime) + if ( + descriptor["gitCommit"] != commit + or descriptor["version"] != bundle.crate_version() + ): + raise bundle.BundleError("MCK candidate source/version differs") + platform = bundle._target() if runtime == "process" else "portable" + selected = [ + entry + for entry in descriptor["artifacts"] + if runtime == "wasm" or entry["platform"] == platform + ] + if len(selected) != 1: + raise bundle.BundleError("MCK needs exactly the selected runtime artifact") + entry = selected[0] + guest = (directory / entry["filename"]).resolve() + roles: dict[str, dict[str, str]] = { + "guest": {"path": str(guest), "sha256": mep_tool.sha(guest)}, + "compilerLock": { + "path": str(bundle.ROOT / "rust/Cargo.lock"), + "sha256": mep_tool.sha(bundle.ROOT / "rust/Cargo.lock"), + }, + } + host, arguments = guest, [] + if runtime == "wasm": + if adapter is None: + raise bundle.BundleError( + "WASM MCK qualification needs its concrete adapter" + ) + host = adapter.resolve(strict=True) + arguments = [str(guest)] + for name, path in ( + ("adapter", host), + ("adapterLock", bundle.ROOT / "tools/extension-qualification/Cargo.lock"), + ( + "adapterSource", + bundle.ROOT / "tools/extension-qualification/src/bin/mep_wasm_host.rs", + ), + ): + roles[name] = {"path": str(path), "sha256": mep_tool.sha(path)} + tool = _tool(tool_path) + output = output.resolve() + output.parent.mkdir(parents=True, exist_ok=True) + command = [ + tool["executable"], + "--no-banner", + "mck", + "mep", + "guest-run", + "--kit", + tool["kit"], + "--expect-kit-sha256", + tool["corpusSha256"], + "--host", + str(host), + "--report", + str(output), + ] + for argument in arguments: + command.extend(("--host-arg", argument)) + for role in roles.values(): + command.extend(("--identity-file", role["path"])) + with tempfile.TemporaryDirectory(prefix="canonical-python-mep-") as scratch: + environment = os.environ | {"MORPHIR_HOME": scratch, "MORPHIR_NO_BANNER": "1"} + for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + environment[name] = environment[name.lower()] = bundle.OFFLINE + environment.pop("NO_PROXY", None) + environment.pop("no_proxy", None) + first = subprocess.run( + command, env=environment, capture_output=True, text=True, timeout=300 + ) + if first.returncode: + raise bundle.BundleError( + f"canonical MCK run failed: {first.stdout} {first.stderr}; " + f"report {output}" + ) + recheck_command = [ + tool["executable"], + "--no-banner", + "mck", + "mep", + "report-check", + "--kit", + tool["kit"], + str(output), + ] + recheck = subprocess.run( + recheck_command, env=environment, capture_output=True, text=True, timeout=60 + ) + if recheck.returncode: + raise bundle.BundleError( + f"canonical MCK recheck failed: {recheck.stdout} {recheck.stderr}" + ) + report = mep_tool.object_file(output) + if report["identity"]["arguments"] != [ + argument_blob(value, windows=os.name == "nt") for value in arguments + ]: + raise bundle.BundleError("MCK invocation arguments differ") + roles = _bind_roles(roles, report["identity"]) + receipt = { + "kind": "canonical-mep-producer-recheck", + "sourceCommit": commit, + "runtime": runtime, + "platform": platform, + "artifactSha256": entry["sha256"], + "definitionInputs": definition_hashes(), + "checker": tool, + "reportFilename": output.name, + "reportSha256": mep_tool.sha(output), + "roles": roles, + "invocation": { + "host": str(host), + "arguments": arguments, + "windows": os.name == "nt", + }, + "checkedIdentity": report["identity"], + "recheck": { + "exitCode": recheck.returncode, + "stdout": recheck.stdout, + "stderr": recheck.stderr, + }, + } + verify_transfer( + receipt, + output, + commit=commit, + runtime=runtime, + platform=platform, + artifact_sha=entry["sha256"], + claims=entry["claims"], + ) + receipt_path.parent.mkdir(parents=True, exist_ok=True) + receipt_path.write_text(json.dumps(receipt, indent=2) + "\n") + return receipt + + +def verify_release( + inputs: Path, wasm_bundle: Path, wasm_proofs: Path, commit: str +) -> dict[str, Any]: + """Require all six producer rechecks before the existing release assembly.""" + from scripts.process_extension import PLATFORMS + + proofs = [] + for target in PLATFORMS: + root = inputs / f"morphir-python-process-{target}" + proofs.append( + ( + root / ".morphir/build/extensions/python-process", + root / ".dev/qualification", + "process", + target, + f"mep-{target}", + ) + ) + proofs.append((wasm_bundle, wasm_proofs, "wasm", "portable", "bundle-mep")) + results = [] + for directory, evidence, runtime, platform, prefix in proofs: + descriptor = _descriptor(directory, runtime) + if descriptor["gitCommit"] != commit: + raise bundle.BundleError("MCK release source differs") + (entry,) = descriptor["artifacts"] + if runtime == "process" and entry["platform"] != platform: + raise bundle.BundleError("MCK release platform differs") + report_path = evidence / f"{prefix}-report.json" + receipt_path = evidence / f"{prefix}-recheck.json" + receipt = mep_tool.object_file(receipt_path) + verify_transfer( + receipt, + report_path, + commit=commit, + runtime=runtime, + platform=platform, + artifact_sha=mep_tool.sha(directory / entry["filename"]), + claims=entry["claims"], + ) + results.append( + { + "runtime": runtime, + "platform": platform, + "artifactSha256": entry["sha256"], + "reportFilename": report_path.name, + "reportSha256": mep_tool.sha(report_path), + "receiptFilename": receipt_path.name, + "receiptSha256": mep_tool.sha(receipt_path), + "checkerBinarySha256": receipt["checker"]["binarySha256"], + } + ) + pin = bundle.declaration()["mep"] + return { + "sourceCommit": commit, + "profile": pin["profile"], + "checkerRevision": pin["rev"], + "corpusSha256": pin["corpus_sha256"], + "requiredCasesPerArtifact": pin["required_cases"], + "producerRechecks": results, + } + + +def main() -> None: + """Qualify one producer's canonical bundle using an explicitly prepared tool.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--bundle", type=Path, required=True) + parser.add_argument("--runtime", choices=("process", "wasm"), required=True) + parser.add_argument("--tool", type=Path, required=True) + parser.add_argument("--report", type=Path, required=True) + parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--commit", required=True) + parser.add_argument("--adapter", type=Path) + args = parser.parse_args() + qualify( + args.bundle, + args.runtime, + args.tool, + args.report, + args.receipt, + args.commit, + args.adapter, + ) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/test_mep_evidence.py b/tests/unit/test_mep_evidence.py new file mode 100644 index 00000000..779b5bf0 --- /dev/null +++ b/tests/unit/test_mep_evidence.py @@ -0,0 +1,423 @@ +"""Bind native OS arguments without rewriting canonical MCK report identities.""" + +import copy +import json +from typing import TYPE_CHECKING, Any + +import pytest + +from scripts import extension_bundle as bundle +from scripts import qualify_mep + +if TYPE_CHECKING: + from pathlib import Path + + +def test_native_argument_blobs_preserve_unicode_bytes_and_utf16_units() -> None: + path = "guest-λ.wasm" + assert qualify_mep.argument_blob(path, windows=False) == { + "Unix": list(path.encode()) + } + assert qualify_mep.argument_blob(path, windows=True) == { + "Windows": [ord(character) for character in path] + } + assert qualify_mep.argument_blob("🙂", windows=True) == { + "Windows": [0xD83D, 0xDE42] + } + + +@pytest.fixture +def witness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: + """A small transfer fixture, never a canonical conformance report.""" + declaration = copy.deepcopy(bundle.declaration()) + records = [ + {"caseId": "describe", "observed": {"result": {"fixture": "claims"}}}, + {"caseId": "opaque-request-id", "observed": {"id": "λ"}}, + ] + declaration["mep"]["required_cases"] = len(records) + declaration["mep"]["case_set_sha256"] = qualify_mep.case_set_sha(records) + monkeypatch.setattr(bundle, "declaration", lambda: declaration) + pin = declaration["mep"] + roles = { + "guest": {"path": "/producer/guest", "sha256": "a" * 64}, + "compilerLock": { + "path": "/producer/rust/Cargo.lock", + "sha256": qualify_mep.mep_tool.sha(bundle.ROOT / "rust/Cargo.lock"), + }, + } + identity = { + "runner": { + "name": "morphir", + "version": pin["runner_version"], + "commit": pin["rev"], + "dirty": False, + }, + "runnerBinary": {"path": "/producer/morphir", "sha256": "b" * 64}, + "host": roles["guest"], + "arguments": [], + "files": list(roles.values()), + } + report = { + "contractVersion": "0.1.0-draft.1", + "suite": "mep-guest", + "profile": pin["profile"], + "corpusSha256": pin["corpus_sha256"], + "qualified": True, + "error": None, + "records": records, + "identity": identity, + } + path = tmp_path / "mep-report.json" + path.write_text(json.dumps(report)) + receipt = { + "kind": "canonical-mep-producer-recheck", + "sourceCommit": "c" * 40, + "runtime": "process", + "platform": "aarch64-apple-darwin", + "artifactSha256": "a" * 64, + "definitionInputs": qualify_mep.definition_hashes(), + "reportSha256": qualify_mep.mep_tool.sha(path), + "reportFilename": path.name, + "recheck": { + "exitCode": 0, + "stdout": "MEP guest report: qualified\n", + "stderr": "", + }, + "checker": { + "revision": pin["rev"], + "tree": pin["tree"], + "cargoLockSha256": pin["cargo_lock_sha256"], + "binarySha256": "b" * 64, + "executable": "/producer/morphir", + }, + "checkedIdentity": copy.deepcopy(identity), + "roles": copy.deepcopy(roles), + "invocation": {"host": "/producer/guest", "arguments": [], "windows": False}, + } + return {"receipt": receipt, "report": report, "path": path} + + +def verify(witness: dict[str, Any]) -> dict[str, Any]: + return qualify_mep.verify_transfer( + witness["receipt"], + witness["path"], + commit="c" * 40, + runtime=witness["receipt"]["runtime"], + platform=witness["receipt"]["platform"], + artifact_sha="a" * 64, + claims={"fixture": "claims"}, + ) + + +def reseal(witness: dict[str, Any]) -> None: + witness["path"].write_text(json.dumps(witness["report"])) + witness["receipt"]["reportSha256"] = qualify_mep.mep_tool.sha(witness["path"]) + + +def test_transfer_preserves_original_foreign_paths(witness: dict[str, Any]) -> None: + assert verify(witness) == witness["report"] + + +@pytest.mark.parametrize( + "field", + [ + "sourceCommit", + "artifactSha256", + "definitionInputs", + "reportSha256", + "reportFilename", + "recheck", + "checker", + "checkedIdentity", + "roles", + "invocation", + ], +) +def test_missing_producer_evidence_blocks_transfer( + witness: dict[str, Any], + field: str, +) -> None: + del witness["receipt"][field] + with pytest.raises(bundle.BundleError): + verify(witness) + + +@pytest.mark.parametrize( + "mutation", + [ + "source", + "artifact", + "definitions", + "checker", + "failed-recheck", + "pending-recheck", + "report-bytes", + "corpus", + "profile", + "case-set", + "duplicate-case", + "missing-case", + "claims", + "lock", + "extra-file", + "arguments", + "host", + "dirty-runner", + ], +) +def test_altered_evidence_blocks_transfer_even_when_qualified( + witness: dict[str, Any], + mutation: str, +) -> None: + receipt, report = witness["receipt"], witness["report"] + if mutation == "source": + receipt["sourceCommit"] = "d" * 40 + elif mutation == "artifact": + receipt["artifactSha256"] = "d" * 64 + elif mutation == "definitions": + receipt["definitionInputs"] = {} + elif mutation == "checker": + receipt["checker"]["revision"] = "d" * 40 + elif mutation == "failed-recheck": + receipt["recheck"]["exitCode"] = 1 + elif mutation == "pending-recheck": + receipt["recheck"]["stdout"] = "pending" + elif mutation == "report-bytes": + witness["path"].write_text("{}") + with pytest.raises(bundle.BundleError): + verify(witness) + return + elif mutation == "corpus": + report["corpusSha256"] = "d" * 64 + elif mutation == "profile": + report["profile"] = "moonbit-rich-ion" + elif mutation == "case-set": + report["records"][1]["caseId"] = "unknown" + elif mutation == "duplicate-case": + report["records"][1]["caseId"] = "describe" + elif mutation == "missing-case": + report["records"].pop() + elif mutation == "claims": + report["records"][0]["observed"]["result"] = {} + elif mutation == "lock": + receipt["roles"]["compilerLock"]["sha256"] = "d" * 64 + elif mutation == "extra-file": + report["identity"]["files"].append({"path": "/extra", "sha256": "d" * 64}) + elif mutation == "arguments": + report["identity"]["arguments"] = [ + qualify_mep.argument_blob("x", windows=False) + ] + elif mutation == "host": + report["identity"]["host"] = {"path": "/other", "sha256": "a" * 64} + elif mutation == "dirty-runner": + report["identity"]["runner"]["dirty"] = True + reseal(witness) + with pytest.raises(bundle.BundleError): + verify(witness) + + +def test_live_roles_use_physical_files(tmp_path: Path) -> None: + guest = tmp_path / "guest" + guest.write_bytes(b"actual artifact") + alias = tmp_path / "alias" + alias.symlink_to(guest) + record = {"path": str(alias), "sha256": qualify_mep.mep_tool.sha(guest)} + assert qualify_mep._bind_roles( + {"guest": {"path": str(guest), "sha256": record["sha256"]}}, + {"files": [record]}, + ) == {"guest": record} + + +def test_windows_paths_do_not_use_collector_filesystem() -> None: + assert qualify_mep._same_recorded_path( + "C:/runner/guest.exe", + r"\\?\C:\runner\guest.exe", + windows=True, + ) + assert not qualify_mep._same_recorded_path( + "C:/runner/guest.exe", + "C:/other/guest.exe", + windows=True, + ) + + +def wasm_witness(witness: dict[str, Any]) -> None: + receipt, report = witness["receipt"], witness["report"] + roles = receipt["roles"] + roles["adapter"] = {"path": "/producer/adapter", "sha256": "d" * 64} + for name, relative in ( + ("adapterLock", "tools/extension-qualification/Cargo.lock"), + ("adapterSource", "tools/extension-qualification/src/bin/mep_wasm_host.rs"), + ): + roles[name] = { + "path": f"/producer/{relative}", + "sha256": qualify_mep.mep_tool.sha(bundle.ROOT / relative), + } + receipt["runtime"], receipt["platform"] = "wasm", "portable" + receipt["invocation"] = { + "host": roles["adapter"]["path"], + "arguments": [roles["guest"]["path"]], + "windows": False, + } + report["identity"]["host"] = roles["adapter"] + report["identity"]["arguments"] = [ + qualify_mep.argument_blob(roles["guest"]["path"], windows=False) + ] + report["identity"]["files"] = list(roles.values()) + receipt["checkedIdentity"] = copy.deepcopy(report["identity"]) + reseal(witness) + + +def test_wasm_transfer_binds_adapter_and_guest_argument( + witness: dict[str, Any], +) -> None: + wasm_witness(witness) + assert verify(witness) == witness["report"] + # Even a matching receipt/report cannot substitute another argument for the + # concrete guest whose bytes the producer checked. + witness["receipt"]["invocation"]["arguments"] = ["/producer/other.wasm"] + witness["report"]["identity"]["arguments"] = [ + qualify_mep.argument_blob("/producer/other.wasm", windows=False) + ] + witness["receipt"]["checkedIdentity"] = copy.deepcopy(witness["report"]["identity"]) + reseal(witness) + with pytest.raises(bundle.BundleError): + verify(witness) + + +def test_failed_saved_recheck_never_emits_a_receipt( + witness: dict[str, Any], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + import subprocess + + candidate = tmp_path / "candidate" + candidate.mkdir() + (candidate / "guest").write_bytes(b"guest") + monkeypatch.setattr( + qualify_mep, + "_descriptor", + lambda *_: { + "gitCommit": "c" * 40, + "version": bundle.crate_version(), + "artifacts": [ + { + "platform": bundle._target(), + "filename": "guest", + "sha256": "a" * 64, + "claims": {"fixture": "claims"}, + } + ], + }, + ) + monkeypatch.setattr( + qualify_mep, + "_tool", + lambda _: { + "executable": "checker", + "kit": "kit", + "corpusSha256": "b" * 64, + }, + ) + results = iter( + [ + subprocess.CompletedProcess([], 0, "qualified", ""), + subprocess.CompletedProcess([], 1, "", "identity no longer matches"), + ] + ) + monkeypatch.setattr(qualify_mep.subprocess, "run", lambda *_, **__: next(results)) + output = tmp_path / "recheck.json" + with pytest.raises(bundle.BundleError, match="recheck failed"): + qualify_mep.qualify( + candidate, + "process", + tmp_path / "tool.json", + witness["path"], + output, + "c" * 40, + ) + assert not output.exists() + + +@pytest.mark.parametrize( + "missing", + [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", + "portable", + ], +) +def test_release_requires_every_platform_and_wasm_proof( + witness: dict[str, Any], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + missing: str, +) -> None: + from scripts.process_extension import PLATFORMS + + inputs = tmp_path / "inputs" + wasm_bundle, wasm_proofs = tmp_path / "wasm", tmp_path / "wasm-proofs" + monkeypatch.setattr( + qualify_mep, + "_descriptor", + lambda directory, _: ( + qualify_mep.mep_tool.object_file(directory / "release.json") + ), + ) + missing_path = None + for target in (*PLATFORMS, "portable"): + item = copy.deepcopy(witness) + is_wasm = target == "portable" + if is_wasm: + wasm_witness(item) + directory, evidence, prefix = wasm_bundle, wasm_proofs, "bundle-mep" + else: + root = inputs / f"morphir-python-process-{target}" + directory = root / ".morphir/build/extensions/python-process" + evidence = root / ".dev/qualification" + prefix = f"mep-{target}" + directory.mkdir(parents=True) + evidence.mkdir(parents=True) + guest = directory / "guest" + guest.write_bytes(target.encode()) + digest = qualify_mep.mep_tool.sha(guest) + receipt, report = item["receipt"], item["report"] + receipt["platform"] = target + receipt["artifactSha256"] = digest + receipt["roles"]["guest"]["sha256"] = digest + receipt["invocation"]["windows"] = target.endswith("windows-msvc") + report["identity"]["files"] = list(receipt["roles"].values()) + report["identity"]["host"] = receipt["roles"]["adapter" if is_wasm else "guest"] + receipt["checkedIdentity"] = copy.deepcopy(report["identity"]) + item["path"] = evidence / f"{prefix}-report.json" + receipt["reportFilename"] = item["path"].name + reseal(item) + path = evidence / f"{prefix}-recheck.json" + path.write_text(json.dumps(receipt)) + (directory / "release.json").write_text( + json.dumps( + { + "gitCommit": "c" * 40, + "artifacts": [ + { + "platform": target, + "filename": "guest", + "sha256": digest, + "claims": {"fixture": "claims"}, + } + ], + } + ) + ) + if target == missing: + missing_path = path + result = qualify_mep.verify_release(inputs, wasm_bundle, wasm_proofs, "c" * 40) + assert len(result["producerRechecks"]) == 6 + assert missing_path is not None + missing_path.unlink() + with pytest.raises((bundle.BundleError, OSError)): + qualify_mep.verify_release(inputs, wasm_bundle, wasm_proofs, "c" * 40) diff --git a/tests/unit/test_mep_tool.py b/tests/unit/test_mep_tool.py new file mode 100644 index 00000000..5be7e0fb --- /dev/null +++ b/tests/unit/test_mep_tool.py @@ -0,0 +1,31 @@ +"""Source-built checker identities must survive later Cargo rebuilds.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from scripts import extension_bundle as bundle +from scripts import mep_tool + +if TYPE_CHECKING: + from pathlib import Path + + +def test_frozen_checker_keeps_old_report_identity_when_build_output_changes( + tmp_path: Path, +) -> None: + executable = tmp_path / "morphir" + executable.write_bytes(b"qualified checker") + first = mep_tool.freeze_binary(executable, tmp_path / "cache") + executable.write_bytes(b"a later compatible rebuild") + second = mep_tool.freeze_binary(executable, tmp_path / "cache") + assert first != second + assert first.read_bytes() == b"qualified checker" + assert second.read_bytes() == b"a later compatible rebuild" + assert first.parent.name == mep_tool.sha(first) + assert mep_tool.freeze_binary(executable, tmp_path / "cache") == second + second.write_bytes(b"changed stored checker") + with pytest.raises(bundle.BundleError, match="integrity"): + mep_tool.freeze_binary(executable, tmp_path / "cache") diff --git a/tools/extension-qualification/README.md b/tools/extension-qualification/README.md index eea2af9d..14eacd00 100644 --- a/tools/extension-qualification/README.md +++ b/tools/extension-qualification/README.md @@ -91,6 +91,13 @@ The saved report records the actual host executable, arguments, guest bytes, checker, corpus, identities and observations. Its canonical recheck requires the same checker and all recorded identity paths while they are available. +The release producers automate this through `scripts.mep_tool` and +`scripts.qualify_mep`. Each producer rechecks its saved report locally before +uploading it. The Linux collector verifies the source/artifact/report identities +and successful producer receipt from the same workflow run; it does not rewrite +foreign filesystem paths or claim to re-run their filesystem recheck. See the +[release contract](../../docs/releasing.md#qualify-both-extension-runtimes). + Extism exposes a requests-only `handle` ABI. As in the pinned upstream `ExtismChannel`, notifications are not forwarded; an exit notification closes the bridge and unloads the guest. Those lifecycle observations qualify this From 15c4453993e46b048d3051b53699208ded79378a Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:13:53 -0500 Subject: [PATCH 18/26] fix(release): preserve declaration bytes on Windows --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 8a6f0f83..12af7521 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,7 @@ # Release receipts bind these inputs as bytes on every runner OS. rust/Cargo.lock text eol=lf +rust/extension.toml text eol=lf scripts/*.py text eol=lf .github/scripts/*.ps1 text eol=lf tools/extension-qualification/Cargo.lock text eol=lf From faf66f9a193e9cf35513aa502cdb9eb936528de0 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:15:52 -0500 Subject: [PATCH 19/26] build(mck): bound checker build disk usage Disable checker debug information and incremental objects. Local upstream development artifacts exceed 17 GiB; release runners also need the compiler and independent host builds. Keep dev optimization and record both settings in the checker receipt. --- scripts/mep_tool.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/mep_tool.py b/scripts/mep_tool.py index cd498edd..cca889a1 100644 --- a/scripts/mep_tool.py +++ b/scripts/mep_tool.py @@ -134,6 +134,7 @@ def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: "morphir", ], cwd=bundle.ROOT, + env=os.environ | {"CARGO_PROFILE_DEV_DEBUG": "0", "CARGO_INCREMENTAL": "0"}, stdout=stream, stderr=subprocess.STDOUT, timeout=1800, @@ -154,6 +155,8 @@ def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: "corpusSha256": sha(kit), "cargoLockSha256": sha(source / "Cargo.lock"), "profile": "dev", + "debugInfo": False, + "incremental": False, "rustc": command(["rustc", "-vV"]), "preparerSha256": sha(Path(__file__)), } From 63ba6ce53a10e3c978d16102c8eba9d208a59e23 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:18:01 -0500 Subject: [PATCH 20/26] fix(mck): decode reports independently of locale --- scripts/mep_tool.py | 2 +- tests/unit/test_mep_tool.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/scripts/mep_tool.py b/scripts/mep_tool.py index cca889a1..4c261255 100644 --- a/scripts/mep_tool.py +++ b/scripts/mep_tool.py @@ -22,7 +22,7 @@ def sha(path: Path) -> str: def object_file(path: Path) -> dict[str, Any]: """Read a required JSON object at the tool boundary.""" - value = json.loads(path.read_text()) + value = json.loads(path.read_bytes()) if not isinstance(value, dict): raise bundle.BundleError(f"expected an object: {path}") return value diff --git a/tests/unit/test_mep_tool.py b/tests/unit/test_mep_tool.py index 5be7e0fb..f45a9d58 100644 --- a/tests/unit/test_mep_tool.py +++ b/tests/unit/test_mep_tool.py @@ -2,6 +2,9 @@ from __future__ import annotations +import os +import subprocess +import sys from typing import TYPE_CHECKING import pytest @@ -29,3 +32,28 @@ def test_frozen_checker_keeps_old_report_identity_when_build_output_changes( second.write_bytes(b"changed stored checker") with pytest.raises(bundle.BundleError, match="integrity"): mep_tool.freeze_binary(executable, tmp_path / "cache") + + +def test_unicode_report_does_not_depend_on_producer_locale(tmp_path: Path) -> None: + report = tmp_path / "report.json" + report.write_bytes('{"opaqueId":"λ雪🙂"}'.encode()) + result = subprocess.run( + [ + sys.executable, + "-c", + "from pathlib import Path; import sys; " + "from scripts.mep_tool import object_file; " + "assert object_file(Path(sys.argv[1]))['opaqueId'] == " + "'\\u03bb\\u96ea\\U0001f642'", + str(report), + ], + env=os.environ + | { + "LC_ALL": "C", + "PYTHONUTF8": "0", + "PYTHONCOERCECLOCALE": "0", + }, + capture_output=True, + timeout=10, + ) + assert result.returncode == 0, result.stderr From 27bbf5eb1618febf370706c4478fd50792c16126 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:59:44 -0500 Subject: [PATCH 21/26] fix(mck): expose failed checker build diagnostics --- .github/workflows/ci.yml | 9 +++++++ .github/workflows/release.yml | 46 ++++++++++++++++++++++++++++++-- scripts/mep_tool.py | 50 ++++++++++++++++++++--------------- tests/unit/test_mep_tool.py | 21 ++++++++++++++- 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8e3367f..6ed0746f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,6 +265,7 @@ jobs: with: path: .dev/mep-tool/ key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} + restore-keys: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1- - name: Cache the independent WASM adapter build if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' @@ -295,6 +296,14 @@ jobs: --report .dev/qualification/bundle-mep-report.json \ --receipt .dev/qualification/bundle-mep-recheck.json + - name: Retain failed checker build diagnostics + if: failure() && hashFiles('.dev/mep-tool/*/build.log') != '' + uses: actions/upload-artifact@v6 + with: + name: mep-build-log-linux-x86_64 + path: .dev/mep-tool/*/build.log + include-hidden-files: true + - name: Upload the qualified process bundle and receipt uses: actions/upload-artifact@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9deef98c..ed3c1129 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,17 +10,50 @@ on: # Qualification only, including every native wheel platform. Publishing # requires a tag push. workflow_dispatch: + inputs: + mck-diagnostic: + description: Build only the shared MCK on Windows; never stage or publish a release + type: boolean + default: false permissions: contents: read concurrency: - group: release-${{ github.ref }} + group: ${{ inputs.mck-diagnostic && 'mck-diagnostic' || 'release' }}-${{ github.ref }} cancel-in-progress: false jobs: + mck-diagnostic: + name: Diagnose Windows shared MCK build + if: github.event_name == 'workflow_dispatch' && inputs.mck-diagnostic + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v3 + with: + install_args: rust python + - uses: actions/cache@v5 + with: + path: .dev/mep-tool/ + key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} + restore-keys: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1- + - name: Build the pinned canonical checker + run: python -m scripts.mep_tool --output .dev/qualification/mep-tool.json + - name: Retain checker setup diagnostics + if: always() + uses: actions/upload-artifact@v6 + with: + name: windows-mck-build-diagnostic + path: | + .dev/mep-tool/*/build.log + .dev/qualification/mep-tool.json + include-hidden-files: true + if-no-files-found: warn + qualify: name: Qualify release + if: inputs.mck-diagnostic != true uses: ./.github/workflows/ci.yml with: release-tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }} @@ -236,8 +269,9 @@ jobs: with: path: .dev/mep-tool/ key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} + restore-keys: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1- - - name: Run and independently recheck the canonical process MEP report + - name: Build the shared checker and qualify process MEP if: >- !matrix.reuse && (github.event_name == 'workflow_dispatch' || @@ -251,6 +285,14 @@ jobs: --report .dev/qualification/mep-${{ matrix.target }}-report.json \ --receipt .dev/qualification/mep-${{ matrix.target }}-recheck.json + - name: Retain failed checker build diagnostics + if: failure() && hashFiles('.dev/mep-tool/*/build.log') != '' + uses: actions/upload-artifact@v6 + with: + name: mep-build-log-${{ matrix.target }} + path: .dev/mep-tool/*/build.log + include-hidden-files: true + - name: Upload the qualified process bundle and receipt if: >- !matrix.reuse && diff --git a/scripts/mep_tool.py b/scripts/mep_tool.py index 4c261255..e72223b4 100644 --- a/scripts/mep_tool.py +++ b/scripts/mep_tool.py @@ -69,6 +69,34 @@ def command(arguments: list[str], *, cwd: Path | None = None) -> str: return result.stdout.strip() +def build_checker(source: Path, target: Path, log: Path) -> None: + """Build with bounded CI diagnostics and preserve the complete raw log.""" + with log.open("wb") as stream: + result = subprocess.run( + [ + "cargo", + "build", + "--locked", + "--manifest-path", + str(source / "Cargo.toml"), + "--target-dir", + str(target), + "-p", + "morphir", + ], + cwd=bundle.ROOT, + env=os.environ | {"CARGO_PROFILE_DEV_DEBUG": "0", "CARGO_INCREMENTAL": "0"}, + stdout=stream, + stderr=subprocess.STDOUT, + timeout=1800, + ) + if result.returncode: + tail = "\n".join( + log.read_bytes().decode("utf-8", errors="replace").splitlines()[-80:] + ) + raise bundle.BundleError(f"MCK build failed; full log {log}\n{tail[-16000:]}") + + def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: """Fetch source by Git identity and run Cargo even when a build cache exists. @@ -120,27 +148,7 @@ def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: log = cache / "build.log" # This is a correctness checker, not the implementation being benchmarked. # A dev-profile build reduces setup cost and is identified in the receipt. - with log.open("w") as stream: - result = subprocess.run( - [ - "cargo", - "build", - "--locked", - "--manifest-path", - str(source / "Cargo.toml"), - "--target-dir", - str(target), - "-p", - "morphir", - ], - cwd=bundle.ROOT, - env=os.environ | {"CARGO_PROFILE_DEV_DEBUG": "0", "CARGO_INCREMENTAL": "0"}, - stdout=stream, - stderr=subprocess.STDOUT, - timeout=1800, - ) - if result.returncode: - raise bundle.BundleError(f"MCK build failed; see {log}") + build_checker(source, target, log) if command(["git", "status", "--porcelain"], cwd=source): raise bundle.BundleError("MCK build changed its pinned source") executable = target / "debug" / ("morphir.exe" if os.name == "nt" else "morphir") diff --git a/tests/unit/test_mep_tool.py b/tests/unit/test_mep_tool.py index f45a9d58..a7bc9513 100644 --- a/tests/unit/test_mep_tool.py +++ b/tests/unit/test_mep_tool.py @@ -5,7 +5,7 @@ import os import subprocess import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import pytest @@ -57,3 +57,22 @@ def test_unicode_report_does_not_depend_on_producer_locale(tmp_path: Path) -> No timeout=10, ) assert result.returncode == 0, result.stderr + + +def test_failed_build_exposes_diagnostics_and_retains_full_log( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + diagnostic = ( + "earlier build output\n" * 100 + "error: missing Windows prerequisite λ\n" + ).encode() + + def fail(arguments: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + kwargs["stdout"].write(diagnostic) + return subprocess.CompletedProcess(arguments, 1) + + monkeypatch.setattr(mep_tool.subprocess, "run", fail) + log = tmp_path / "build.log" + with pytest.raises(bundle.BundleError, match="missing Windows prerequisite λ"): + mep_tool.build_checker(tmp_path, tmp_path / "target", log) + assert log.read_bytes() == diagnostic From 0af53391b95fc514af121bacea21097ad349a52d Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:04:34 -0500 Subject: [PATCH 22/26] fix(mck): shorten Windows dependency checkout paths --- .github/workflows/release.yml | 12 ++++++++++-- scripts/mep_tool.py | 11 ++++++++++- tests/unit/test_mep_tool.py | 21 +++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed3c1129..2e327a6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,10 +35,14 @@ jobs: install_args: rust python - uses: actions/cache@v5 with: - path: .dev/mep-tool/ + path: | + .dev/mep-tool/ + ${{ runner.temp }}/mck key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} restore-keys: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1- - name: Build the pinned canonical checker + env: + MORPHIR_MCK_CARGO_HOME: ${{ runner.temp }}/mck run: python -m scripts.mep_tool --output .dev/qualification/mep-tool.json - name: Retain checker setup diagnostics if: always() @@ -267,7 +271,9 @@ jobs: startsWith(github.ref, 'refs/tags/extension/python/v')) uses: actions/cache@v5 with: - path: .dev/mep-tool/ + path: | + .dev/mep-tool/ + ${{ runner.os == 'Windows' && format('{0}/mck', runner.temp) || '' }} key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} restore-keys: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1- @@ -277,6 +283,8 @@ jobs: (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/extension/python/v')) shell: bash + env: + MORPHIR_MCK_CARGO_HOME: ${{ runner.os == 'Windows' && format('{0}/mck', runner.temp) || '' }} run: | python -m scripts.mep_tool --output .dev/qualification/mep-tool.json python -m scripts.qualify_mep \ diff --git a/scripts/mep_tool.py b/scripts/mep_tool.py index e72223b4..5031c00a 100644 --- a/scripts/mep_tool.py +++ b/scripts/mep_tool.py @@ -71,6 +71,14 @@ def command(arguments: list[str], *, cwd: Path | None = None) -> str: def build_checker(source: Path, target: Path, log: Path) -> None: """Build with bounded CI diagnostics and preserve the complete raw log.""" + environment = os.environ | { + "CARGO_PROFILE_DEV_DEBUG": "0", + "CARGO_INCREMENTAL": "0", + } + if cache_home := environment.get("MORPHIR_MCK_CARGO_HOME"): + # Cargo's Git checkout includes a 192-character Gleam snapshot path. + # Windows CI uses RUNNER_TEMP/mck rather than the longer user profile. + environment["CARGO_HOME"] = str(Path(cache_home).resolve()) with log.open("wb") as stream: result = subprocess.run( [ @@ -85,7 +93,7 @@ def build_checker(source: Path, target: Path, log: Path) -> None: "morphir", ], cwd=bundle.ROOT, - env=os.environ | {"CARGO_PROFILE_DEV_DEBUG": "0", "CARGO_INCREMENTAL": "0"}, + env=environment, stdout=stream, stderr=subprocess.STDOUT, timeout=1800, @@ -165,6 +173,7 @@ def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: "profile": "dev", "debugInfo": False, "incremental": False, + "cargoHomeOverride": os.environ.get("MORPHIR_MCK_CARGO_HOME") or None, "rustc": command(["rustc", "-vV"]), "preparerSha256": sha(Path(__file__)), } diff --git a/tests/unit/test_mep_tool.py b/tests/unit/test_mep_tool.py index a7bc9513..b68e275a 100644 --- a/tests/unit/test_mep_tool.py +++ b/tests/unit/test_mep_tool.py @@ -76,3 +76,24 @@ def fail(arguments: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str with pytest.raises(bundle.BundleError, match="missing Windows prerequisite λ"): mep_tool.build_checker(tmp_path, tmp_path / "target", log) assert log.read_bytes() == diagnostic + + +def test_short_cargo_cache_is_scoped_to_the_checker_build( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + original, short = tmp_path / "ordinary-cache", tmp_path / "short" + monkeypatch.setenv("CARGO_HOME", str(original)) + monkeypatch.setenv("MORPHIR_MCK_CARGO_HOME", str(short)) + environments = [] + + def compile( + arguments: list[str], **kwargs: Any + ) -> subprocess.CompletedProcess[str]: + environments.append(kwargs["env"]) + return subprocess.CompletedProcess(arguments, 0) + + monkeypatch.setattr(mep_tool.subprocess, "run", compile) + mep_tool.build_checker(tmp_path, tmp_path / "target", tmp_path / "build.log") + assert environments[0]["CARGO_HOME"] == str(short.resolve()) + assert os.environ["CARGO_HOME"] == str(original) From 4520858c7f4cd1a7cd1801ea0695c7e148e0b70b Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:13:38 -0500 Subject: [PATCH 23/26] ci(mck): inspect Windows build checkout changes --- .github/workflows/release.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e327a6b..00ecc559 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,18 @@ jobs: env: MORPHIR_MCK_CARGO_HOME: ${{ runner.temp }}/mck run: python -m scripts.mep_tool --output .dev/qualification/mep-tool.json + - name: Inspect checkout changes after the Windows build + if: always() + shell: pwsh + run: | + Get-ChildItem .dev/mep-tool/*/source -Directory | ForEach-Object { + $source = $_.FullName + git -C $source status --short + git -C $source diff --stat + git -C $source diff --ignore-space-at-eol --stat + git -C $source submodule foreach --recursive 'git status --short' + git -C $source submodule foreach --recursive 'git diff --stat' + } - name: Retain checker setup diagnostics if: always() uses: actions/upload-artifact@v6 From e338a68694061b288db91fc88c3c98573096178e Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:14:14 -0500 Subject: [PATCH 24/26] fix(ci): inspect checker source directories directly --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00ecc559..5f37282d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,8 +48,9 @@ jobs: if: always() shell: pwsh run: | - Get-ChildItem .dev/mep-tool/*/source -Directory | ForEach-Object { + Get-Item .dev/mep-tool/*/source | ForEach-Object { $source = $_.FullName + Write-Output "Pinned MCK source: $source" git -C $source status --short git -C $source diff --stat git -C $source diff --ignore-space-at-eol --stat From bce5d859b524edf452b3031e2e2e56fc32f4da71 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:27:54 -0500 Subject: [PATCH 25/26] fix(mck): configure submodule path handling --- scripts/mep_tool.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/scripts/mep_tool.py b/scripts/mep_tool.py index 5031c00a..a61fe741 100644 --- a/scripts/mep_tool.py +++ b/scripts/mep_tool.py @@ -69,6 +69,12 @@ def command(arguments: list[str], *, cwd: Path | None = None) -> str: return result.stdout.strip() +def configure_checkout(checkout: Path) -> None: + """Keep parent and submodule byte/path handling consistent, without resets.""" + command(["git", "config", "core.autocrlf", "false"], cwd=checkout) + command(["git", "config", "core.longpaths", "true"], cwd=checkout) + + def build_checker(source: Path, target: Path, log: Path) -> None: """Build with bounded CI diagnostics and preserve the complete raw log.""" environment = os.environ | { @@ -122,6 +128,10 @@ def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: command(["git", "config", "core.longpaths", "true"], cwd=source) if command(["git", "remote", "get-url", "origin"], cwd=source) != pin["repository"]: raise bundle.BundleError("MCK source cache has another repository") + configure_checkout(source) + rust_source = source / "ecosystem/morphir-rust" + if (rust_source / ".git").exists(): + configure_checkout(rust_source) command(["git", "fetch", "--depth", "1", "origin", pin["rev"]], cwd=source) # Refuse tracked/untracked modifications rather than resetting somebody's work. if command(["git", "status", "--porcelain"], cwd=source): @@ -145,6 +155,12 @@ def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: ], cwd=source, ) + configure_checkout(rust_source) + state = command(["git", "status", "--porcelain"], cwd=source) + if state: + raise bundle.BundleError( + f"MCK checkout is not clean before compilation:\n{state}" + ) kit = source / pin["kit"] if ( sha(source / "Cargo.lock") != pin["cargo_lock_sha256"] @@ -157,8 +173,9 @@ def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: # This is a correctness checker, not the implementation being benchmarked. # A dev-profile build reduces setup cost and is identified in the receipt. build_checker(source, target, log) - if command(["git", "status", "--porcelain"], cwd=source): - raise bundle.BundleError("MCK build changed its pinned source") + state = command(["git", "status", "--porcelain"], cwd=source) + if state: + raise bundle.BundleError(f"MCK build changed its pinned source:\n{state}") executable = target / "debug" / ("morphir.exe" if os.name == "nt" else "morphir") executable = freeze_binary(executable, cache / "bin") receipt = { From d16b917a64bfbe0d767478085506d244c43683e7 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:37:19 -0500 Subject: [PATCH 26/26] docs(mep): record qualified dual-runtime delivery Sync the verified model transport and process contracts. Record all-platform canonical MCK, archive, host and promotion evidence. Keep operational publication and archival pending maintainer approval. --- docs/releasing.md | 17 ++++++ .../add-process-mep-extension/design.html | 21 +++---- .../add-process-mep-extension/tasks.md | 19 +++--- openspec/specs/python/extension/spec.md | 43 +++++++++++++- openspec/specs/python/model-transport/spec.md | 58 +++++++++++++++++++ 5 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 openspec/specs/python/model-transport/spec.md diff --git a/docs/releasing.md b/docs/releasing.md index 09adbb1f..626a0267 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -66,6 +66,9 @@ pins the merged upstream source, tree, lockfile, Python JSON corpus and complete 91-case identity set. `scripts.mep_tool` verifies that source and builds its CLI; it never replaces upstream expectations. Cargo may reuse compatible cached objects. Each report names an immutable copy of the built checker. +The published host CLI and this source-built checker can have the same package +version. Use the pinned source commit for the Python JSON profile; the older +published CLI does not contain that profile. `scripts.qualify_mep` runs `morphir mck mep guest-run`, then `report-check` with the original checker and all identity files still available. A failed run or @@ -89,6 +92,20 @@ final release retains raw MCK reports and recheck receipts, assembly and runtime receipts, and the host timing evidence. Publication verifies final checksums. Historical WASM releases remain available with their original bytes. +## Investigate Windows checker setup + +Dispatch the Release workflow with `mck-diagnostic=true` to build only the shared +checker on Windows and retain its complete build log, tool receipt and checkout +status. This mode skips candidate qualification and publication. Leave the input +false for a full release qualification run. + +The Windows checker uses a short Cargo cache under `RUNNER_TEMP/mck` so locked +Git dependencies fit Windows path limits. The override applies only to checker +compilation. Both the parent checkout and initialized Rust submodule use local +Git settings for LF content and long paths. Preparation requires a clean source +tree before and after building; it refuses genuine source edits without +resetting them. These settings preserve the pinned source, lockfile and corpus. + ## Reproduce the installer checks Check the published configurations without changing your global mise setup: diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 06faf248..109387eb 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -22,31 +22,32 @@

add-process-mep-extension

-

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Applying; SDK published and Python MCK profile merged

- -

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The baseline published SDK 0.3.0 exposed NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type used a numeric ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The baseline Python0.4.2 implementation declared legacy MEP0.1 only. Candidate0.5.0 uses publishedSDK0.4.0 and opts into the exact JSON model draft after native/WASM executable qualification. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

+

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Implementation qualified; landing and component publication pending

+ +

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The baseline published SDK 0.3.0 exposed NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type used a numeric ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The baseline Python0.4.2 implementation declared legacy MEP0.1 only. Candidate0.5.0 uses publishedSDK 0.4.0 and opts into the exact JSON model draft after native/WASM executable qualification. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. The maintainer approved publication at that exact merge. Tag crates/morphir-extension-sdk/v0.4.0 triggered successful workflow40695514380; Cargo independently downloaded the published0.4.0 crate. The compiler now pins registry dependency =0.4.0, without a sibling or Git SDK dependency. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with canonical empty arguments selecting stdio by default. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

-

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The baseline executable MCK accepted only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. The maintainer approved a separate Python JSON profile, merged in upstreamPR1109 as85cf4352eeb517fefb85cda10961f749cce51004 and tracked by closed morphir-av8s. Its treec42c55ef3aa9d0190850251dac97bd094cebb4c4 equals the independently audited328c0216 source tree. The source, lockfile, corpus and complete91-case identity set are pinned in rust/extension.toml. Existing MoonBit rich-Ion checks remain unchanged. The clean committed328c0216 shared checker independently qualified all91observations for both actual release-native and WASM0.5.0 bytes, and saved report-check passed for both. Review corrected two non-isolated negative vectors; reports now reach invalid_base64 and unsupported_ir_version. The WASM bridge passes raw request/response bytes; notification silence and exit qualify the adapter/guest route because Extism exposes a requests-only handle ABI. The merged85cf4352 checker also passes both actual runtime candidates and saved rechecks. Release producers now run the canonical suite and saved recheck before upload. No unit-test result substitutes for the executable suite; the replacement five-platform candidate run remains required.

-

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. The0.5.0 candidate advertises exact0.2.0-draft.1 and legacy0.1 after publishedSDK adoption and passing independent native/WASM reports. Extension0.4.2 remains the published legacy release. Candidate platform and promotion assurance remain in progress.

+

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The baseline executable MCK accepted only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. The maintainer approved a separate Python JSON profile, merged in upstreamPR1109 as85cf4352eeb517fefb85cda10961f749cce51004 and tracked by closed morphir-av8s. Its treec42c55ef3aa9d0190850251dac97bd094cebb4c4 equals the independently audited328c0216 source tree. The source, lockfile, corpus and complete91-case identity set are pinned in rust/extension.toml. Existing MoonBit rich-Ion checks remain unchanged. The clean committed328c0216 shared checker independently qualified all91observations for both actual release-native and WASM0.5.0 bytes, and saved report-check passed for both. Review corrected two non-isolated negative vectors; reports now reach invalid_base64 and unsupported_ir_version. The WASM bridge passes raw request/response bytes; notification silence and exit qualify the adapter/guest route because Extism exposes a requests-only handle ABI. The merged85cf4352 checker also passes both actual runtime candidates and saved rechecks. Release producers now run the canonical suite and saved recheck before upload. No unit-test result substitutes for the executable suite; the replacement five-platform candidate run 40726356123 now passes.

+

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. The0.5.0 candidate advertises exact0.2.0-draft.1 and legacy0.1 after publishedSDK adoption and passing independent native/WASM reports. Extension0.4.2 remains the published legacy release. All five process platforms, portable WASM and final candidate promotion pass in run 40726356123 at bce5d85. Component publication remains subject to exact maintainer approval.

Build and release assessment

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. A dependent read-only assembly job requires all five byte-bound installed-host and executable MCK receipts plus the WASM reference and its MCK receipt. The existing extension promotion job stages both runtime asset sets and their evidence; only an approved extension tag triggers publication. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

- +
Risk and deadlineExecution and ownerEvidence and failure response
Incorrect shared compiler or process bytes, before mergeExisting Linux PR bundle category, repository maintainersCanonical bundle probe, isolated installation, deleted source repository, offline v3/v4 comparison with the exact native wheel; failure blocks the selected aggregate.
Platform-specific executable or wheel failure, before promotionExisting five-platform release matrix, explicit manual qualification and extension tag eventsNative target and compiler/toolchain identity, exact digest/claims/source receipts and installed host comparisons. Missing, failed or skipped selected producers block extension publication through its needs graph.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteSDK0.4.0 is published and exact-pinned. The Python profile inPR1109 passes actual native/WASM qualification; merged-source pin is recorded; producers run canonical guest-run and saved report-check with all original identities available. Platform packaging does not establish draft qualification. Assembly verifies the successful same-workflow producer recheck, exact report/artifact/source/checker/lock/corpus identities, all91case identities and staged discovery claims. It preserves Windows/macOS paths and does not claim a Linux filesystem recheck of foreign paths. Missing, stale, failed or mismatched evidence blocks the dependent promotion job.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteSDK 0.4.0 is published and exact-pinned. The Python profile inPR1109 passes actual native/WASM qualification; merged-source pin is recorded; producers run canonical guest-run and saved report-check with all original identities available. Platform packaging does not establish draft qualification. Assembly verifies the successful same-workflow producer recheck, exact report/artifact/source/checker/lock/corpus identities, all91case identities and staged discovery claims. It preserves Windows/macOS paths and does not claim a Linux filesystem recheck of foreign paths. Missing, stale, failed or mismatched evidence blocks the dependent promotion job.

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Maturin builds the CI wheel from an isolated source archive, so a hook alone did not share registry dependency objects. Run 40672914229 showed two compilations: 3m07s for the process guest and 2m57s for the archive wheel; the whole producer took 7m07s, compared with 2m57s for earlier job 122760321213. An explicit Cargo target directory now allows compatible registry objects to be reused across those source paths while retaining the independent archive build. The replacement b38e00e Linux producer took 5m01s versus 7m07s, and the two reported compile durations changed from 188/194 seconds to 188/58 seconds. This single observation does not establish a universal speedup. Local path-dependent compiler objects may still rebuild because their source identity differs. macOS and Windows use the same native target and deployment configuration as their wheel producer. Cached CLI bytes are checked against the digest-verified archive before execution. Host receipts retain normalized IR and generated artifact digests, actual CLI and qualification-script identity. Byte-bound lockfiles and script sources use LF on every checkout platform. Artifact ZIP transfer restores only the declared executable mode; payload digests and the original producer mode remain checked. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

- + - +
CriterionWeightCurrent assessment
Risk and coverage fit253: focused refusals, installed macOS checks and the b38e00e five-platform matrix pass; replacement source 820c9d0 also passes Windows static-CRT and installed-integrity proof.
Feedback and placement203: one Linux PR path; broader work stays manual/release. Added CI latency not yet measured.
Release evidence and trust20U: source-built canonical MCK and transfer refusals pass locally; final five-platform run and promotion assembly remain pending.
Release evidence and trust204: all six canonical producer/rechecks and final promotion pass; actual checksums, archive payloads/modes and transferred evidence are independently verified.
Execution efficiency153: compatible target reuse reduced one observed Linux producer from 7m07s to 5m01s; platform-specific CRT objects remain separate.
Ownership and structure104: existing bundle and native-wheel categories, explicit needs, repository maintainers own failures.
Operability and recovery102: candidate refusal tests pass; platform rerun/transfer and final promotion refusals pending.
Operability and recovery104: source/path setup failures are diagnosed and fixed; missing, stale, altered and incomplete evidence is refused; full platform transfer and promotion pass.
-

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. Replacement source 820c9d0 passed all five producers and assembly in run 40685569455. Its Windows dependency proof admits only OS DLLs, with no VC or Python runtime imports. All eleven combined release files, the five producer receipts and the Windows checker proof were independently matched to the collected assembly hashes. Runtime switching, installed-byte tampering and foreign-platform refusal passed on the assembled Linux candidate.

Total withheld while 20 weighted points remain unknown. This is a provisional workflow assessment, not release eligibility or measured performance. Local full Python/Rust checks and actionlint validate source changes; real matrix runs must supply platform and timing evidence. No native speed claim follows from this build design.

-

Download assets and transformed-byte checks

The assembly category stages all five raw executables with checksums, a separately named process descriptor, and one deterministic gzip/tar download containing the canonical eleven-file process bundle. Archive metadata uses fixed ownership/timestamps and explicit executable permissions; every copied and archived payload must retain its original digest. Corrupt transfers, mixed source/claims/platforms, symlinks and extra files are refused without leaving partial assets. The candidate receipt and final checksum list bind the download bytes to assembly. The existing runtime-selection and source-pinned host checks consume the extracted archive, preserving the build-once contract. Actual 820c9d0 archive bytes pass local offline switching, installed-byte refusal and unsupported-platform refusal. The same manual/extension-tag workflow now requires canonical producer MCK rechecks before assembly. Final promotion copies verified raw process assets, their separate descriptor/archive, WASM assets and all producer evidence, then seals their final names. Manual runs stage this exact release set without publishing. An approved extension tag publishes it after all selected jobs pass.

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

+

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. Replacement source 820c9d0 passed all five producers and assembly in run 40685569455. Its Windows dependency proof admits only OS DLLs, with no VC or Python runtime imports. All eleven combined release files, the five producer receipts and the Windows checker proof were independently matched to the collected assembly hashes. Runtime switching, installed-byte tampering and foreign-platform refusal passed on the assembled Linux candidate.

The current candidate supplies the previously missing platform, conformance, transfer and promotion evidence. The assessment concerns implementation assurance; it is not publication authorization or a general performance result. Per-tag qualification still gates publication at the exact approved source. No native speed claim follows from this build design.

+

Windows checker setup and source integrity

The full candidate run exposed Cargo/libgit2 failing on a locked Gleam snapshot path of 265 characters under the default Windows Cargo home. A short checker-only cache under RUNNER_TEMP/mck reduces the longest pinned path to 252 characters. A second diagnostic showed that transient parent Git options did not persist into the Rust submodule: inaccessible long fixtures produced false deleted/modified statuses. Preparation now configures LF and long-path handling locally in each checkout and requires clean source before and after compilation. An actual tracked-file edit is still refused and preserved. No source patch, ignored diff, changed corpus or reduced platform requirement was accepted.

Diagnostic run 40726235322 passes with clean parent/submodule state and a prepared Windows checker receipt. The manual mck-diagnostic mode builds only that checker and retains raw compiler and Git diagnostics; it cannot qualify or publish a release. Its separate concurrency group permits diagnosis while the full platform run continues. The normal workflow remains the promotion authority. The first Linux preparation of the shared checker and WASM adapter took 12m24s, followed by 1m31s for canonical execution and saved rechecks; these are assurance costs, not guest performance measurements.

Download assets and transformed-byte checks

The assembly category stages all five raw executables with checksums, a separately named process descriptor, and one deterministic gzip/tar download containing the canonical eleven-file process bundle. Archive metadata uses fixed ownership/timestamps and explicit executable permissions; every copied and archived payload must retain its original digest. Corrupt transfers, mixed source/claims/platforms, symlinks and extra files are refused without leaving partial assets. The candidate receipt and final checksum list bind the download bytes to assembly. The existing runtime-selection and source-pinned host checks consume the extracted archive, preserving the build-once contract. Actual 820c9d0 archive bytes pass local offline switching, installed-byte refusal and unsupported-platform refusal. The same manual/extension-tag workflow now requires canonical producer MCK rechecks before assembly. Final promotion copies verified raw process assets, their separate descriptor/archive, WASM assets and all producer evidence, then seals their final names. Manual runs stage this exact release set without publishing. An approved extension tag publishes it after all selected jobs pass.

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

+

Qualification result

All 16 implementation tasks are complete. Full candidate run 40726356123 succeeds at source bce5d859b524edf452b3031e2e2e56fc32f4da71. Later documentation, task and spec-sync edits preserve the production code and check definitions. Publication remains pending exact maintainer approval and per-tag validation.

ObligationObserved evidence
Platform and API agreementAll five process producers and WASM pass installed offline v3/v4 operation checks. Full process diagnostic records match native Python API 0.1.2 with compiler 0.5.0. Windows runtime imports pass.
Canonical current/legacy MEPSix reports each contain 91 required observations with successful saved rechecks from clean merged85cf4352 source, totaling 546. Local transfer verification preserves original paths and exact source/tool/artifact/claim identities.
Assembly and download formFive raw process targets combine into an eleven-file canonical bundle and deterministic archive. Extracted-archive switching, installed-byte tampering and foreign-platform refusal pass. An actual four-platform input set is refused.
Real hosts and timingsPinned Rust host d51daa8 passes actual WASM/process legacy lifecycle, v3/v4 operations, diagnostics, timeout and child cleanup. Ten raw samples, five per small/medium workload, retain inputs and exact toolchain/artifact identities. No general speed claim.
Promotion and retentionFinal staging has 39 assets plus SHA256SUMS. Every final checksum, six raw report/recheck pairs and all eleven archive payload/mode entries are independently checked. Historical 0.4.0/0.4.2 WASM assets remain uploaded with their original digests.

Local validation passes 2632 tests with five intentional skips, strict typing, BDD, focused evidence/assembly tests, actionlint, site checks and strict OpenSpec validation. SDK 0.4.0 is published; Python/native/extension tags have not yet been published. The OpenSpec change remains active until that operational follow-up is complete.

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk0.4.0: canonical request IDs, native session dispatch and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 3b2479e3..6e1a880d 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -1,6 +1,6 @@ # Tasks -This change is being applied. SDK 0.4.0 is published and the separate upstream Python JSON MCK profile is merged. Final platform qualification and release promotion remain incomplete. +This change is being applied. SDK 0.4.0 is published and the separate upstream Python JSON MCK profile is merged. All implementation and candidate qualification tasks are complete. Landing and approved component publication remain in progress. ## 1. Negotiate current and legacy MEP (morphir-5hxy.1) @@ -22,21 +22,22 @@ This change is being applied. SDK 0.4.0 is published and the separate upstream P ## 4. Distribute both runtime choices (morphir-5hxy.4) -- [ ] 4.1 Build process artifacts for each advertised platform and retain the WASM artifact; verify both use the shared compiler and produce matching supported results and diagnostics. +- [x] 4.1 Build process artifacts for each advertised platform and retain the WASM artifact; verify both use the shared compiler and produce matching supported results and diagnostics. - [x] 4.2 Stage canonical runtime/platform/mode/argument/digest/claim descriptors and source receipts with pinned tooling; verify host readers, installed-byte integrity, offline activation and explicit unsupported-platform refusal. -- [ ] 4.3 Add release assurance and runtime-selection documentation alongside each artifact producer, including exact native/CLI pairing where affected; verify missing or mismatched evidence blocks promotion and historical WASM assets remain available. +- [x] 4.3 Add release assurance and runtime-selection documentation alongside each artifact producer, including exact native/CLI pairing where affected; verify missing or mismatched evidence blocks promotion and historical WASM assets remain available. ## 5. Qualify host interoperability (morphir-5hxy.5) -- [ ] 5.1 Run the pinned executable MEP guest suite for both runtimes and protocol offers, including supported model operations and negative admission; retain source/artifact-bound conformance receipts. +- [x] 5.1 Run the pinned executable MEP guest suite for both runtimes and protocol offers, including supported model operations and negative admission; retain source/artifact-bound conformance receipts. - [x] 5.2 Qualify the concrete Rust host process and WASM routes using a pinned compatible release or source build; verify initialization, v3/v4 operations, parity, timeout and child cleanup with a clean child environment. - [x] 5.3 Test documented MoonBit routes independently, record prerequisite upstream work for any unsupported path and publish only evidenced compatibility claims; verify documentation recipes against the specified host revisions. - [x] 5.4 Measure representative cold-start and compile/generate workloads for process and WASM without making an unmeasured speed claim; retain the reproducible inputs, toolchain and timing report. ## Workflow follow-up -- Implementation is in progress under the active apply goal. The maintainer approved both prerequisites. SDK0.4.0 is published and registry-pinned. Upstream Python JSON MCK profilePR1109 merged as85cf4352 with audited treec42c55ef, pinned in rust/extension.toml, and preserves MoonBit rich-Ion checks; independent actual release-native/WASM0.5.0 runs and saved rechecks pass91observations each. Producer run/recheck and the same-workflow transfer gate are implemented; the final five-platform run remains pending. -- Task 4.1 now requires a source/artifact-bound framed diagnostic receipt from every process producer. Actual macOS process bytes match the installed Python API's complete v3/v4 diagnostic records with a clean child environment. Missing, changed, stale or mismatched diagnostic proofs block assembly. The checkbox remains open until the replacement platform run supplies all five receipts. -- Task 4.3 now stages deterministic download assets and checksums alongside assembly. The all-platform archive preserves exact bytes, canonical `release.json` and executable permissions; real runtime switching, installed integrity and platform refusal pass after extracting actual 820c9d0 candidate bytes. CI now tests that packaged form. Archive staging is separate from the still-pending independent MCK promotion gate and GitHub process publication. -- Source-pinned legacy Rust host checks pass for both actual b38e00e runtime artifacts, including v3/v4 parity, timeout and cleanup. Five release-host timing samples per workload retain exact inputs and raw reports in `.dev/qualification/timings-b38-release/`; the manual/release candidate job owns reruns and artifact retention. MoonBit remains unsupported at the documented pin, with explicit v3/v4 envelope refusals reverified by run 40685569455. These checks do not substitute for task 5.1's canonical executable conformance. -- All merges are approved. Exact SDK0.4.0 tag publication was separately approved and completed; Python/native/extension release tags still require maintainer approval; sync/archive only after implementation and qualification are complete. +- All 16 implementation tasks are complete. Full candidate [40726356123](https://github.com/finos/morphir-python/actions/runs/40726356123) succeeded at `bce5d859b524edf452b3031e2e2e56fc32f4da71`. The five process platforms and portable WASM each pass 91 canonical observations and saved rechecks, totaling 546 observations. All transferred receipts were independently checked locally against current source, helper, corpus, checker, lockfile, artifact and claim identities. +- Exact native `0.1.2` / CLI `0.1.1` pairing and compiler/extension `0.5.0` are qualified. Every process producer passes installed offline operation and full structured diagnostic parity with the Python API. Windows imports only allowed OS runtimes. The extracted process archive passes runtime switching, installed-byte integrity and unsupported-platform refusal. +- Final promotion stages 39 assets plus `SHA256SUMS`. Local audit verifies every final checksum, all 11 canonical process archive members, payload hashes, executable modes and all six raw MCK report/recheck pairs. A real four-platform input set is refused for missing Intel evidence. Historical WASM releases `0.4.0` and `0.4.2` remain available with their recorded digests. +- Source-pinned Rust host checks and ten fresh timing samples for small/medium workloads pass on the actual candidate bytes. Raw inputs, reports and toolchain/build identities are retained in the candidate and final host-evidence archive. No general performance claim is made. MoonBit routes remain unsupported at the documented pin with checked limitations. +- SDK `0.4.0` publication and the separate upstream Python JSON MCK profile are complete. The Windows checker build passes with short scoped Cargo storage and explicit parent/submodule Git settings; real source changes are still refused. No corpus change or integrity exception was accepted. +- Main specs are synced. All merges and admin bypass are approved. Python/native/extension release tags require exact maintainer approval once the merged source is concrete. Keep this change active until publication and final verification; archive afterward. Do not treat staged assets as published releases. diff --git a/openspec/specs/python/extension/spec.md b/openspec/specs/python/extension/spec.md index f8ca615e..1d288aeb 100644 --- a/openspec/specs/python/extension/spec.md +++ b/openspec/specs/python/extension/spec.md @@ -14,12 +14,16 @@ The migrated crate SHALL retain the `morphir-python-binding` package name and na - **THEN** it builds and invokes compile and generate using declared core/SDK dependencies without local source overrides or Python development libraries ### Requirement: MEP behavior remains compatible -The extension SHALL retain identity `morphir-python`, Python language/target identifiers, initialization and capability negotiation, compile/generate methods and advertised IR v3/v4 support. Native Rust, Python and packaged WASM invocations SHALL agree on supported model semantics and language diagnostic codes. (bead: morphir-ukvn.1.6) +The extension SHALL retain identity `morphir-python`, Python language/target identifiers, initialization and capability negotiation, legacy compile/generate methods and advertised IR v3/v4 support. Native Rust, Python, packaged WASM and process invocations SHALL agree on supported model semantics and language diagnostic codes. New model methods MUST require their negotiated draft and admitted capabilities. (bead: morphir-5hxy) #### Scenario: Existing host installs the replacement bundle - **WHEN** a compatible Morphir host installs the bundle built from morphir-python - **THEN** initialization, capability discovery, Python compilation and Python generation succeed through the existing protocol +#### Scenario: Users select different supported runtimes +- **WHEN** qualified WASM and process guests handle equivalent supported compilation or generation requests +- **THEN** they agree on compiler identity, supported semantics, deterministic artifacts and language diagnostic codes + ### Requirement: Python installation hides Rust implementation details Installing `morphir-python` on qualified CPython platforms SHALL install its compatible native compiler dependency automatically from wheels. Compilation and generation MUST require neither a Rust toolchain nor a separately installed Morphir CLI at runtime. The `morphir` distribution SHALL keep zero third-party runtime dependencies. (bead: morphir-ukvn.1.6) @@ -51,3 +55,40 @@ The cutover SHALL remove the Python binding implementation, workspace membership #### Scenario: Rust workspace after removal - **WHEN** the replacement provider has passed migration qualification and the coordinated removal lands - **THEN** the morphir-rust workspace and its CI remain valid with no Python compiler implementation or references requiring the former crate directory + +### Requirement: A native process guest serves framed MEP sessions +A released native process guest SHALL serve JSON-RPC MEP over Content-Length-framed stdin/stdout without CPython or Rust installed by the user. It MUST enforce documented header/body bounds, UTF-8 byte lengths, request identity and the SDK lifecycle. Stdout MUST contain only protocol frames; notifications MUST NOT receive responses. (bead: morphir-5hxy) + +#### Scenario: Multiple operations in one process +- **WHEN** a host initializes the guest, performs supported operations, requests shutdown and sends exit +- **THEN** responses retain request IDs, session capabilities remain fixed, and the process exits cleanly + +#### Scenario: Numeric and string request identity +- **WHEN** a supported request uses either a numeric or string JSON-RPC ID +- **THEN** its response preserves that exact ID type and value in both runtime routes + +#### Scenario: Malformed or oversized frame +- **WHEN** a frame has conflicting lengths, truncated data, invalid JSON, invalid UTF-8 or exceeds a declared bound +- **THEN** the guest refuses it without unbounded allocation or logging non-protocol text to stdout + +#### Scenario: Operation outside its lifecycle +- **WHEN** an operation is requested before initialization or after shutdown +- **THEN** the guest applies the protocol lifecycle refusal and does not invoke the compiler + +### Requirement: Both runtime choices have qualified release artifacts +Extension releases SHALL retain portable WASM and provide qualified process artifacts for each advertised process platform. Descriptors MUST bind runtime, platform, arguments, executable mode, digest, protocol versions and capability claims to the selected bytes. Unsupported runtime/platform combinations MUST fail explicitly. (bead: morphir-5hxy) + +#### Scenario: Process artifact is selected offline +- **WHEN** a compatible host selects a qualified installed process artifact +- **THEN** it verifies and launches the locked executable and arguments without obtaining Python, Rust or another artifact from the network + +#### Scenario: Existing user continues with WASM +- **WHEN** a compatible host selects the retained WASM runtime +- **THEN** the extension remains portable and its operations preserve the qualified compiler behavior + +### Requirement: Host support claims follow actual qualification +Each documented successful host route SHALL have retained candidate-bound installation, lifecycle and operation evidence for the stated host revision and runtime. Unqualified or incompatible Rust or MoonBit routes MUST be labeled unsupported with their observed limitations. A transport wrapper alone MUST NOT establish model-envelope support. (bead: morphir-5hxy) + +#### Scenario: A host requires unavailable model behavior +- **WHEN** a host route requires a profile or model operation that the selected guest cannot admit +- **THEN** documentation records that route as unsupported rather than claiming that process transport resolves the incompatibility diff --git a/openspec/specs/python/model-transport/spec.md b/openspec/specs/python/model-transport/spec.md new file mode 100644 index 00000000..6c770031 --- /dev/null +++ b/openspec/specs/python/model-transport/spec.md @@ -0,0 +1,58 @@ +# python/model-transport Specification + +## Purpose + +Expose the Python compiler through the current canonical MEP draft with explicit model transport negotiation, bounded envelopes and truthful supported-profile claims. + +## Requirements + +### Requirement: New model operations require exact draft negotiation +The extension SHALL support MEP `0.2.0-draft.1` for model operations while preserving legacy `0.1` behavior. Draft negotiation MUST match the full version exactly. A session negotiated as `0.1` MUST NOT enable model operations. Package, claims, model-envelope and IR versions MUST remain independent. (bead: morphir-5hxy) + +#### Scenario: Host offers the supported new draft +- **WHEN** a host and extension both offer `0.2.0-draft.1` +- **THEN** initialization selects the exact draft before any model operation is admitted + +#### Scenario: Legacy or different draft session +- **WHEN** the session selects `0.1` or the host offers only another draft version +- **THEN** model operations are unavailable and an incompatible-only handshake is refused + +### Requirement: Model operation admission is bilateral +Each model operation SHALL require an initialized, advertised tuple matching operation, profile, codec, envelope version and semantic IR version. Session tuples and limits MUST remain within both peers' offers and permitted static claims. Unknown required guarantees and unadmitted tuples MUST be refused. (bead: morphir-5hxy) + +#### Scenario: Compile support does not grant generation +- **WHEN** only a compile tuple was admitted and the host requests model generation +- **THEN** the operation is refused without invoking the generator + +#### Scenario: Independent lists would produce an unsupported combination +- **WHEN** an operation combines otherwise known tuple members in a combination not admitted as a complete tuple +- **THEN** the extension refuses that combination rather than forming a Cartesian product + +### Requirement: Model envelopes bind exact bounded bytes +Model payloads SHALL use the canonical `0.1.0-draft.1` envelope and validate codec, layout, canonical padded base64, decoded length, SHA-256, semantic IR version and required critical fields before compiler invocation. Advertised and local message/decoded limits MUST be enforced before unbounded allocation or decoding. (bead: morphir-5hxy) + +#### Scenario: Payload length or digest disagrees +- **WHEN** an envelope's declared length or digest differs from its decoded bytes +- **THEN** admission fails without compiling or generating + +#### Scenario: Oversized or unknown critical input +- **WHEN** input exceeds an applicable bound or names a critical field the receiver does not understand +- **THEN** the extension refuses the request and does not silently ignore that condition + +### Requirement: Initial profile support is explicit and lossless +The initial model adapter SHALL support `morphir-ir` with `morphir-json` for supported v3/v4 Library models through `compileModel` and `generateModel`. Unsupported rich Ion profiles, distribution forms or preservation guarantees MUST be refused explicitly. No automatic lossy conversion or fallback SHALL occur. (bead: morphir-5hxy) + +#### Scenario: Supported JSON model round trip +- **WHEN** a qualified JSON-profile model is compiled, generated and recompiled using an admitted tuple +- **THEN** its supported semantics and diagnostics agree with the existing compiler for the selected IR version + +#### Scenario: Rich model requires unsupported preservation +- **WHEN** a host requests an Ion profile or guarantee not advertised by this increment +- **THEN** the extension refuses it rather than converting to JSON and discarding information + +### Requirement: New protocol claims require executable conformance evidence +The extension SHALL advertise the new draft and model tuples only after candidate-bound executable guest qualification covers negotiation, lifecycle, supported operations and negative admission cases. Documentation MUST distinguish implemented profiles from unimplemented optional MEP capabilities. (bead: morphir-5hxy) + +#### Scenario: Protocol constants exist without operation support +- **WHEN** an SDK exposes model types or method names but the Python adapter has not passed qualification +- **THEN** those symbols do not establish or justify an advertised support claim