diff --git a/Cargo.lock b/Cargo.lock
index c454bdd1652..b3e83726928 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -739,7 +739,7 @@ dependencies = [
  "bitflags 2.6.0",
  "cexpr",
  "clang-sys",
- "itertools 0.13.0",
+ "itertools 0.12.1",
  "log",
  "prettyplease",
  "proc-macro2",
@@ -3467,6 +3467,7 @@ version = "0.1.0"
 dependencies = [
  "base64 0.22.1",
  "chrono",
+ "daft",
  "gateway-messages",
  "omicron-workspace-hack",
  "progenitor 0.9.1",
@@ -7527,6 +7528,7 @@ dependencies = [
  "crossterm",
  "crypto-common",
  "curve25519-dalek",
+ "daft",
  "digest",
  "dof",
  "dropshot 0.12.0",
@@ -12567,7 +12569,7 @@ dependencies = [
 [[package]]
 name = "tufaceous"
 version = "0.1.0"
-source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#d2387032714f66e31b7e255d89f9bf6eb9b3a010"
+source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#bbadc279786d8da5376f61067b42738e2ad6342e"
 dependencies = [
  "anyhow",
  "camino",
@@ -12588,8 +12590,9 @@ dependencies = [
 [[package]]
 name = "tufaceous-artifact"
 version = "0.1.0"
-source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#d2387032714f66e31b7e255d89f9bf6eb9b3a010"
+source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#bbadc279786d8da5376f61067b42738e2ad6342e"
 dependencies = [
+ "daft",
  "parse-display",
  "proptest",
  "schemars",
@@ -12603,7 +12606,7 @@ dependencies = [
 [[package]]
 name = "tufaceous-brand-metadata"
 version = "0.1.0"
-source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#d2387032714f66e31b7e255d89f9bf6eb9b3a010"
+source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#bbadc279786d8da5376f61067b42738e2ad6342e"
 dependencies = [
  "semver 1.0.25",
  "serde",
@@ -12614,7 +12617,7 @@ dependencies = [
 [[package]]
 name = "tufaceous-lib"
 version = "0.1.0"
-source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#d2387032714f66e31b7e255d89f9bf6eb9b3a010"
+source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#bbadc279786d8da5376f61067b42738e2ad6342e"
 dependencies = [
  "anyhow",
  "async-trait",
diff --git a/clients/gateway-client/Cargo.toml b/clients/gateway-client/Cargo.toml
index 96f6484122c..56ae822a249 100644
--- a/clients/gateway-client/Cargo.toml
+++ b/clients/gateway-client/Cargo.toml
@@ -10,6 +10,7 @@ workspace = true
 [dependencies]
 base64.workspace = true
 chrono.workspace = true
+daft.workspace = true
 gateway-messages.workspace = true
 progenitor.workspace = true
 rand.workspace = true
diff --git a/clients/gateway-client/src/lib.rs b/clients/gateway-client/src/lib.rs
index a78204d731b..0daa4817c8a 100644
--- a/clients/gateway-client/src/lib.rs
+++ b/clients/gateway-client/src/lib.rs
@@ -69,6 +69,7 @@ progenitor::generate_api!(
         SpIgnition = { derives = [PartialEq, Eq, PartialOrd, Ord] },
         SpIgnitionSystemType = { derives = [Copy, PartialEq, Eq, PartialOrd, Ord] },
         SpState = { derives = [PartialEq, Eq, PartialOrd, Ord] },
+        SpType = { derives = [daft::Diffable] },
     },
 );
 
diff --git a/common/src/update.rs b/common/src/update.rs
index c4d24537614..6201509cdcb 100644
--- a/common/src/update.rs
+++ b/common/src/update.rs
@@ -4,6 +4,7 @@
 
 use std::{fmt, str::FromStr};
 
+use daft::Diffable;
 use hex::FromHexError;
 use schemars::{
     JsonSchema,
@@ -64,6 +65,7 @@ impl From<Artifact> for ArtifactId {
 /// by name and version. This type indicates that.
 #[derive(
     Debug,
+    Diffable,
     Clone,
     PartialEq,
     Eq,
@@ -86,6 +88,7 @@ pub struct ArtifactHashId {
 #[derive(
     Copy,
     Clone,
+    Diffable,
     Eq,
     PartialEq,
     Ord,
@@ -95,6 +98,7 @@ pub struct ArtifactHashId {
     Deserialize,
     JsonSchema,
 )]
+#[daft(leaf)]
 #[serde(transparent)]
 #[cfg_attr(feature = "testing", derive(test_strategy::Arbitrary))]
 pub struct ArtifactHash(
diff --git a/dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout b/dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout
index a2639cd2db7..20cc5fee234 100644
--- a/dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout
+++ b/dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout
@@ -358,6 +358,7 @@ parent:    02697f74-b14a-4418-90f0-c28b2a3a6aa9
     internal DNS version:   1
     external DNS version:   1
 
+ PENDING MGS-MANAGED UPDATES: 0
 
 
 > 
@@ -453,5 +454,6 @@ parent:    02697f74-b14a-4418-90f0-c28b2a3a6aa9
     internal DNS version:   1
     external DNS version:   1
 
+ PENDING MGS-MANAGED UPDATES: 0
 
 
diff --git a/dev-tools/reconfigurator-cli/tests/output/cmd-expunge-newly-added-stdout b/dev-tools/reconfigurator-cli/tests/output/cmd-expunge-newly-added-stdout
index d7791c3baa4..3dc26155c76 100644
--- a/dev-tools/reconfigurator-cli/tests/output/cmd-expunge-newly-added-stdout
+++ b/dev-tools/reconfigurator-cli/tests/output/cmd-expunge-newly-added-stdout
@@ -302,6 +302,7 @@ parent:    06c88262-f435-410e-ba98-101bed41ec27
     internal DNS version:   1
     external DNS version:   1
 
+ PENDING MGS-MANAGED UPDATES: 0
 
 
 > blueprint-edit 3f00b694-1b16-4aaa-8f78-e6b3a527b434 expunge-zone 9995de32-dd52-4eb1-b0eb-141eb84bc739
@@ -605,6 +606,7 @@ parent:    3f00b694-1b16-4aaa-8f78-e6b3a527b434
     internal DNS version:   1
     external DNS version:   1
 
+ PENDING MGS-MANAGED UPDATES: 0
 
 
 > blueprint-plan 366b0b68-d80e-4bc1-abd3-dc69837847e0
@@ -922,6 +924,7 @@ parent:    366b0b68-d80e-4bc1-abd3-dc69837847e0
     internal DNS version:   1
     external DNS version:   1
 
+ PENDING MGS-MANAGED UPDATES: 0
 
 
 > blueprint-edit 9c998c1d-1a7b-440a-ae0c-40f781dea6e2 expunge-zone d786ef4a-5acb-4f5d-a732-a00addf986b5
diff --git a/dev-tools/reconfigurator-cli/tests/output/cmd-set-zone-images-stdout b/dev-tools/reconfigurator-cli/tests/output/cmd-set-zone-images-stdout
index bade5023dbf..5d09efd3854 100644
--- a/dev-tools/reconfigurator-cli/tests/output/cmd-set-zone-images-stdout
+++ b/dev-tools/reconfigurator-cli/tests/output/cmd-set-zone-images-stdout
@@ -94,6 +94,7 @@ parent:    1b013011-2062-4b48-b544-a32b23bce83a
     internal DNS version:   1
     external DNS version:   1
 
+ PENDING MGS-MANAGED UPDATES: 0
 
 
 > 
@@ -192,6 +193,7 @@ parent:    971eeb12-1830-4fa0-a699-98ea0164505c
     internal DNS version:   1
     external DNS version:   1
 
+ PENDING MGS-MANAGED UPDATES: 0
 
 
 > blueprint-diff 971eeb12-1830-4fa0-a699-98ea0164505c 9766ca20-38d4-4380-b005-e7c43c797e7c
@@ -455,6 +457,7 @@ parent:    9766ca20-38d4-4380-b005-e7c43c797e7c
     internal DNS version:   1
     external DNS version:   1
 
+ PENDING MGS-MANAGED UPDATES: 0
 
 
 > blueprint-diff 9766ca20-38d4-4380-b005-e7c43c797e7c f714e6ea-e85a-4d7d-93c2-a018744fe176
diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs
index 988628f867a..10297a09199 100644
--- a/nexus/db-queries/src/db/datastore/deployment.rs
+++ b/nexus/db-queries/src/db/datastore/deployment.rs
@@ -873,6 +873,8 @@ impl DataStore {
 
         Ok(Blueprint {
             id: blueprint_id,
+            // TODO these need to be serialized to the database.
+            pending_mgs_updates: BTreeMap::new(),
             sleds: sled_configs,
             parent_blueprint_id,
             internal_dns_version,
diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs
index b7b3bc764c8..e1ec30152d8 100644
--- a/nexus/db-queries/src/db/datastore/rack.rs
+++ b/nexus/db-queries/src/db/datastore/rack.rs
@@ -1054,6 +1054,7 @@ mod test {
                 blueprint: Blueprint {
                     id: BlueprintUuid::new_v4(),
                     sleds: BTreeMap::new(),
+                    pending_mgs_updates: BTreeMap::new(),
                     cockroachdb_setting_preserve_downgrade:
                         CockroachDbPreserveDowngrade::DoNotModify,
                     parent_blueprint_id: None,
@@ -1539,6 +1540,7 @@ mod test {
         let blueprint = Blueprint {
             id: BlueprintUuid::new_v4(),
             sleds: make_sled_config_only_zones(blueprint_zones),
+            pending_mgs_updates: BTreeMap::new(),
             cockroachdb_setting_preserve_downgrade:
                 CockroachDbPreserveDowngrade::DoNotModify,
             parent_blueprint_id: None,
@@ -1796,6 +1798,7 @@ mod test {
         let blueprint = Blueprint {
             id: BlueprintUuid::new_v4(),
             sleds: make_sled_config_only_zones(blueprint_zones),
+            pending_mgs_updates: BTreeMap::new(),
             cockroachdb_setting_preserve_downgrade:
                 CockroachDbPreserveDowngrade::DoNotModify,
             parent_blueprint_id: None,
@@ -2002,6 +2005,7 @@ mod test {
         let blueprint = Blueprint {
             id: BlueprintUuid::new_v4(),
             sleds: make_sled_config_only_zones(blueprint_zones),
+            pending_mgs_updates: BTreeMap::new(),
             cockroachdb_setting_preserve_downgrade:
                 CockroachDbPreserveDowngrade::DoNotModify,
             parent_blueprint_id: None,
@@ -2138,6 +2142,7 @@ mod test {
         let blueprint = Blueprint {
             id: BlueprintUuid::new_v4(),
             sleds: make_sled_config_only_zones(blueprint_zones),
+            pending_mgs_updates: BTreeMap::new(),
             cockroachdb_setting_preserve_downgrade:
                 CockroachDbPreserveDowngrade::DoNotModify,
             parent_blueprint_id: None,
diff --git a/nexus/reconfigurator/execution/src/dns.rs b/nexus/reconfigurator/execution/src/dns.rs
index 84644d0b235..63c361c3235 100644
--- a/nexus/reconfigurator/execution/src/dns.rs
+++ b/nexus/reconfigurator/execution/src/dns.rs
@@ -678,6 +678,7 @@ mod test {
         let mut blueprint = Blueprint {
             id: BlueprintUuid::new_v4(),
             sleds: blueprint_sleds,
+            pending_mgs_updates: BTreeMap::new(),
             cockroachdb_setting_preserve_downgrade:
                 CockroachDbPreserveDowngrade::DoNotModify,
             parent_blueprint_id: None,
diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs
index 2c12069d02d..422b495c7b1 100644
--- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs
+++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs
@@ -454,6 +454,7 @@ impl<'a> BlueprintBuilder<'a> {
         Blueprint {
             id: rng.next_blueprint(),
             sleds,
+            pending_mgs_updates: BTreeMap::new(),
             parent_blueprint_id: None,
             internal_dns_version: Generation::new(),
             external_dns_version: Generation::new(),
@@ -710,6 +711,10 @@ impl<'a> BlueprintBuilder<'a> {
         Blueprint {
             id: blueprint_id,
             sleds,
+            pending_mgs_updates: self
+                .parent_blueprint
+                .pending_mgs_updates
+                .clone(),
             parent_blueprint_id: Some(self.parent_blueprint.id),
             internal_dns_version: self.input.internal_dns_version(),
             external_dns_version: self.input.external_dns_version(),
diff --git a/nexus/src/app/background/tasks/blueprint_execution.rs b/nexus/src/app/background/tasks/blueprint_execution.rs
index 752a58dd1e9..26471b57250 100644
--- a/nexus/src/app/background/tasks/blueprint_execution.rs
+++ b/nexus/src/app/background/tasks/blueprint_execution.rs
@@ -247,6 +247,7 @@ mod test {
         let blueprint = Blueprint {
             id,
             sleds: blueprint_sleds,
+            pending_mgs_updates: BTreeMap::new(),
             cockroachdb_setting_preserve_downgrade:
                 CockroachDbPreserveDowngrade::DoNotModify,
             parent_blueprint_id: Some(current_target.target_id),
diff --git a/nexus/src/app/background/tasks/blueprint_load.rs b/nexus/src/app/background/tasks/blueprint_load.rs
index da2bc6dee2b..31638b00f1d 100644
--- a/nexus/src/app/background/tasks/blueprint_load.rs
+++ b/nexus/src/app/background/tasks/blueprint_load.rs
@@ -217,6 +217,7 @@ mod test {
             Blueprint {
                 id,
                 sleds: BTreeMap::new(),
+                pending_mgs_updates: BTreeMap::new(),
                 cockroachdb_setting_preserve_downgrade:
                     CockroachDbPreserveDowngrade::DoNotModify,
                 parent_blueprint_id: Some(parent_blueprint_id),
diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs
index edcf387d172..7adba713579 100644
--- a/nexus/test-utils/src/lib.rs
+++ b/nexus/test-utils/src/lib.rs
@@ -929,6 +929,7 @@ impl<'a, N: NexusServer> ControlPlaneTestContextBuilder<'a, N> {
             Blueprint {
                 id: BlueprintUuid::new_v4(),
                 sleds: blueprint_sleds,
+                pending_mgs_updates: BTreeMap::new(),
                 parent_blueprint_id: None,
                 internal_dns_version: dns_config.generation,
                 external_dns_version: Generation::new(),
diff --git a/nexus/types/src/deployment.rs b/nexus/types/src/deployment.rs
index f15e74cd688..a0440d2f4cf 100644
--- a/nexus/types/src/deployment.rs
+++ b/nexus/types/src/deployment.rs
@@ -101,7 +101,11 @@ use blueprint_display::{
 };
 use id_map::{IdMap, IdMappable};
 
+use crate::inventory::BaseboardId;
 pub use blueprint_diff::BlueprintDiffSummary;
+use blueprint_display::BpPendingMgsUpdates;
+use gateway_client::types::SpType;
+use omicron_common::update::ArtifactHashId;
 
 /// Describes a complete set of software and configuration for the system
 // Blueprints are a fundamental part of how the system modifies itself.  Each
@@ -151,6 +155,9 @@ pub struct Blueprint {
     /// A map of sled id -> desired configuration of the sled.
     pub sleds: BTreeMap<SledUuid, BlueprintSledConfig>,
 
+    /// List of pending MGS-mediated updates
+    pub pending_mgs_updates: BTreeMap<BaseboardId, PendingMgsUpdate>,
+
     /// which blueprint this blueprint is based on
     pub parent_blueprint_id: Option<BlueprintUuid>,
 
@@ -488,6 +495,7 @@ impl fmt::Display for BlueprintDisplay<'_> {
         let Blueprint {
             id,
             sleds,
+            pending_mgs_updates,
             parent_blueprint_id,
             // These two cockroachdb_* fields are handled by
             // `make_cockroachdb_table()`, called below.
@@ -569,6 +577,38 @@ impl fmt::Display for BlueprintDisplay<'_> {
         writeln!(f, "{}", self.make_cockroachdb_table())?;
         writeln!(f, "{}", self.make_metadata_table())?;
 
+        writeln!(
+            f,
+            " PENDING MGS-MANAGED UPDATES: {}",
+            pending_mgs_updates.len()
+        )?;
+        if !pending_mgs_updates.is_empty() {
+            writeln!(
+                f,
+                "{}",
+                BpTable::new(
+                    BpPendingMgsUpdates {},
+                    None,
+                    pending_mgs_updates
+                        .values()
+                        .map(|pu| {
+                            BpTableRow::from_strings(
+                                BpDiffState::Unchanged,
+                                vec![
+                                    pu.sp_type.to_string(),
+                                    pu.slot_id.to_string(),
+                                    pu.baseboard_id.part_number.clone(),
+                                    pu.baseboard_id.serial_number.clone(),
+                                    pu.artifact_hash_id.kind.to_string(),
+                                    pu.artifact_hash_id.hash.to_string(),
+                                ],
+                            )
+                        })
+                        .collect()
+                )
+            )?;
+        }
+
         Ok(())
     }
 }
@@ -963,6 +1003,21 @@ impl fmt::Display for BlueprintZoneImageSource {
     }
 }
 
+#[derive(
+    Clone, Debug, Eq, PartialEq, JsonSchema, Deserialize, Serialize, Diffable,
+)]
+pub struct PendingMgsUpdate {
+    /// id of the baseboard that we're going to update
+    baseboard_id: BaseboardId,
+    /// what type of baseboard this is
+    sp_type: SpType,
+    /// last known MGS slot (cubby number) of the baseboard
+    slot_id: u16,
+    /// which artifact to apply to this device
+    /// (implies which component is being updated)
+    artifact_hash_id: ArtifactHashId,
+}
+
 /// The desired state of an Omicron-managed physical disk in a blueprint.
 #[derive(
     Debug,
diff --git a/nexus/types/src/deployment/blueprint_diff.rs b/nexus/types/src/deployment/blueprint_diff.rs
index a0bda212873..3caea50fe53 100644
--- a/nexus/types/src/deployment/blueprint_diff.rs
+++ b/nexus/types/src/deployment/blueprint_diff.rs
@@ -58,6 +58,9 @@ impl<'a> BlueprintDiffSummary<'a> {
         let BlueprintDiff {
             // Fields in which changes are meaningful.
             sleds,
+            // TODO Will need to diff these when we can actually create
+            // blueprints with pending MGS updates.
+            pending_mgs_updates: _,
             clickhouse_cluster_config,
             // Metadata fields for which changes don't reflect semantic
             // changes from one blueprint to the next.
diff --git a/nexus/types/src/deployment/blueprint_display.rs b/nexus/types/src/deployment/blueprint_display.rs
index ee11559cb35..bfdcbef98bc 100644
--- a/nexus/types/src/deployment/blueprint_display.rs
+++ b/nexus/types/src/deployment/blueprint_display.rs
@@ -100,23 +100,23 @@ impl fmt::Display for BpGeneration {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             BpGeneration::Value(generation) => {
-                write!(f, "at generation {generation}")
+                write!(f, " at generation {generation}")
             }
             BpGeneration::Diff { before: None, after: Some(after) } => {
-                write!(f, "at generation {after}")
+                write!(f, " at generation {after}")
             }
             BpGeneration::Diff { before: Some(before), after: None } => {
-                write!(f, "from generation {before}")
+                write!(f, " from generation {before}")
             }
             BpGeneration::Diff { before: Some(before), after: Some(after) } => {
                 if before == after {
-                    write!(f, "at generation {after}")
+                    write!(f, " at generation {after}")
                 } else {
-                    write!(f, "generation {before} -> {after}")
+                    write!(f, " generation {before} -> {after}")
                 }
             }
             BpGeneration::Diff { before: None, after: None } => {
-                write!(f, "unknown generation")
+                write!(f, " unknown generation")
             }
         }
     }
@@ -400,6 +400,25 @@ impl BpTableSchema for BpClickhouseServersTableSchema {
     }
 }
 
+/// The [`BpTable`] schema for pending MGS updates
+pub struct BpPendingMgsUpdates {}
+impl BpTableSchema for BpPendingMgsUpdates {
+    fn table_name(&self) -> &'static str {
+        "Pending MGS-managed updates"
+    }
+
+    fn column_names(&self) -> &'static [&'static str] {
+        &[
+            "sp_type",
+            "slot",
+            "part_number",
+            "serial_number",
+            "artifact_kind",
+            "artifact_hash",
+        ]
+    }
+}
+
 // An entry in a [`KvListWithHeading`]
 #[derive(Debug)]
 pub struct KvPair {
diff --git a/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs
index 95c4f80e05f..c2aeba993fa 100644
--- a/nexus/types/src/inventory.rs
+++ b/nexus/types/src/inventory.rs
@@ -14,6 +14,7 @@ use crate::external_api::params::UninitializedSledId;
 use chrono::DateTime;
 use chrono::Utc;
 use clickhouse_admin_types::ClickhouseKeeperClusterMembership;
+use daft::Diffable;
 pub use gateway_client::types::PowerState;
 pub use gateway_client::types::RotImageError;
 pub use gateway_client::types::RotSlot;
@@ -34,6 +35,7 @@ use omicron_uuid_kinds::CollectionUuid;
 use omicron_uuid_kinds::DatasetUuid;
 use omicron_uuid_kinds::SledUuid;
 use omicron_uuid_kinds::ZpoolUuid;
+use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_with::serde_as;
 use std::collections::BTreeMap;
@@ -205,7 +207,16 @@ impl Collection {
 /// the same part number and serial number but a new revision number, we'd want
 /// to treat that as the same baseboard as one with a different revision number.
 #[derive(
-    Clone, Debug, Ord, Eq, PartialOrd, PartialEq, Deserialize, Serialize,
+    Clone,
+    Debug,
+    Diffable,
+    Ord,
+    Eq,
+    PartialOrd,
+    PartialEq,
+    Deserialize,
+    Serialize,
+    JsonSchema,
 )]
 pub struct BaseboardId {
     /// Oxide Part Number
diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json
index 23b5598a13a..8955f2f169e 100644
--- a/openapi/nexus-internal.json
+++ b/openapi/nexus-internal.json
@@ -1546,6 +1546,25 @@
           }
         ]
       },
+      "ArtifactHashId": {
+        "description": "A hash-based identifier for an artifact.\n\nSome places, e.g. the installinator, request artifacts by hash rather than by name and version. This type indicates that.",
+        "type": "object",
+        "properties": {
+          "hash": {
+            "description": "The hash of the artifact.",
+            "type": "string",
+            "format": "hex string (32 bytes)"
+          },
+          "kind": {
+            "description": "The kind of artifact this is.",
+            "type": "string"
+          }
+        },
+        "required": [
+          "hash",
+          "kind"
+        ]
+      },
       "BackgroundTask": {
         "description": "Background tasks\n\nThese are currently only intended for observability by developers.  We will eventually want to flesh this out into something more observable for end users.",
         "type": "object",
@@ -1629,6 +1648,24 @@
           "serial"
         ]
       },
+      "BaseboardId": {
+        "description": "A unique baseboard id found during a collection\n\nBaseboard ids are the keys used to link up information from disparate sources (like a service processor and a sled agent).\n\nThese are normalized in the database.  Each distinct baseboard id is assigned a uuid and shared across the many possible collections that reference it.\n\nUsually, the part number and serial number are combined with a revision number.  We do not include that here.  If we ever did find a baseboard with the same part number and serial number but a new revision number, we'd want to treat that as the same baseboard as one with a different revision number.",
+        "type": "object",
+        "properties": {
+          "part_number": {
+            "description": "Oxide Part Number",
+            "type": "string"
+          },
+          "serial_number": {
+            "description": "Serial number (unique for a given part number)",
+            "type": "string"
+          }
+        },
+        "required": [
+          "part_number",
+          "serial_number"
+        ]
+      },
       "BfdMode": {
         "description": "BFD connection mode.",
         "type": "string",
@@ -1917,6 +1954,13 @@
               }
             ]
           },
+          "pending_mgs_updates": {
+            "description": "List of pending MGS-mediated updates",
+            "type": "object",
+            "additionalProperties": {
+              "$ref": "#/components/schemas/PendingMgsUpdate"
+            }
+          },
           "sleds": {
             "description": "A map of sled id -> desired configuration of the sled.",
             "type": "object",
@@ -1938,6 +1982,7 @@
           "external_dns_version",
           "id",
           "internal_dns_version",
+          "pending_mgs_updates",
           "sleds",
           "time_created"
         ]
@@ -4596,6 +4641,47 @@
           "collector_id"
         ]
       },
+      "PendingMgsUpdate": {
+        "type": "object",
+        "properties": {
+          "artifact_hash_id": {
+            "description": "which artifact to apply to this device (implies which component is being updated)",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/ArtifactHashId"
+              }
+            ]
+          },
+          "baseboard_id": {
+            "description": "id of the baseboard that we're going to update",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/BaseboardId"
+              }
+            ]
+          },
+          "slot_id": {
+            "description": "last known MGS slot (cubby number) of the baseboard",
+            "type": "integer",
+            "format": "uint16",
+            "minimum": 0
+          },
+          "sp_type": {
+            "description": "what type of baseboard this is",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/SpType"
+              }
+            ]
+          }
+        },
+        "required": [
+          "artifact_hash_id",
+          "baseboard_id",
+          "slot_id",
+          "sp_type"
+        ]
+      },
       "PhysicalDiskKind": {
         "description": "Describes the form factor of physical disks.",
         "type": "string",
@@ -5717,6 +5803,15 @@
           "last_port"
         ]
       },
+      "SpType": {
+        "description": "SpType\n\n<details><summary>JSON schema</summary>\n\n```json { \"type\": \"string\", \"enum\": [ \"sled\", \"power\", \"switch\" ] } ``` </details>",
+        "type": "string",
+        "enum": [
+          "sled",
+          "power",
+          "switch"
+        ]
+      },
       "Srv": {
         "type": "object",
         "properties": {
diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs
index b0ec33d001f..c1de8fe0fe4 100644
--- a/sled-agent/src/rack_setup/service.rs
+++ b/sled-agent/src/rack_setup/service.rs
@@ -1540,6 +1540,7 @@ pub(crate) fn build_initial_blueprint_from_sled_configs(
     Ok(Blueprint {
         id: BlueprintUuid::new_v4(),
         sleds: blueprint_sleds,
+        pending_mgs_updates: BTreeMap::new(),
         parent_blueprint_id: None,
         internal_dns_version,
         // We don't configure external DNS during RSS, so set it to an initial
diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml
index 84feaf9a8b8..6513f6d6b63 100644
--- a/workspace-hack/Cargo.toml
+++ b/workspace-hack/Cargo.toml
@@ -41,6 +41,7 @@ crossbeam-utils = { version = "0.8.20" }
 crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
 crypto-common = { version = "0.1.6", default-features = false, features = ["getrandom", "std"] }
 curve25519-dalek = { version = "4.1.3", features = ["digest", "legacy_compatibility", "rand_core"] }
+daft = { version = "0.1.2", features = ["derive", "newtype-uuid1", "oxnet01", "uuid1"] }
 digest = { version = "0.10.7", features = ["mac", "oid", "std"] }
 dropshot = { version = "0.12.0", default-features = false, features = ["usdt-probes"] }
 ecdsa = { version = "0.16.9", features = ["pem", "signing", "std", "verifying"] }
@@ -70,6 +71,7 @@ idna = { version = "1.0.3" }
 indexmap = { version = "2.7.1", features = ["serde"] }
 inout = { version = "0.1.3", default-features = false, features = ["std"] }
 ipnetwork = { version = "0.21.1", features = ["schemars", "serde"] }
+itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" }
 itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" }
 lalrpop-util = { version = "0.19.12" }
 lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] }
@@ -164,6 +166,7 @@ crossbeam-utils = { version = "0.8.20" }
 crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
 crypto-common = { version = "0.1.6", default-features = false, features = ["getrandom", "std"] }
 curve25519-dalek = { version = "4.1.3", features = ["digest", "legacy_compatibility", "rand_core"] }
+daft = { version = "0.1.2", features = ["derive", "newtype-uuid1", "oxnet01", "uuid1"] }
 digest = { version = "0.10.7", features = ["mac", "oid", "std"] }
 dropshot = { version = "0.12.0", default-features = false, features = ["usdt-probes"] }
 ecdsa = { version = "0.16.9", features = ["pem", "signing", "std", "verifying"] }
@@ -193,6 +196,7 @@ idna = { version = "1.0.3" }
 indexmap = { version = "2.7.1", features = ["serde"] }
 inout = { version = "0.1.3", default-features = false, features = ["std"] }
 ipnetwork = { version = "0.21.1", features = ["schemars", "serde"] }
+itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" }
 itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" }
 lalrpop-util = { version = "0.19.12" }
 lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] }
@@ -337,7 +341,6 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.1", default
 hyper-rustls = { version = "0.27.3", default-features = false, features = ["http1", "http2", "ring", "tls12", "webpki-tokio"] }
 hyper-util = { version = "0.1.10", features = ["full"] }
 indicatif = { version = "0.17.11", features = ["rayon"] }
-itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" }
 mio = { version = "1.0.2", features = ["net", "os-ext"] }
 rustix = { version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] }
 tokio-rustls = { version = "0.26.0", default-features = false, features = ["logging", "ring", "tls12"] }
@@ -352,7 +355,6 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.1", default
 hyper-rustls = { version = "0.27.3", default-features = false, features = ["http1", "http2", "ring", "tls12", "webpki-tokio"] }
 hyper-util = { version = "0.1.10", features = ["full"] }
 indicatif = { version = "0.17.11", features = ["rayon"] }
-itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" }
 mio = { version = "1.0.2", features = ["net", "os-ext"] }
 rustix = { version = "0.38.37", features = ["event", "fs", "net", "pipe", "process", "stdio", "system", "termios", "time"] }
 tokio-rustls = { version = "0.26.0", default-features = false, features = ["logging", "ring", "tls12"] }