diff --git a/Cargo.lock b/Cargo.lock
index 366f001a6..b0a13cff1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -421,6 +421,62 @@ dependencies = [
"sha2 0.10.9",
]
+[[package]]
+name = "bf-core"
+version = "0.0.0"
+dependencies = [
+ "async-trait",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "bf-driver"
+version = "0.0.0"
+dependencies = [
+ "bf-vm",
+ "clap",
+ "futures",
+ "miette",
+ "openshell-core",
+ "openshell-driver-vm",
+ "rustix 1.1.4",
+ "tempfile",
+ "tokio",
+ "tonic",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "bf-inventory"
+version = "0.0.0"
+dependencies = [
+ "bf-core",
+ "openshell-vfio",
+]
+
+[[package]]
+name = "bf-vm"
+version = "0.0.0"
+dependencies = [
+ "bf-core",
+ "bf-inventory",
+ "clap",
+ "openshell-core",
+ "openshell-driver-vm",
+ "openshell-vfio",
+ "serde",
+ "serde_json",
+ "sha2 0.10.9",
+ "tempfile",
+ "tokio",
+ "tonic",
+ "tracing",
+ "url",
+]
+
[[package]]
name = "bindgen"
version = "0.72.1"
@@ -3392,6 +3448,10 @@ dependencies = [
"url",
]
+[[package]]
+name = "openshell-driver-bluefield"
+version = "0.0.0"
+
[[package]]
name = "openshell-driver-docker"
version = "0.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index 86025646a..5057b56cc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@
[workspace]
resolver = "2"
-members = ["crates/*"]
+members = ["crates/*", "crates/openshell-driver-bluefield/bf-*"]
[workspace.package]
version = "0.0.0"
diff --git a/crates/openshell-driver-bluefield/Cargo.toml b/crates/openshell-driver-bluefield/Cargo.toml
new file mode 100644
index 000000000..658fa032b
--- /dev/null
+++ b/crates/openshell-driver-bluefield/Cargo.toml
@@ -0,0 +1,19 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+[package]
+name = "openshell-driver-bluefield"
+description = "BlueField compute driver package marker"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+publish = false
+
+[lib]
+name = "openshell_driver_bluefield"
+path = "src/lib.rs"
+
+[lints]
+workspace = true
diff --git a/crates/openshell-driver-bluefield/README.md b/crates/openshell-driver-bluefield/README.md
new file mode 100644
index 000000000..553e7e60e
--- /dev/null
+++ b/crates/openshell-driver-bluefield/README.md
@@ -0,0 +1,95 @@
+# openshell-driver-bluefield
+
+> Status: Experimental. The current BlueField compute driver variant is
+> `bf-vm`, which extends the VM compute driver with BlueField VF passthrough
+> and VF-backed guest egress.
+
+Host-side BlueField compute driver for OpenShell. This crate is a package
+marker — a workspace anchor for the private `bf-*` implementation crates that
+intentionally re-exports nothing. The driver runs sandbox workloads on a worker
+host while offloading egress to a BlueField DPU: each sandbox claims one host
+VF, which is bound to `vfio-pci` and passed through to the runtime as the
+sandbox's egress NIC. The agent workload still runs behind the normal OpenShell
+veth-to-policy-proxy path and never sees the VF directly.
+
+## How it fits together
+
+```mermaid
+flowchart LR
+ subgraph host["Worker host"]
+ gateway["openshell-gateway"]
+ driver["openshell-driver-bluefield
(bf-driver binary)
├── bf-vm (lifecycle extension)
├── bf-inventory (VF discovery)
└── bf-core (contracts)"]
+ vf["Host VF
vfio-pci"]
+ gateway <-->|"gRPC over private UDS"| driver
+ driver -->|"claim / bind / pass through"| vf
+ end
+
+ subgraph guest["Sandbox runtime (QEMU guest today)"]
+ proxy["OpenShell policy proxy"]
+ guestvf["BlueField VF NIC"]
+ agent["agent process"]
+ agent -->|"veth"| proxy --> guestvf
+ end
+
+ subgraph dpu["BlueField / DPU"]
+ rep["VF representor"]
+ policy["DPU policy / proxy path"]
+ end
+
+ driver -->|"launch + attach VF"| guest
+ gateway -.->|"sandbox callback"| proxy
+ vf -.-> guestvf
+ guestvf --> rep --> policy --> upstream["gateway / internet"]
+```
+
+The gateway runs as a host process and spawns the BlueField driver as a
+subprocess over a private Unix socket. For each sandbox the driver selects a
+free host VF, binds it to `vfio-pci`, launches the sandbox runtime with the VF
+attached, wires the VF as the guest egress NIC, and restores the VF to its
+original binding on teardown. The resulting datapath is:
+
+```text
+agent -> veth -> OpenShell policy proxy -> VF -> DPU representor -> gateway/internet
+```
+
+## Crate Layout
+
+The implementation is split into private `bf-*` crates so the build and review
+boundaries match responsibilities:
+
+| Crate | Role |
+|---|---|
+| `bf-core` | Shared contracts: VF handles (`VfRef`, `VfSlot`), DPU claims and network/storage modes, the `BluefieldLifecycleExtension` and `RuntimeAdapter` traits, sandbox state records, and error types. Holds no host I/O. |
+| `bf-inventory` | VF discovery and allocation: sysfs-backed VF and representor inventories, a static inventory for tests, and the `VfPool` allocator that hands out slots. |
+| `bf-vm` | The current driver implementation. A BlueField lifecycle extension over the VM compute driver that handles preflight, VF binding, guest egress wiring, host PF resolution, and guest kernel selection. |
+| `bf-driver` | The external driver binary (`openshell-driver-bluefield`) that the gateway spawns. Wires the chosen implementation to the gRPC driver transport. |
+
+## Implementations
+
+A BlueField implementation pairs a sandbox runtime with the VF passthrough and
+egress contract above. Each implementation documents its own requirements,
+configuration, and validation steps.
+
+| Implementation | Description | README |
+|---|---|---|
+| `bf-vm` | VM runtime adapter: BlueField VF passthrough and VF-backed guest egress on a QEMU-backed guest. The current driver variant. | [bf-vm/README.md](bf-vm/README.md) |
+
+## Driver Contract
+
+Regardless of implementation, the BlueField driver owns these
+security-relevant behaviors:
+
+| Behavior | Purpose |
+|---|---|
+| VF selection | Allocates VFs only from the configured PF, skipping reserved indexes and VFs owned by DRA, Kubernetes, or other services. |
+| `vfio-pci` binding | Rebinds the selected VF to `vfio-pci` for passthrough and restores the original binding on sandbox teardown. |
+| Egress placement | Configures the VF as the guest root-namespace egress NIC; the agent keeps using the veth-to-policy-proxy path and never receives the VF directly. |
+| Preflight gating | Refuses to start unless host prerequisites (IOMMU, `/dev/kvm`, `vfio-pci`, required tools, a BlueField-capable guest kernel) are satisfied. |
+| Lifecycle ownership | The gateway owns the driver subprocess; the driver owns VF claim, runtime launch, and cleanup so leaked VFs are returned to the host. |
+
+## Build and Deploy
+
+The driver binary is built from `bf-driver` and installed where the gateway's
+VM driver path expects it. Build commands, install layout, gateway and driver
+configuration, sandbox lifecycle, network verification, and troubleshooting
+live with the implementation in [`bf-vm/README.md`](bf-vm/README.md).
diff --git a/crates/openshell-driver-bluefield/bf-core/Cargo.toml b/crates/openshell-driver-bluefield/bf-core/Cargo.toml
new file mode 100644
index 000000000..747885e10
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/Cargo.toml
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+[package]
+name = "bf-core"
+description = "Shared contracts for the OpenShell BlueField compute driver"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+publish = false
+
+[dependencies]
+async-trait = "0.1"
+serde = { workspace = true }
+serde_json = { workspace = true }
+tokio = { workspace = true }
diff --git a/crates/openshell-driver-bluefield/bf-core/src/assignment.rs b/crates/openshell-driver-bluefield/bf-core/src/assignment.rs
new file mode 100644
index 000000000..914c3a4a6
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/assignment.rs
@@ -0,0 +1,199 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! The network-function assignment the control-plane leader hands to a compute
+//! node.
+//!
+//! The leader allocates a function, programs OVS via the DPU controller, then
+//! stamps the resulting assignment into the sandbox's `template.labels`. The
+//! compute-node role reads it back and binds exactly that function. Carrying the
+//! assignment as labels keeps it on the existing `ComputeDriver` contract with
+//! no new proto, and makes it policy-stamped (a guest cannot forge it).
+
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+
+use crate::FunctionKind;
+
+/// Label key prefix for all BlueField assignment fields.
+pub const LABEL_PREFIX: &str = "openshell.io/bluefield.";
+
+pub const LABEL_HOST_BDF: &str = "openshell.io/bluefield.host-bdf";
+pub const LABEL_LEASE_GENERATION: &str = "openshell.io/bluefield.lease-generation";
+pub const LABEL_MAC: &str = "openshell.io/bluefield.mac";
+pub const LABEL_ATTACHMENT_ID: &str = "openshell.io/bluefield.attachment-id";
+pub const LABEL_KIND: &str = "openshell.io/bluefield.kind";
+pub const LABEL_PF: &str = "openshell.io/bluefield.pf";
+pub const LABEL_INDEX: &str = "openshell.io/bluefield.index";
+
+/// A leader-decided network-function assignment for one sandbox.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct BluefieldAssignment {
+ /// Host PCI BDF of the function the compute node must bind.
+ pub host_bdf: String,
+ /// Controller lease generation. Carried for correlation/fencing; the
+ /// compute node never detaches, so it does not act on this directly.
+ pub lease_generation: u64,
+ /// Guest-visible function MAC (the leader derives this deterministically).
+ pub mac: String,
+ /// Controller attachment id, for logging/correlation.
+ pub attachment_id: String,
+ /// Kind of network function the leader allocated. Defaults to `Vf`.
+ pub kind: FunctionKind,
+ /// Optional cross-host coordinate; not required to bind.
+ pub pf: Option,
+ /// Identity index within the parent PF (`vf_index`, `sf_num`, ...).
+ pub index: Option,
+}
+
+impl BluefieldAssignment {
+ /// True when the labels carry a (claimed) BlueField assignment. Used by the
+ /// compute node to fail closed when an unassigned sandbox arrives.
+ #[must_use]
+ pub fn is_present(labels: &HashMap) -> bool {
+ labels.contains_key(LABEL_HOST_BDF)
+ }
+
+ /// Render the assignment as label key/value pairs.
+ #[must_use]
+ pub fn to_labels(&self) -> Vec<(String, String)> {
+ let mut out = vec![
+ (LABEL_HOST_BDF.to_string(), self.host_bdf.clone()),
+ (
+ LABEL_LEASE_GENERATION.to_string(),
+ self.lease_generation.to_string(),
+ ),
+ (LABEL_MAC.to_string(), self.mac.clone()),
+ (LABEL_ATTACHMENT_ID.to_string(), self.attachment_id.clone()),
+ (LABEL_KIND.to_string(), self.kind.as_str().to_string()),
+ ];
+ if let Some(pf) = &self.pf {
+ out.push((LABEL_PF.to_string(), pf.clone()));
+ }
+ if let Some(index) = self.index {
+ out.push((LABEL_INDEX.to_string(), index.to_string()));
+ }
+ out
+ }
+
+ /// Stamp the assignment into a labels map (overwriting any prior values).
+ pub fn apply(&self, labels: &mut HashMap) {
+ for (key, value) in self.to_labels() {
+ labels.insert(key, value);
+ }
+ }
+
+ /// Parse an assignment from a labels map. Returns `Err` when a required
+ /// key is missing or malformed (the compute node treats this as fail-closed).
+ pub fn from_labels(labels: &HashMap) -> Result {
+ let required = |key: &str| -> Result {
+ labels
+ .get(key)
+ .map(|v| v.trim().to_string())
+ .filter(|v| !v.is_empty())
+ .ok_or_else(|| format!("missing required BlueField assignment label {key}"))
+ };
+
+ let host_bdf = required(LABEL_HOST_BDF)?;
+ let mac = required(LABEL_MAC)?;
+ let attachment_id = required(LABEL_ATTACHMENT_ID)?;
+ let lease_generation = required(LABEL_LEASE_GENERATION)?
+ .parse::()
+ .map_err(|err| format!("invalid {LABEL_LEASE_GENERATION}: {err}"))?;
+
+ let kind = match labels.get(LABEL_KIND).map(|v| v.trim()) {
+ Some(v) if !v.is_empty() => {
+ FunctionKind::parse(v).ok_or_else(|| format!("invalid {LABEL_KIND}: {v}"))?
+ }
+ _ => FunctionKind::Vf,
+ };
+ let pf = labels
+ .get(LABEL_PF)
+ .map(|v| v.trim().to_string())
+ .filter(|v| !v.is_empty());
+ let index = match labels.get(LABEL_INDEX).map(|v| v.trim()) {
+ Some(v) if !v.is_empty() => Some(
+ v.parse::()
+ .map_err(|err| format!("invalid {LABEL_INDEX}: {err}"))?,
+ ),
+ _ => None,
+ };
+
+ Ok(Self {
+ host_bdf,
+ lease_generation,
+ mac,
+ attachment_id,
+ kind,
+ pf,
+ index,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn sample() -> BluefieldAssignment {
+ BluefieldAssignment {
+ host_bdf: "0000:03:00.2".to_string(),
+ lease_generation: 42,
+ mac: "02:00:00:00:00:01".to_string(),
+ attachment_id: "bf-sb-1".to_string(),
+ kind: FunctionKind::Vf,
+ pf: Some("0".to_string()),
+ index: Some(3),
+ }
+ }
+
+ #[test]
+ fn round_trips_through_labels() {
+ let assignment = sample();
+ let mut labels = HashMap::new();
+ assignment.apply(&mut labels);
+ assert!(BluefieldAssignment::is_present(&labels));
+ assert_eq!(
+ BluefieldAssignment::from_labels(&labels).unwrap(),
+ assignment
+ );
+ }
+
+ #[test]
+ fn round_trips_without_optional_coordinate() {
+ let assignment = BluefieldAssignment {
+ pf: None,
+ index: None,
+ ..sample()
+ };
+ let mut labels = HashMap::new();
+ assignment.apply(&mut labels);
+ assert_eq!(
+ BluefieldAssignment::from_labels(&labels).unwrap(),
+ assignment
+ );
+ }
+
+ #[test]
+ fn missing_required_label_is_rejected() {
+ let mut labels = HashMap::new();
+ sample().apply(&mut labels);
+ labels.remove(LABEL_HOST_BDF);
+ assert!(!BluefieldAssignment::is_present(&labels));
+ let err = BluefieldAssignment::from_labels(&labels).unwrap_err();
+ assert!(err.contains(LABEL_HOST_BDF));
+ }
+
+ #[test]
+ fn malformed_lease_generation_is_rejected() {
+ let mut labels = HashMap::new();
+ sample().apply(&mut labels);
+ labels.insert(
+ LABEL_LEASE_GENERATION.to_string(),
+ "not-a-number".to_string(),
+ );
+ let err = BluefieldAssignment::from_labels(&labels).unwrap_err();
+ assert!(err.contains(LABEL_LEASE_GENERATION));
+ }
+}
diff --git a/crates/openshell-driver-bluefield/bf-core/src/claim.rs b/crates/openshell-driver-bluefield/bf-core/src/claim.rs
new file mode 100644
index 000000000..1cca69d2d
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/claim.rs
@@ -0,0 +1,101 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Runtime-neutral BlueField resource claims.
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub enum NetworkMode {
+ #[default]
+ ProxyOnly,
+ DirectDevice,
+}
+
+impl NetworkMode {
+ #[must_use]
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::ProxyOnly => "proxy-only",
+ Self::DirectDevice => "direct-device",
+ }
+ }
+
+ #[must_use]
+ pub fn parse(value: &str) -> Option {
+ match value.trim().to_ascii_lowercase().as_str() {
+ "proxy" | "proxy-only" | "proxy_only" => Some(Self::ProxyOnly),
+ "direct" | "direct-device" | "direct_device" | "vf" | "sriov" => {
+ Some(Self::DirectDevice)
+ }
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub enum StorageMode {
+ #[default]
+ None,
+ Workspace,
+ VmDisk,
+}
+
+impl StorageMode {
+ #[must_use]
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::None => "none",
+ Self::Workspace => "workspace",
+ Self::VmDisk => "vm-disk",
+ }
+ }
+
+ #[must_use]
+ pub fn parse(value: &str) -> Option {
+ match value.trim().to_ascii_lowercase().as_str() {
+ "" | "none" | "disabled" => Some(Self::None),
+ "workspace" | "workspaces" => Some(Self::Workspace),
+ "vm-disk" | "vm_disk" | "vmdisk" => Some(Self::VmDisk),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct DpuClaim {
+ pub claim_id: String,
+ pub sandbox_id: String,
+ pub runtime: String,
+ pub network_mode: NetworkMode,
+ pub storage_mode: StorageMode,
+ pub attachment_id: Option,
+ pub lease_generation: u64,
+ pub node: Option,
+ pub workload_identity: Option,
+ pub policy_hash: Option,
+}
+
+impl DpuClaim {
+ #[must_use]
+ pub fn new(
+ claim_id: impl Into,
+ sandbox_id: impl Into,
+ runtime: impl Into,
+ network_mode: NetworkMode,
+ storage_mode: StorageMode,
+ ) -> Self {
+ Self {
+ claim_id: claim_id.into(),
+ sandbox_id: sandbox_id.into(),
+ runtime: runtime.into(),
+ network_mode,
+ storage_mode,
+ attachment_id: None,
+ lease_generation: 0,
+ node: None,
+ workload_identity: None,
+ policy_hash: None,
+ }
+ }
+}
diff --git a/crates/openshell-driver-bluefield/bf-core/src/env.rs b/crates/openshell-driver-bluefield/bf-core/src/env.rs
new file mode 100644
index 000000000..ecfa289d9
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/env.rs
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Environment variable names that make up the BlueField driver's external
+//! configuration contract.
+//!
+//! Centralizing the names here gives the host-side driver, the guest-init
+//! path, and any future runtime adapters (containers, Kubernetes) a single
+//! source of truth. The values are `&'static str` so they can be referenced
+//! both from `clap` `env = ...` attributes and from plain `std::env` lookups.
+
+/// Master switch that enables the BlueField driver.
+pub const BLUEFIELD: &str = "OPENSHELL_BLUEFIELD";
+
+/// Deployment role: `all-in-one`, `control-plane`, or `compute-node`.
+pub const BLUEFIELD_ROLE: &str = "OPENSHELL_BLUEFIELD_ROLE";
+
+/// gRPC endpoint of the control-plane controller (compute-node role).
+pub const BLUEFIELD_CONTROLLER_ENDPOINT: &str = "OPENSHELL_BLUEFIELD_CONTROLLER_ENDPOINT";
+
+/// Directory holding the mutual-TLS material for the controller channel.
+pub const BLUEFIELD_TLS_DIR: &str = "OPENSHELL_BLUEFIELD_TLS_DIR";
+
+/// Expected TLS server name for the controller certificate.
+pub const BLUEFIELD_TLS_DOMAIN: &str = "OPENSHELL_BLUEFIELD_TLS_DOMAIN";
+
+/// Host physical function (netdev name or PCI BDF) backing the VFs.
+pub const BLUEFIELD_HOST_PF: &str = "OPENSHELL_BLUEFIELD_HOST_PF";
+
+/// Comma-separated VF indexes reserved from the assignable pool.
+pub const BLUEFIELD_RESERVED_VF_INDEXES: &str = "OPENSHELL_BLUEFIELD_RESERVED_VF_INDEXES";
+
+/// Identifier of the PF used when computing per-function keys.
+pub const BLUEFIELD_PF_KEY: &str = "OPENSHELL_BLUEFIELD_PF_KEY";
+
+/// Source NAT IP applied on the DPU for sandbox egress.
+pub const BLUEFIELD_SNAT_IP: &str = "OPENSHELL_BLUEFIELD_SNAT_IP";
+
+/// Uplink port on the DPU that carries sandbox egress.
+pub const BLUEFIELD_UPLINK_PORT: &str = "OPENSHELL_BLUEFIELD_UPLINK_PORT";
+
+/// Path to the BlueField guest kernel image.
+pub const BLUEFIELD_KERNEL_IMAGE: &str = "OPENSHELL_BLUEFIELD_KERNEL_IMAGE";
+
+/// Expected version string for the guest kernel image.
+pub const BLUEFIELD_KERNEL_VERSION: &str = "OPENSHELL_BLUEFIELD_KERNEL_VERSION";
+
+/// Expected SHA-256 of the guest kernel image.
+pub const BLUEFIELD_KERNEL_SHA256: &str = "OPENSHELL_BLUEFIELD_KERNEL_SHA256";
+
+/// Comma-separated guest kernel modules to load.
+pub const BLUEFIELD_KERNEL_MODULES: &str = "OPENSHELL_BLUEFIELD_KERNEL_MODULES";
+
+/// Egress CIDR assigned to a single sandbox function.
+pub const BLUEFIELD_EGRESS_CIDR: &str = "OPENSHELL_BLUEFIELD_EGRESS_CIDR";
+
+/// Comma-separated pool of egress CIDRs handed out per function.
+pub const BLUEFIELD_EGRESS_CIDR_POOL: &str = "OPENSHELL_BLUEFIELD_EGRESS_CIDR_POOL";
+
+/// Default gateway for sandbox egress traffic.
+pub const BLUEFIELD_EGRESS_GATEWAY: &str = "OPENSHELL_BLUEFIELD_EGRESS_GATEWAY";
+
+/// Comma-separated DNS resolvers advertised to the sandbox.
+pub const BLUEFIELD_EGRESS_DNS: &str = "OPENSHELL_BLUEFIELD_EGRESS_DNS";
+
+/// Proxy placement: `none` or `dpu`.
+pub const BLUEFIELD_PROXY_PLACEMENT: &str = "OPENSHELL_BLUEFIELD_PROXY_PLACEMENT";
+
+/// Explicit proxy URL injected into the sandbox when proxying is enabled.
+pub const BLUEFIELD_EXPLICIT_PROXY_URL: &str = "OPENSHELL_BLUEFIELD_EXPLICIT_PROXY_URL";
+
+/// Guest data-path egress mode (e.g. `external-vf`).
+pub const VM_DATA_EGRESS: &str = "OPENSHELL_VM_DATA_EGRESS";
+
+/// Guest data-path IP assignment mode (e.g. `static`).
+pub const VM_DATA_IP_MODE: &str = "OPENSHELL_VM_DATA_IP_MODE";
+
+/// Guest data-path interface address in CIDR notation.
+pub const VM_DATA_IP: &str = "OPENSHELL_VM_DATA_IP";
+
+/// Guest data-path default gateway.
+pub const VM_DATA_GW: &str = "OPENSHELL_VM_DATA_GW";
+
+/// Guest data-path interface MAC address.
+pub const VM_DATA_MAC: &str = "OPENSHELL_VM_DATA_MAC";
diff --git a/crates/openshell-driver-bluefield/bf-core/src/error.rs b/crates/openshell-driver-bluefield/bf-core/src/error.rs
new file mode 100644
index 000000000..88bca56bd
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/error.rs
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Error surface shared by BlueField driver crates.
+
+pub type Result = std::result::Result;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum BluefieldError {
+ InvalidConfig(String),
+ Unsupported(String),
+ ResourceExhausted(String),
+ Runtime(String),
+ Network(String),
+ Storage(String),
+ State(String),
+}
+
+impl std::fmt::Display for BluefieldError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::InvalidConfig(message)
+ | Self::Unsupported(message)
+ | Self::ResourceExhausted(message)
+ | Self::Runtime(message)
+ | Self::Network(message)
+ | Self::Storage(message)
+ | Self::State(message) => f.write_str(message),
+ }
+ }
+}
+
+impl std::error::Error for BluefieldError {}
diff --git a/crates/openshell-driver-bluefield/bf-core/src/handles.rs b/crates/openshell-driver-bluefield/bf-core/src/handles.rs
new file mode 100644
index 000000000..4ef5774a7
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/handles.rs
@@ -0,0 +1,169 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Shared BlueField handles that cross the driver, host, and DPU seam.
+
+use serde::{Deserialize, Serialize};
+
+/// The kind of network function backing a sandbox.
+///
+/// BlueField can hand a sandbox different function types depending on the
+/// runtime and fabric configuration. The discovery and allocation layers are
+/// kind-agnostic; this discriminant lets a consumer (and the attach mechanism)
+/// know which kind a slot represents.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum FunctionKind {
+ /// SR-IOV virtual function (the `bf-vm` passthrough path).
+ #[default]
+ Vf,
+ /// Scalable Function (e.g. container/Kubernetes adapters via `mlnx-sf`).
+ Sf,
+}
+
+impl FunctionKind {
+ /// Stable wire/label string for this kind.
+ #[must_use]
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Vf => "vf",
+ Self::Sf => "sf",
+ }
+ }
+
+ /// Parse a [`FunctionKind`] from its wire/label string.
+ #[must_use]
+ pub fn parse(s: &str) -> Option {
+ match s.trim() {
+ "vf" => Some(Self::Vf),
+ "sf" => Some(Self::Sf),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct FunctionSlot {
+ pub id: String,
+ pub host_bdf: String,
+ pub kind: FunctionKind,
+ pub pf: Option,
+ /// Identity index within the parent PF. `vf_index` for a VF, `sf_num` for
+ /// an SF, function index for a virtio-net device.
+ pub index: Option,
+ pub representor: Option,
+ pub ovs_port: Option,
+ pub datapath_address: Option,
+ pub mac: Option,
+}
+
+impl FunctionSlot {
+ #[must_use]
+ pub fn new(id: impl Into, host_bdf: impl Into) -> Self {
+ Self {
+ id: id.into(),
+ host_bdf: host_bdf.into(),
+ kind: FunctionKind::Vf,
+ pf: None,
+ index: None,
+ representor: None,
+ ovs_port: None,
+ datapath_address: None,
+ mac: None,
+ }
+ }
+
+ #[must_use]
+ pub fn with_kind(mut self, kind: FunctionKind) -> Self {
+ self.kind = kind;
+ self
+ }
+
+ #[must_use]
+ pub fn with_pf(mut self, pf: impl Into) -> Self {
+ self.pf = Some(pf.into());
+ self
+ }
+
+ #[must_use]
+ pub fn with_index(mut self, index: u32) -> Self {
+ self.index = Some(index);
+ self
+ }
+
+ #[must_use]
+ pub fn with_representor(mut self, representor: impl Into) -> Self {
+ self.representor = Some(representor.into());
+ self
+ }
+
+ #[must_use]
+ pub fn with_ovs_port(mut self, ovs_port: impl Into) -> Self {
+ self.ovs_port = Some(ovs_port.into());
+ self
+ }
+
+ #[must_use]
+ pub fn with_datapath_address(mut self, address: impl Into) -> Self {
+ self.datapath_address = Some(address.into());
+ self
+ }
+
+ #[must_use]
+ pub fn with_mac(mut self, mac: impl Into) -> Self {
+ self.mac = Some(mac.into());
+ self
+ }
+
+ #[must_use]
+ pub fn net_function(&self) -> Option {
+ match (&self.pf, self.index) {
+ (Some(pf), Some(idx)) => Some(NetFunction::new(pf.clone(), idx).with_kind(self.kind)),
+ _ => None,
+ }
+ }
+}
+
+/// A reference to a single network function: `(kind, pf, index)`.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct NetFunction {
+ pub kind: FunctionKind,
+ pub pf: String,
+ pub index: u32,
+}
+
+impl NetFunction {
+ #[must_use]
+ pub fn new(pf: impl Into, index: u32) -> Self {
+ Self {
+ kind: FunctionKind::Vf,
+ pf: pf.into(),
+ index,
+ }
+ }
+
+ #[must_use]
+ pub fn with_kind(mut self, kind: FunctionKind) -> Self {
+ self.kind = kind;
+ self
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub enum ProxyPlacement {
+ #[default]
+ None,
+ Dpu,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AttachSpec {
+ pub sandbox_id: String,
+ pub function: NetFunction,
+ pub host_bdf: String,
+ pub representor: Option,
+ pub endpoint_ip: Option,
+ pub mac: Option,
+ pub openshell_endpoint: Option,
+ pub sandbox_token: Option,
+}
diff --git a/crates/openshell-driver-bluefield/bf-core/src/lib.rs b/crates/openshell-driver-bluefield/bf-core/src/lib.rs
new file mode 100644
index 000000000..80b094a11
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/lib.rs
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Shared contracts for the BlueField compute driver.
+
+pub mod assignment;
+pub mod claim;
+pub mod env;
+pub mod error;
+pub mod handles;
+pub mod lifecycle;
+pub mod role;
+pub mod runtime;
+pub mod state;
+
+pub use assignment::BluefieldAssignment;
+pub use claim::{DpuClaim, NetworkMode, StorageMode};
+pub use error::{BluefieldError, Result};
+pub use handles::{AttachSpec, FunctionKind, FunctionSlot, NetFunction, ProxyPlacement};
+pub use lifecycle::{
+ BluefieldLifecycleExtension, LaunchAbortReason, LifecycleActivation, LifecycleContext,
+ LifecycleRegistry, RestoreContext, RuntimePlan, SandboxIdentity,
+};
+pub use role::BluefieldRole;
+pub use runtime::{
+ RuntimeAdapter, RuntimeCapabilities, RuntimeCondition, RuntimeEvent, RuntimeEventKind,
+ RuntimeHandle, RuntimeResourceRequirements, RuntimeSandboxStatus, RuntimeWorkload,
+};
+pub use state::{SandboxRecord, SandboxRecordPhase};
diff --git a/crates/openshell-driver-bluefield/bf-core/src/lifecycle.rs b/crates/openshell-driver-bluefield/bf-core/src/lifecycle.rs
new file mode 100644
index 000000000..e0715f0bb
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/lifecycle.rs
@@ -0,0 +1,447 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! BlueField driver lifecycle extension framework.
+//!
+//! This mirrors the in-tree VM lifecycle extension hook chain, but the hooks
+//! run inside the external BlueField compute driver and apply to any runtime.
+
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use serde::{Deserialize, Serialize};
+
+use crate::{DpuClaim, NetworkMode, Result, RuntimeHandle, RuntimeWorkload, StorageMode};
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct SandboxIdentity {
+ pub sandbox_id: String,
+ pub sandbox_name: String,
+ pub namespace: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct LifecycleContext {
+ pub sandbox: SandboxIdentity,
+ pub runtime: String,
+ pub network_mode: NetworkMode,
+ pub storage_mode: StorageMode,
+ pub node: Option,
+ pub policy_hash: Option,
+ pub labels: Vec<(String, String)>,
+ pub annotations: Vec<(String, String)>,
+}
+
+impl LifecycleContext {
+ #[must_use]
+ pub fn extension_enabled(&self, key: &str) -> bool {
+ let extension_label = format!("openshell.io/extension.{key}");
+ self.labels
+ .iter()
+ .chain(self.annotations.iter())
+ .any(|(name, value)| name == &extension_label && value == "enabled")
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct RuntimePlan {
+ pub runtime: String,
+ pub workload: RuntimeWorkload,
+ pub environment: Vec<(String, String)>,
+ pub labels: Vec<(String, String)>,
+ pub annotations: Vec<(String, String)>,
+ pub dpu_claim: Option,
+}
+
+impl RuntimePlan {
+ #[must_use]
+ pub fn new(runtime: impl Into) -> Self {
+ Self {
+ runtime: runtime.into(),
+ workload: RuntimeWorkload::default(),
+ environment: Vec::new(),
+ labels: Vec::new(),
+ annotations: Vec::new(),
+ dpu_claim: None,
+ }
+ }
+
+ pub fn set_env(&mut self, key: impl Into, value: impl Into) {
+ self.environment.push((key.into(), value.into()));
+ }
+
+ pub fn set_label(&mut self, key: impl Into, value: impl Into) {
+ self.labels.push((key.into(), value.into()));
+ }
+
+ pub fn set_annotation(&mut self, key: impl Into, value: impl Into) {
+ self.annotations.push((key.into(), value.into()));
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum LaunchAbortReason {
+ RuntimeCreateFailed,
+ BeforeRuntimeCreateFailed,
+ DpuAttachFailed,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RestoreContext {
+ pub sandbox: SandboxIdentity,
+ pub runtime: String,
+ pub runtime_handle: Option,
+ pub dpu_claim: Option,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum LifecycleActivation {
+ Global,
+ OnRequest { key: &'static str },
+}
+
+#[async_trait]
+pub trait BluefieldLifecycleExtension: std::fmt::Debug + Send + Sync {
+ fn name(&self) -> &'static str;
+
+ fn activation(&self) -> LifecycleActivation {
+ LifecycleActivation::Global
+ }
+
+ /// Pure planning hook.
+ async fn configure_runtime(
+ &self,
+ _ctx: &LifecycleContext,
+ _plan: &mut RuntimePlan,
+ ) -> Result<()> {
+ Ok(())
+ }
+
+ /// Side-effect hook before the runtime creates the workload.
+ async fn before_runtime_create(
+ &self,
+ _ctx: &LifecycleContext,
+ _plan: &mut RuntimePlan,
+ ) -> Result<()> {
+ Ok(())
+ }
+
+ /// Cleanup hook when runtime creation aborts.
+ async fn after_runtime_create_failed(
+ &self,
+ _ctx: &LifecycleContext,
+ _plan: &RuntimePlan,
+ _reason: LaunchAbortReason,
+ ) -> Result<()> {
+ Ok(())
+ }
+
+ /// Cleanup hook after the runtime deletes the workload.
+ async fn after_runtime_delete(
+ &self,
+ _ctx: &LifecycleContext,
+ _plan: &RuntimePlan,
+ ) -> Result<()> {
+ Ok(())
+ }
+
+ /// Re-adopt claims before restoring an existing runtime workload.
+ async fn before_runtime_restore(
+ &self,
+ _ctx: &RestoreContext,
+ _plan: &mut RuntimePlan,
+ ) -> Result<()> {
+ Ok(())
+ }
+
+ /// Reconcile DPU state after runtime restore completes.
+ async fn after_runtime_restore(
+ &self,
+ _ctx: &RestoreContext,
+ _plan: &RuntimePlan,
+ ) -> Result<()> {
+ Ok(())
+ }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct LifecycleRegistry {
+ extensions: Vec>,
+}
+
+impl LifecycleRegistry {
+ #[must_use]
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn push(&mut self, extension: Arc) {
+ self.extensions.push(extension);
+ }
+
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.extensions.len()
+ }
+
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.extensions.is_empty()
+ }
+
+ fn active<'a>(
+ &'a self,
+ ctx: &'a LifecycleContext,
+ ) -> impl Iterator- > {
+ self.extensions.iter().filter(move |extension| {
+ matches!(extension.activation(), LifecycleActivation::Global)
+ || matches!(
+ extension.activation(),
+ LifecycleActivation::OnRequest { key } if ctx.extension_enabled(key)
+ )
+ })
+ }
+
+ pub async fn configure_runtime(
+ &self,
+ ctx: &LifecycleContext,
+ plan: &mut RuntimePlan,
+ ) -> Result<()> {
+ for extension in self.active(ctx) {
+ extension.configure_runtime(ctx, plan).await?;
+ }
+ Ok(())
+ }
+
+ pub async fn before_runtime_create(
+ &self,
+ ctx: &LifecycleContext,
+ plan: &mut RuntimePlan,
+ ) -> Result<()> {
+ for extension in self.active(ctx) {
+ extension.before_runtime_create(ctx, plan).await?;
+ }
+ Ok(())
+ }
+
+ pub async fn after_runtime_create_failed(
+ &self,
+ ctx: &LifecycleContext,
+ plan: &RuntimePlan,
+ reason: LaunchAbortReason,
+ ) -> Result<()> {
+ let active = self.active(ctx).cloned().collect::>();
+ for extension in active.iter().rev() {
+ extension
+ .after_runtime_create_failed(ctx, plan, reason)
+ .await?;
+ }
+ Ok(())
+ }
+
+ pub async fn after_runtime_delete(
+ &self,
+ ctx: &LifecycleContext,
+ plan: &RuntimePlan,
+ ) -> Result<()> {
+ let active = self.active(ctx).cloned().collect::>();
+ for extension in active.iter().rev() {
+ extension.after_runtime_delete(ctx, plan).await?;
+ }
+ Ok(())
+ }
+
+ pub async fn before_runtime_restore(
+ &self,
+ ctx: &RestoreContext,
+ plan: &mut RuntimePlan,
+ ) -> Result<()> {
+ let lifecycle_ctx = LifecycleContext {
+ sandbox: ctx.sandbox.clone(),
+ runtime: ctx.runtime.clone(),
+ network_mode: ctx
+ .dpu_claim
+ .as_ref()
+ .map(|claim| claim.network_mode.clone())
+ .unwrap_or_default(),
+ storage_mode: ctx
+ .dpu_claim
+ .as_ref()
+ .map(|claim| claim.storage_mode.clone())
+ .unwrap_or_default(),
+ node: ctx.dpu_claim.as_ref().and_then(|claim| claim.node.clone()),
+ policy_hash: ctx
+ .dpu_claim
+ .as_ref()
+ .and_then(|claim| claim.policy_hash.clone()),
+ labels: Vec::new(),
+ annotations: Vec::new(),
+ };
+ for extension in self.active(&lifecycle_ctx) {
+ extension.before_runtime_restore(ctx, plan).await?;
+ }
+ Ok(())
+ }
+
+ pub async fn after_runtime_restore(
+ &self,
+ ctx: &RestoreContext,
+ plan: &RuntimePlan,
+ ) -> Result<()> {
+ let lifecycle_ctx = LifecycleContext {
+ sandbox: ctx.sandbox.clone(),
+ runtime: ctx.runtime.clone(),
+ network_mode: ctx
+ .dpu_claim
+ .as_ref()
+ .map(|claim| claim.network_mode.clone())
+ .unwrap_or_default(),
+ storage_mode: ctx
+ .dpu_claim
+ .as_ref()
+ .map(|claim| claim.storage_mode.clone())
+ .unwrap_or_default(),
+ node: ctx.dpu_claim.as_ref().and_then(|claim| claim.node.clone()),
+ policy_hash: ctx
+ .dpu_claim
+ .as_ref()
+ .and_then(|claim| claim.policy_hash.clone()),
+ labels: Vec::new(),
+ annotations: Vec::new(),
+ };
+ for extension in self.active(&lifecycle_ctx) {
+ extension.after_runtime_restore(ctx, plan).await?;
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::{Arc, Mutex};
+
+ use super::*;
+
+ #[derive(Debug)]
+ struct RecordingExtension {
+ name: &'static str,
+ activation: LifecycleActivation,
+ events: Arc>>,
+ }
+
+ #[async_trait]
+ impl BluefieldLifecycleExtension for RecordingExtension {
+ fn name(&self) -> &'static str {
+ self.name
+ }
+
+ fn activation(&self) -> LifecycleActivation {
+ self.activation
+ }
+
+ async fn configure_runtime(
+ &self,
+ _ctx: &LifecycleContext,
+ _plan: &mut RuntimePlan,
+ ) -> Result<()> {
+ self.events
+ .lock()
+ .expect("events lock poisoned")
+ .push(format!("{}:configure", self.name));
+ Ok(())
+ }
+
+ async fn after_runtime_delete(
+ &self,
+ _ctx: &LifecycleContext,
+ _plan: &RuntimePlan,
+ ) -> Result<()> {
+ self.events
+ .lock()
+ .expect("events lock poisoned")
+ .push(format!("{}:delete", self.name));
+ Ok(())
+ }
+ }
+
+ fn ctx(labels: Vec<(String, String)>) -> LifecycleContext {
+ LifecycleContext {
+ sandbox: SandboxIdentity {
+ sandbox_id: "sb".to_string(),
+ sandbox_name: "sandbox".to_string(),
+ namespace: "default".to_string(),
+ },
+ runtime: "vm".to_string(),
+ network_mode: NetworkMode::ProxyOnly,
+ storage_mode: StorageMode::None,
+ node: None,
+ policy_hash: None,
+ labels,
+ annotations: Vec::new(),
+ }
+ }
+
+ #[tokio::test]
+ async fn registry_runs_cleanup_in_reverse_order() {
+ let events = Arc::new(Mutex::new(Vec::new()));
+ let mut registry = LifecycleRegistry::new();
+ registry.push(Arc::new(RecordingExtension {
+ name: "first",
+ activation: LifecycleActivation::Global,
+ events: events.clone(),
+ }));
+ registry.push(Arc::new(RecordingExtension {
+ name: "second",
+ activation: LifecycleActivation::Global,
+ events: events.clone(),
+ }));
+
+ let ctx = ctx(Vec::new());
+ let mut plan = RuntimePlan::new("vm");
+ registry.configure_runtime(&ctx, &mut plan).await.unwrap();
+ registry.after_runtime_delete(&ctx, &plan).await.unwrap();
+
+ assert_eq!(
+ *events.lock().expect("events lock poisoned"),
+ vec![
+ "first:configure".to_string(),
+ "second:configure".to_string(),
+ "second:delete".to_string(),
+ "first:delete".to_string()
+ ]
+ );
+ }
+
+ #[tokio::test]
+ async fn registry_filters_on_request_extensions() {
+ let events = Arc::new(Mutex::new(Vec::new()));
+ let mut registry = LifecycleRegistry::new();
+ registry.push(Arc::new(RecordingExtension {
+ name: "requested",
+ activation: LifecycleActivation::OnRequest { key: "network" },
+ events: events.clone(),
+ }));
+
+ let mut plan = RuntimePlan::new("vm");
+ registry
+ .configure_runtime(&ctx(Vec::new()), &mut plan)
+ .await
+ .unwrap();
+ assert!(events.lock().expect("events lock poisoned").is_empty());
+
+ registry
+ .configure_runtime(
+ &ctx(vec![(
+ "openshell.io/extension.network".to_string(),
+ "enabled".to_string(),
+ )]),
+ &mut plan,
+ )
+ .await
+ .unwrap();
+ assert_eq!(
+ *events.lock().expect("events lock poisoned"),
+ vec!["requested:configure".to_string()]
+ );
+ }
+}
diff --git a/crates/openshell-driver-bluefield/bf-core/src/role.rs b/crates/openshell-driver-bluefield/bf-core/src/role.rs
new file mode 100644
index 000000000..fc2a4bc38
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/role.rs
@@ -0,0 +1,110 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Deployment role for the BlueField compute driver.
+//!
+//! A single driver binary runs in one of three roles, selected at startup.
+//! The role is workload-agnostic, so it lives in `bf-core` and is reused by
+//! every leaf driver (`bf-vm`, a future `bf-container`, ...).
+
+use std::fmt;
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+
+/// Which part of the split topology this driver instance plays.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum BluefieldRole {
+ /// In-process control + compute on one node (dev / single host).
+ #[default]
+ AllInOne,
+ /// Leader: allocates VFs, programs OVS via the DPU controller, and
+ /// forwards sandbox lifecycle to a downstream compute-node driver. Never
+ /// binds a VF or launches a workload itself.
+ ControlPlane,
+ /// Follower: binds the leader-assigned VF and launches the workload.
+ /// Holds no control-plane endpoint.
+ ComputeNode,
+}
+
+impl BluefieldRole {
+ #[must_use]
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::AllInOne => "all-in-one",
+ Self::ControlPlane => "control-plane",
+ Self::ComputeNode => "compute-node",
+ }
+ }
+
+ /// True when this role allocates VFs and drives the DPU controller.
+ #[must_use]
+ pub fn is_control_plane(self) -> bool {
+ matches!(self, Self::AllInOne | Self::ControlPlane)
+ }
+
+ /// True when this role binds a VF and launches the workload locally.
+ #[must_use]
+ pub fn runs_workload(self) -> bool {
+ matches!(self, Self::AllInOne | Self::ComputeNode)
+ }
+}
+
+impl fmt::Display for BluefieldRole {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+impl FromStr for BluefieldRole {
+ type Err = String;
+
+ fn from_str(value: &str) -> Result {
+ match value {
+ "all-in-one" => Ok(Self::AllInOne),
+ "control-plane" => Ok(Self::ControlPlane),
+ "compute-node" => Ok(Self::ComputeNode),
+ other => Err(format!(
+ "invalid BlueField role {other:?}; expected 'all-in-one', 'control-plane', or 'compute-node'"
+ )),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn round_trips_through_str() {
+ for role in [
+ BluefieldRole::AllInOne,
+ BluefieldRole::ControlPlane,
+ BluefieldRole::ComputeNode,
+ ] {
+ assert_eq!(role.as_str().parse::().unwrap(), role);
+ }
+ }
+
+ #[test]
+ fn default_is_all_in_one() {
+ assert_eq!(BluefieldRole::default(), BluefieldRole::AllInOne);
+ }
+
+ #[test]
+ fn capability_predicates() {
+ assert!(BluefieldRole::ControlPlane.is_control_plane());
+ assert!(!BluefieldRole::ControlPlane.runs_workload());
+ assert!(BluefieldRole::ComputeNode.runs_workload());
+ assert!(!BluefieldRole::ComputeNode.is_control_plane());
+ assert!(BluefieldRole::AllInOne.is_control_plane());
+ assert!(BluefieldRole::AllInOne.runs_workload());
+ }
+
+ #[test]
+ fn rejects_unknown_role() {
+ let err = "leader".parse::().unwrap_err();
+ assert!(err.contains("invalid BlueField role"));
+ }
+}
diff --git a/crates/openshell-driver-bluefield/bf-core/src/runtime.rs b/crates/openshell-driver-bluefield/bf-core/src/runtime.rs
new file mode 100644
index 000000000..c9e2d8fdb
--- /dev/null
+++ b/crates/openshell-driver-bluefield/bf-core/src/runtime.rs
@@ -0,0 +1,176 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Runtime adapter contract.
+
+use async_trait::async_trait;
+use serde::{Deserialize, Serialize};
+
+use crate::{DpuClaim, Result, RuntimePlan};
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RuntimeCapabilities {
+ pub name: String,
+ pub supports_proxy_only: bool,
+ pub supports_direct_device: bool,
+ pub supports_storage: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RuntimeHandle {
+ pub runtime: String,
+ pub sandbox_id: String,
+ pub namespace: String,
+ pub name: String,
+ pub native_id: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct RuntimeResourceRequirements {
+ pub cpu_request: String,
+ pub cpu_limit: String,
+ pub memory_request: String,
+ pub memory_limit: String,
+}
+
+impl RuntimeResourceRequirements {
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.cpu_request.is_empty()
+ && self.cpu_limit.is_empty()
+ && self.memory_request.is_empty()
+ && self.memory_limit.is_empty()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct RuntimeWorkload {
+ pub sandbox_id: String,
+ pub sandbox_name: String,
+ pub namespace: String,
+ pub image: Option,
+ pub log_level: Option,
+ pub environment: Vec<(String, String)>,
+ pub template_environment: Vec<(String, String)>,
+ pub template_labels: Vec<(String, String)>,
+ pub agent_socket_path: Option,
+ pub gpu: bool,
+ pub resources: Option,
+ pub platform_config: serde_json::Value,
+}
+
+impl Default for RuntimeWorkload {
+ fn default() -> Self {
+ Self {
+ sandbox_id: String::new(),
+ sandbox_name: String::new(),
+ namespace: String::new(),
+ image: None,
+ log_level: None,
+ environment: Vec::new(),
+ template_environment: Vec::new(),
+ template_labels: Vec::new(),
+ agent_socket_path: None,
+ gpu: false,
+ resources: None,
+ platform_config: serde_json::Value::Null,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RuntimeCondition {
+ pub r#type: String,
+ pub status: String,
+ pub reason: String,
+ pub message: String,
+ pub last_transition_time: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RuntimeSandboxStatus {
+ pub handle: RuntimeHandle,
+ pub sandbox_name: String,
+ pub agent_fd: String,
+ pub sandbox_fd: String,
+ pub conditions: Vec,
+ pub deleting: bool,
+}
+
+impl RuntimeSandboxStatus {
+ #[must_use]
+ pub fn ready(handle: RuntimeHandle) -> Self {
+ Self {
+ sandbox_name: handle.name.clone(),
+ handle,
+ agent_fd: String::new(),
+ sandbox_fd: String::new(),
+ conditions: vec![RuntimeCondition {
+ r#type: "Ready".to_string(),
+ status: "True".to_string(),
+ reason: "RuntimeObserved".to_string(),
+ message: "Runtime workload observed".to_string(),
+ last_transition_time: String::new(),
+ }],
+ deleting: false,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum RuntimeEventKind {
+ Created,
+ Updated,
+ Deleted,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RuntimeEvent {
+ pub kind: RuntimeEventKind,
+ pub handle: RuntimeHandle,
+ pub message: String,
+}
+
+#[async_trait]
+pub trait RuntimeAdapter: std::fmt::Debug + Send + Sync {
+ fn name(&self) -> &'static str;
+ fn capabilities(&self) -> RuntimeCapabilities;
+
+ async fn validate_claim(&self, claim: &DpuClaim) -> Result<()>;
+
+ /// Validate the final runtime plan after BlueField lifecycle extensions
+ /// have had a chance to add DPU claim material.
+ async fn validate_plan(&self, plan: &RuntimePlan) -> Result<()> {
+ if let Some(claim) = &plan.dpu_claim {
+ self.validate_claim(claim).await?;
+ }
+ Ok(())
+ }
+
+ async fn create(&self, plan: RuntimePlan) -> Result;
+ async fn stop(&self, handle: &RuntimeHandle) -> Result<()>;
+ async fn delete(&self, handle: &RuntimeHandle) -> Result<()>;
+ async fn get(&self, sandbox_id: &str) -> Result