diff --git a/platform/event-log/src/applications.rs b/platform/event-log/src/applications.rs index d20fa621b45..846b218d6b2 100644 --- a/platform/event-log/src/applications.rs +++ b/platform/event-log/src/applications.rs @@ -326,6 +326,62 @@ impl EventLog { .await } + /// Exact bytes of one package in a committed revision, without reading or + /// validating the revision's other packages. `None` when the revision does + /// not contain that package. + pub async fn load_application_package_at( + &self, + source: &DeploymentRef, + package: &PackageRef, + ) -> Result, StoreError> { + source.validate().map_err(invalid)?; + if source.state_domain != self.application_domain()? { + return Err(invalid("foreign application state domain")); + } + package + .validate() + .map_err(|error| invalid(error.to_string()))?; + let deployment = source.deployment.to_string(); + let revision = source.revision as i64; + let requested = package.clone(); + let files = self + .connection + .run(move |connection| { + Box::pin(async move { + let mut tx = connection.begin().await?; + let installed: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM application_source_packages + WHERE deployment = ? AND source_revision = ? AND id = ? AND revision = ?)", + ) + .bind(&deployment) + .bind(revision) + .bind(&requested.id) + .bind(&requested.revision) + .fetch_one(&mut *tx) + .await?; + let files = if installed { + Some(read_package_files(&mut tx, &requested).await?) + } else { + None + }; + tx.commit().await?; + Ok(files) + }) + }) + .await?; + let Some(files) = files else { + return Ok(None); + }; + let package = package.clone(); + tokio::task::spawn_blocking(move || build_checked_package(files, &package).map(Some)) + .await + .map_err(|error| { + invalid(format!( + "application package validation worker failed: {error}" + )) + })? + } + async fn read_metadata( &self, deployment: Uuid, @@ -357,7 +413,8 @@ impl EventLog { return Err(invalid("invalid deployment")); } let owner = std::sync::Arc::downgrade(self.root_owner.as_ref().expect("owned source Root")); - self.connection + let raw = self + .connection .run(move |connection| { Box::pin(async move { let mut tx = connection.begin().await?; @@ -366,28 +423,43 @@ impl EventLog { else { return Ok(None); }; - let mut packages = Vec::new(); - for reference in metadata.packages { - let package = read_package(&mut tx, &reference).await?; - packages.push(package); + let mut raw_packages = Vec::with_capacity(metadata.packages.len()); + for reference in &metadata.packages { + let files = read_package_files(&mut tx, reference).await?; + raw_packages.push((reference.clone(), files)); } - let plan = validate( - &metadata.source.input, - &metadata.source.remote_exports, - &packages, - )?; tx.commit().await?; - let snapshot = - SourceSnapshot::new(owner, metadata.source.reference.clone(), &plan); - Ok(Some(LoadedDeployment { - snapshot, - source: metadata.source, - packages, - plan, - })) + Ok(Some((metadata, raw_packages))) }) }) - .await + .await?; + let Some((metadata, raw_packages)) = raw else { + return Ok(None); + }; + tokio::task::spawn_blocking(move || { + let mut packages = Vec::with_capacity(raw_packages.len()); + for (reference, files) in raw_packages { + packages.push(build_checked_package(files, &reference)?); + } + let plan = validate( + &metadata.source.input, + &metadata.source.remote_exports, + &packages, + )?; + let snapshot = SourceSnapshot::new(owner, metadata.source.reference.clone(), &plan); + Ok(Some(LoadedDeployment { + snapshot, + source: metadata.source, + packages, + plan, + })) + }) + .await + .map_err(|error| { + invalid(format!( + "application source validation worker failed: {error}" + )) + })? } } @@ -461,32 +533,38 @@ async fn read_metadata( })) } -async fn read_package( +async fn read_package_files( connection: &mut SqliteConnection, reference: &PackageRef, -) -> Result { - let rows = sqlx::query("SELECT path, - CASE WHEN typeof(payload) = 'blob' THEN length(payload) END AS bytes FROM application_package_files WHERE id = ? AND revision = ? ORDER BY path") - .bind(&reference.id).bind(&reference.revision).fetch_all(&mut *connection).await?; +) -> Result>, StoreError> { + let rows = sqlx::query( + "SELECT path, + CASE WHEN typeof(payload) = 'blob' THEN payload END AS payload + FROM application_package_files WHERE id = ? AND revision = ? ORDER BY path", + ) + .bind(&reference.id) + .bind(&reference.revision) + .fetch_all(&mut *connection) + .await?; if rows.is_empty() { return Err(invalid("invalid stored application package file count")); } - for row in &rows { - usize::try_from( - row.try_get::, _>("bytes")? - .ok_or_else(|| invalid("stored package payload must be a BLOB"))?, - ) - .map_err(|_| invalid("invalid package payload length"))?; - let path: &str = row.try_get("path")?; - maka_packages::validate_path(path).map_err(|e| invalid(e.to_string()))?; - } let mut files = BTreeMap::new(); for row in rows { let path: String = row.try_get("path")?; - let bytes: Vec = sqlx::query_scalar("SELECT payload FROM application_package_files WHERE id = ? AND revision = ? AND path = ?") - .bind(&reference.id).bind(&reference.revision).bind(&path).fetch_one(&mut *connection).await?; + maka_packages::validate_path(&path).map_err(|e| invalid(e.to_string()))?; + let bytes: Vec = row + .try_get::>, _>("payload")? + .ok_or_else(|| invalid("stored package payload must be a BLOB"))?; files.insert(path, bytes); } + Ok(files) +} + +fn build_checked_package( + files: BTreeMap>, + reference: &PackageRef, +) -> Result { let package = Package::new(files).map_err(|e| invalid(e.to_string()))?; if &package.application_ref() != reference { return Err(invalid("application package digest mismatch")); @@ -494,5 +572,13 @@ async fn read_package( Ok(package) } +async fn read_package( + connection: &mut SqliteConnection, + reference: &PackageRef, +) -> Result { + let files = read_package_files(connection, reference).await?; + build_checked_package(files, reference) +} + #[cfg(test)] mod tests; diff --git a/platform/event-log/src/applications/tests.rs b/platform/event-log/src/applications/tests.rs index f9615a5fff0..5dd2d40288b 100644 --- a/platform/event-log/src/applications/tests.rs +++ b/platform/event-log/src/applications/tests.rs @@ -142,12 +142,48 @@ async fn immutable_bytes_preserve_every_committed_source_revision() { .files(), old.files() ); + assert_eq!( + store + .load_application_package_at(&original, &old.application_ref()) + .await + .unwrap() + .unwrap() + .files(), + old.files() + ); + let current = store + .load_application(a) + .await + .unwrap() + .unwrap() + .source + .reference; + assert!( + store + .load_application_package_at(¤t, &old.application_ref()) + .await + .unwrap() + .is_none() + ); let mut absent = original.clone(); absent.revision = 3; assert!(store.load_application_at(&absent).await.unwrap().is_none()); + assert!( + store + .load_application_package_at(&absent, &old.application_ref()) + .await + .unwrap() + .is_none() + ); let mut foreign = original; foreign.state_domain = "0".repeat(64); assert!(store.load_application_at(&foreign).await.is_err()); + assert!( + store + .load_application_package_at(&foreign, &old.application_ref()) + .await + .is_err() + ); store.close().await.unwrap(); } @@ -156,7 +192,7 @@ async fn corrupted_exact_bytes_cannot_be_loaded_or_silently_overwritten() { let (_dir, store) = store().await; let id = Uuid::new_v4(); let package = package("export default {};"); - store + let source = store .commit_application(id, 0, selected(&package), vec![], vec![package.clone()]) .await .unwrap(); @@ -183,6 +219,15 @@ async fn corrupted_exact_bytes_cannot_be_loaded_or_silently_overwritten() { .to_string() .contains("digest mismatch") ); + assert!( + store + .load_application_package_at(&source, &package.application_ref()) + .await + .err() + .unwrap() + .to_string() + .contains("digest mismatch") + ); assert!( store .commit_application(id, 1, selected(&package), vec![], vec![package]) @@ -217,7 +262,8 @@ async fn stored_package_paths_types_and_immutable_digests_are_checked() { let (_dir, store) = store().await; let id = Uuid::new_v4(); let package = package("export default {};"); - store + let reference = package.application_ref(); + let source = store .commit_application(id, 0, selected(&package), vec![], vec![package]) .await .unwrap(); @@ -231,18 +277,113 @@ async fn stored_package_paths_types_and_immutable_digests_are_checked() { }) .await .unwrap(); - let error = store.load_application(id).await.err().unwrap().to_string(); - if corruption.contains("char(0)") { - assert!(error.contains("payload must be a BLOB"), "{error}"); - } else if corruption.contains("zeroblob") { - assert!(error.contains("digest mismatch"), "{error}"); - } else { - assert!(error.contains("invalid package path"), "{error}"); + for error in [ + store.load_application(id).await.err().unwrap().to_string(), + store + .load_application_package_at(&source, &reference) + .await + .err() + .unwrap() + .to_string(), + ] { + if corruption.contains("char(0)") { + assert!(error.contains("payload must be a BLOB"), "{error}"); + } else if corruption.contains("zeroblob") { + assert!(error.contains("digest mismatch"), "{error}"); + } else { + assert!(error.contains("invalid package path"), "{error}"); + } } store.close().await.unwrap(); } } +#[tokio::test] +async fn single_package_load_does_not_read_or_validate_sibling_packages() { + let (_dir, store) = store().await; + let id = Uuid::new_v4(); + let intact = package("export const intact = true;"); + let sibling = Package::new(BTreeMap::from([ + ( + maka_packages::MANIFEST_FILE.into(), + serde_json::to_vec(&serde_json::json!({ + "schemaVersion": 1, + "id": "sibling", + "application": intact.manifest().application, + })) + .unwrap(), + ), + ("host.mjs".into(), b"export const sibling = true;".to_vec()), + ])) + .unwrap(); + let sibling_ref = sibling.application_ref(); + let input: Input = serde_json::from_value(serde_json::json!({ + "instances": { + "intact": { + "key": Uuid::from_u128(10), + "package": { "source": "pinned", "package": intact.application_ref() }, + "usePackage": "host" + }, + "sibling": { + "key": Uuid::from_u128(11), + "package": { "source": "pinned", "package": sibling_ref }, + "usePackage": "host" + } + } + })) + .unwrap(); + let source = store + .commit_application(id, 0, input, vec![], vec![intact.clone(), sibling]) + .await + .unwrap(); + let corrupted_id = sibling_ref.id.clone(); + let corrupted_revision = sibling_ref.revision.clone(); + store + .connection + .run(move |conn| { + Box::pin(async move { + sqlx::query( + "UPDATE application_package_files SET payload = X'00' WHERE id = ? AND revision = ? AND path = 'host.mjs'", + ) + .bind(corrupted_id) + .bind(corrupted_revision) + .execute(conn) + .await?; + Ok(()) + }) + }) + .await + .unwrap(); + assert!( + store + .load_application_at(&source) + .await + .err() + .unwrap() + .to_string() + .contains("digest mismatch") + ); + assert!( + store + .load_application_package_at(&source, &sibling_ref) + .await + .err() + .unwrap() + .to_string() + .contains("digest mismatch") + ); + assert_eq!( + store + .load_application_package_at(&source, &intact.application_ref()) + .await + .unwrap() + .unwrap() + .files(), + intact.files() + ); + store.close().await.unwrap(); +} + #[tokio::test] async fn concurrent_cas_has_one_commit_and_reopen_keeps_original_domain() { let (dir, store) = store().await; diff --git a/platform/event-log/src/root_store/applications.rs b/platform/event-log/src/root_store/applications.rs index f35758c0c04..dbdd092d89f 100644 --- a/platform/event-log/src/root_store/applications.rs +++ b/platform/event-log/src/root_store/applications.rs @@ -29,6 +29,7 @@ use maka_plugin_core::{ composition::{Input, InstanceId}, deployment::{DeploymentRef, RemoteObservation, management::ManagedDeployment}, operation::RecoveryRef, + package::PackageRef, }; use serde_json::Value; use uuid::Uuid; @@ -72,6 +73,16 @@ impl RootStore { self.require_open()?; self.control.load_application_at(reference).await } + pub async fn load_application_package_at( + &self, + source: &DeploymentRef, + package: &PackageRef, + ) -> Result, StoreError> { + self.require_open()?; + self.control + .load_application_package_at(source, package) + .await + } pub async fn read_application_metadata( &self, deployment: Uuid, diff --git a/platform/host-runtime/src/application/management.rs b/platform/host-runtime/src/application/management.rs index 2dc76dc1bb0..d8e90cc162c 100644 --- a/platform/host-runtime/src/application/management.rs +++ b/platform/host-runtime/src/application/management.rs @@ -410,21 +410,20 @@ impl Manager { async fn export_package(&self, input: Value, context: Context) -> Result { let request: PackageBundleRead = serde_json::from_value(input).map_err(|error| error.to_string())?; - let package = { - let _read = self.writes.lock().await; - let loaded = self.source_at(Some(request.source)).await?; - loaded - .packages() - .iter() - .find(|package| package.application_ref() == request.package) - .cloned() - .ok_or("package is not installed in this source")? - }; let mut ticket = context .scope() .resources .reserve() .map_err(|error| error.to_string())?; + // The UI exports each plugin's package while it starts. Loading the whole + // source here would read and hash every installed package per export. + let package = self + .package_at( + &request.source, + &request.package, + "package is not installed in this source", + ) + .await?; ticket.start(); let bytes = tokio::task::spawn_blocking(move || { let bytes = package.to_bundle(); diff --git a/platform/host-runtime/src/application/management/authoring.rs b/platform/host-runtime/src/application/management/authoring.rs index b70488dc4e8..89bf60a7466 100644 --- a/platform/host-runtime/src/application/management/authoring.rs +++ b/platform/host-runtime/src/application/management/authoring.rs @@ -65,13 +65,13 @@ impl Manager { pub(super) async fn read_package(&self, input: Value) -> Result { let request: PackageSourceRead = serde_json::from_value(input).map_err(|e| e.to_string())?; - let _read = self.writes.lock().await; - let loaded = self.source_at(Some(request.source)).await?; - let package = loaded - .packages() - .iter() - .find(|p| p.application_ref() == request.package) - .ok_or("package is not installed in this source")?; + let package = self + .package_at( + &request.source, + &request.package, + "package is not installed in this source", + ) + .await?; let paths = package.files().keys().cloned().collect(); let (text, next) = if let Some(path) = request.path { let text = std::str::from_utf8(package.file(&path).ok_or("package file unavailable")?) diff --git a/platform/host-runtime/src/application/management/composition.rs b/platform/host-runtime/src/application/management/composition.rs index 7f4d0ef159c..f7ca5b257bc 100644 --- a/platform/host-runtime/src/application/management/composition.rs +++ b/platform/host-runtime/src/application/management/composition.rs @@ -164,6 +164,23 @@ impl Manager { .map_err(|e| e.to_string())? .ok_or_else(|| "managed source not found".into()) } + pub(super) async fn package_at( + &self, + source: &DeploymentRef, + package: &PackageRef, + unavailable: &'static str, + ) -> Result { + if source.state_domain != self.source.state_domain + || source.deployment != self.source.deployment + { + return Err("foreign managed deployment source".into()); + } + self.store + .load_application_package_at(source, package) + .await + .map_err(|error| error.to_string())? + .ok_or_else(|| unavailable.into()) + } pub(super) async fn binding_candidates(&self, input: Value) -> Result { let request: BindingCandidatesRead = serde_json::from_value(input).map_err(|e| e.to_string())?; @@ -340,9 +357,19 @@ impl Manager { let request: PackageSettingsRead = serde_json::from_value(input).map_err(|e| e.to_string())?; settings::check_path(&request.path, true)?; - let _read = self.writes.lock().await; - let loaded = self.source_at(Some(request.source.clone())).await?; - let declaration = declaration(loaded.packages(), &request.package)?; + let package = self + .package_at( + &request.source, + &request.package, + "application package unavailable", + ) + .await?; + let declaration = package + .manifest() + .application + .as_ref() + .map(|declaration| declaration.declaration()) + .ok_or("application package unavailable")?; let root_schema = package_schema(declaration, request.use_package)?; let mut configuration = defaults(&root_schema); if !request.changes.is_empty() { diff --git a/platform/host-runtime/src/application/management/tests.rs b/platform/host-runtime/src/application/management/tests.rs index c2e5c8aea09..267ec45fdf5 100644 --- a/platform/host-runtime/src/application/management/tests.rs +++ b/platform/host-runtime/src/application/management/tests.rs @@ -260,7 +260,7 @@ fn subscribe(manager: Manager, method: &str, input: Value) -> (Subscription, Sco #[tokio::test] async fn ui_catalog_tracks_enabled_instances_at_the_original_revision() { - tokio::time::timeout(Duration::from_secs(10), async { + tokio::time::timeout(Duration::from_secs(60), async { let fixture = Fixture::new().await; let installed = fixture .manager @@ -610,17 +610,53 @@ async fn invalid_bundles_documents_and_stale_replacements_leave_source_unchanged #[tokio::test] async fn historical_binary_bundle_export_round_trips_and_cancels_with_cleanup() { - tokio::time::timeout(Duration::from_secs(10), async { + tokio::time::timeout(Duration::from_secs(60), async { let fixture = Fixture::new().await; let package = package((0..400_000).map(|index| (index % 256) as u8).collect()); + let sibling = Package::new(BTreeMap::from([ + ( + "maka.extension.json".into(), + serde_json::to_vec(&json!({ + "schemaVersion": 1, + "id": "example.sibling", + "application": { + "host": { + "implementation": { "kind": "native", "implementation": "example.sibling" }, + "configuration": { "type": "object" }, + "imports": {}, + "exports": {} + } + } + })) + .unwrap(), + ), + ("sibling.txt".into(), b"sibling".to_vec()), + ])) + .unwrap(); + let sibling_ref = sibling.application_ref(); + let historical_input: Input = serde_json::from_value(json!({ + "instances": { + "payload": { + "key": Uuid::from_u128(1), + "package": { "source": "pinned", "package": package.application_ref() }, + "usePackage": "host" + }, + "sibling": { + "key": Uuid::from_u128(2), + "package": { "source": "pinned", "package": sibling_ref }, + "usePackage": "host" + } + } + })) + .unwrap(); let historical = fixture .store .commit_application( fixture.source.deployment, 1, - input(&package), + historical_input, vec![], - vec![package.clone()], + vec![package.clone(), sibling], ) .await .unwrap(); @@ -635,6 +671,86 @@ async fn historical_binary_bundle_export_round_trips_and_cancels_with_cleanup() ) .await .unwrap(); + // Corrupt the sibling package on disk so any accidental full-source load + // during single-package queries fails immediately at the storage boundary. + let Fixture { + manager, + store, + source, + input, + _directory: directory, + } = fixture; + drop(manager); + store.shutdown().await.unwrap(); + drop(store); + { + use sqlx::Connection as _; + let db_path = directory + .path() + .join("root") + .join(maka_event_log::root::ROOT_DATABASE); + let mut conn = sqlx::SqliteConnection::connect_with( + &sqlx::sqlite::SqliteConnectOptions::new().filename(&db_path), + ) + .await + .unwrap(); + sqlx::query( + "UPDATE application_package_files SET payload = X'00' WHERE id = ? AND revision = ? AND path = 'sibling.txt'", + ) + .bind(&sibling_ref.id) + .bind(&sibling_ref.revision) + .execute(&mut conn) + .await + .unwrap(); + conn.close().await.unwrap(); + } + let owner = Arc::new( + RootOwner::open( + &directory.path().join("root"), + &RootNamespaces { + ownership: directory.path().join("owners"), + control: directory.path().join("control"), + }, + ) + .unwrap(), + ); + let store = Arc::new(RootStore::open(owner).await.unwrap()); + let manager = ManagementTarget::from_source(store.clone(), source.clone()) + .await + .unwrap() + .0; + let fixture = Fixture { + manager, + store, + source, + input, + _directory: directory, + }; + assert!(fixture.store.load_application_at(&historical).await.is_err()); + let read_chunk = fixture + .manager + .read_package(json!({ + "source": historical, + "package": package.application_ref(), + "path": "maka.extension.json", + "offset": 0 + })) + .await + .unwrap(); + assert!(read_chunk["text"].as_str().unwrap().contains("example.payload")); + let settings_page = fixture + .manager + .package_settings(json!({ + "source": historical, + "package": package.application_ref(), + "usePackage": "host", + "changes": [], + "path": [], + "after": null + })) + .await + .unwrap(); + assert_eq!(settings_page["package"], serde_json::to_value(package.application_ref()).unwrap()); let (mut stream, scope) = export( fixture.manager.clone(), historical.clone(), diff --git a/platform/plugin-core/src/deployment/management.rs b/platform/plugin-core/src/deployment/management.rs index 2db18bee084..42f5241029b 100644 --- a/platform/plugin-core/src/deployment/management.rs +++ b/platform/plugin-core/src/deployment/management.rs @@ -263,126 +263,131 @@ pub enum ManagementRejection { } pub fn deployment_manager_contract() -> Contract { - fn method(mode: MethodMode) -> Method { - Method { - progress: None, - mode, - input: serde_json::to_value(schemars::schema_for!(I)) - .expect("management input schema") - .into(), - output: serde_json::to_value(schemars::schema_for!(O)) - .expect("management output schema") - .into(), - resources: BTreeMap::new(), + static CONTRACT: std::sync::LazyLock = std::sync::LazyLock::new(|| { + fn method(mode: MethodMode) -> Method { + Method { + progress: None, + mode, + input: serde_json::to_value(schemars::schema_for!(I)) + .expect("management input schema") + .into(), + output: serde_json::to_value(schemars::schema_for!(O)) + .expect("management output schema") + .into(), + resources: BTreeMap::new(), + } } - } - let mut install = - method::>(MethodMode::Mutation); - install - .resources - .insert("package".into(), binary_resource_type().required()); - let mut replace = - method::>(MethodMode::Mutation); - replace.resources.insert( - "document".into(), - crate::contract::text_resource_type().required(), - ); - // Optional JSON array of existing package bundle objects. The document's - // package references select the exact resulting set in the same CAS commit. - replace - .resources - .insert("packages".into(), binary_resource_type().optional()); - Contract { - name: "maka.application.DeploymentManager".into(), - version: 1, - transport: Transport::Remote, - interface: Interface::Service { - methods: BTreeMap::from([ - ( - "read".into(), - method::(MethodMode::Query), - ), - ( - "readUi".into(), - method::(MethodMode::Query), - ), - ( - "watch".into(), - method::(MethodMode::Subscription), - ), - ("install".into(), install), - ( - "installUrl".into(), - method::>( - MethodMode::Mutation, + let mut install = + method::>(MethodMode::Mutation); + install + .resources + .insert("package".into(), binary_resource_type().required()); + let mut replace = + method::>(MethodMode::Mutation); + replace.resources.insert( + "document".into(), + crate::contract::text_resource_type().required(), + ); + // Optional JSON array of existing package bundle objects. The document's + // package references select the exact resulting set in the same CAS commit. + replace + .resources + .insert("packages".into(), binary_resource_type().optional()); + Contract { + name: "maka.application.DeploymentManager".into(), + version: 1, + transport: Transport::Remote, + interface: Interface::Service { + methods: BTreeMap::from([ + ( + "read".into(), + method::(MethodMode::Query), ), - ), - ( - "installSource".into(), - method::>( - MethodMode::Mutation, + ( + "readUi".into(), + method::(MethodMode::Query), ), - ), - ( - "readPackage".into(), - method::(MethodMode::Query), - ), - ( - "exportPackage".into(), - method::(MethodMode::Subscription), - ), - ( - "replaceSource".into(), - method::>( - MethodMode::Mutation, + ( + "watch".into(), + method::(MethodMode::Subscription), ), - ), - ( - "bindingCandidates".into(), - method::(MethodMode::Query), - ), - ( - "packageSettings".into(), - method::(MethodMode::Query), - ), - ( - "editComposition".into(), - method::>( - MethodMode::Mutation, + ("install".into(), install), + ( + "installUrl".into(), + method::>( + MethodMode::Mutation, + ), ), - ), - ( - "readSettings".into(), - method::(MethodMode::Query), - ), - ( - "configure".into(), - method::>( - MethodMode::Mutation, + ( + "installSource".into(), + method::>( + MethodMode::Mutation, + ), ), - ), - ( - "configureBatch".into(), - method::>( - MethodMode::Mutation, + ( + "readPackage".into(), + method::(MethodMode::Query), ), - ), - ( - "readDocument".into(), - method::(MethodMode::Query), - ), - ( - "instances".into(), - method::(MethodMode::Query), - ), - ( - "setEnabled".into(), - method::>( - MethodMode::Mutation, + ( + "exportPackage".into(), + method::(MethodMode::Subscription), ), - ), - ("replace".into(), replace), - ]), - }, - } + ( + "replaceSource".into(), + method::>( + MethodMode::Mutation, + ), + ), + ( + "bindingCandidates".into(), + method::(MethodMode::Query), + ), + ( + "packageSettings".into(), + method::(MethodMode::Query), + ), + ( + "editComposition".into(), + method::>( + MethodMode::Mutation, + ), + ), + ( + "readSettings".into(), + method::(MethodMode::Query), + ), + ( + "configure".into(), + method::>( + MethodMode::Mutation, + ), + ), + ( + "configureBatch".into(), + method::>( + MethodMode::Mutation, + ), + ), + ( + "readDocument".into(), + method::( + MethodMode::Query, + ), + ), + ( + "instances".into(), + method::(MethodMode::Query), + ), + ( + "setEnabled".into(), + method::>( + MethodMode::Mutation, + ), + ), + ("replace".into(), replace), + ]), + }, + } + }); + CONTRACT.clone() }