From e73862bde8813a04d95abe36c2a41b8b9b576f3d Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 02:53:38 -0700 Subject: [PATCH 01/56] contracts: define controller placement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...2026-08-21-provider-placement-contracts.md | 5 + ...DR-046-components-processes-and-sandbox.md | 8 + .../ADR-046-provider-model-and-packaging.md | 12 + .../generated/provider-catalog-shape.nix | 15 + .../d2b-contracts-provider/src/v3/provider.rs | 966 +++++++++++++++++- .../src/v3/resource_schema.rs | 178 +++- .../d2b-core-controller/src/api_catalog.rs | 40 +- packages/d2b-core-controller/src/providers.rs | 32 + .../src/descriptor.rs | 3 +- .../src/bin/d2b-provider-toolkit.rs | 2 + packages/d2b-provider-toolkit/src/lib.rs | 4 +- packages/d2b-provider-toolkit/src/manifest.rs | 14 +- .../d2b-provider-toolkit/src/registration.rs | 11 + .../d2b-provider-toolkit/tests/conformance.rs | 57 +- .../tests/fake_provider.rs | 64 +- .../tests/malicious_provider.rs | 77 +- packages/d2b-provider/src/installation.rs | 157 ++- packages/d2b-provider/src/lib.rs | 3 +- .../d2b-resource-compiler/tests/phase2.rs | 65 +- packages/xtask/src/provider_packaging.rs | 48 + 20 files changed, 1716 insertions(+), 45 deletions(-) create mode 100644 changelog.d/2026-08-21-provider-placement-contracts.md diff --git a/changelog.d/2026-08-21-provider-placement-contracts.md b/changelog.d/2026-08-21-provider-placement-contracts.md new file mode 100644 index 0000000000..153b7016c4 --- /dev/null +++ b/changelog.d/2026-08-21-provider-placement-contracts.md @@ -0,0 +1,5 @@ +### Added + +- Define signed controller scopes, target capabilities, canonical ResourceType + placement anchors, per-target EffectPort requirements, and shared Host/Guest + daemon and broker artifact checks. diff --git a/docs/specs/ADR-046-components-processes-and-sandbox.md b/docs/specs/ADR-046-components-processes-and-sandbox.md index 59d328f123..c0ef051b8d 100644 --- a/docs/specs/ADR-046-components-processes-and-sandbox.md +++ b/docs/specs/ADR-046-components-processes-and-sandbox.md @@ -64,6 +64,14 @@ After bootstrap: - every process has one executionRef/domain/user placement and selected Process Provider. +Signed component descriptors carry one closed instance scope for controller +roles, a canonical Host/Guest/Zone target set, and a concrete artifact digest +plus required EffectPort classes for each advertised target. Core admits the +descriptor before creating its static Process graph; services and workers +cannot claim ResourceType ownership or controller placement. The only +in-process controller exceptions are the fixed `Provider/system-core` and +`Provider/system-minijail` bootstrap components. + ## Static Provider deployment and optional component state Volumes Core ProviderDeployment reads the signed manifest/catalog entry a Provider's diff --git a/docs/specs/ADR-046-provider-model-and-packaging.md b/docs/specs/ADR-046-provider-model-and-packaging.md index 0b5d8e9243..1aa252d07a 100644 --- a/docs/specs/ADR-046-provider-model-and-packaging.md +++ b/docs/specs/ADR-046-provider-model-and-packaging.md @@ -305,6 +305,8 @@ Descriptor fields include: - component ID/type/binary/template; - exported ResourceTypes/methods; - supported Host/Guest Provider capabilities; +- closed controller instance scope and supported target kinds; +- concrete per-target component artifact and required EffectPort classes; - allowed `system|user` domains; - cardinality; - config projection; @@ -436,6 +438,16 @@ compiles an offline sorted exact-digest catalog: - support channel; - support contact. +The signed manifest portion of the catalog additionally carries the closed +placement contract: each controller role's `instanceScope` and +`supportedTargetKinds`, each component's per-target `targetCapabilities` +(concrete component artifact digest plus required EffectPort classes), each +owned ResourceType's `placementAnchor`, and the target runtime's `d2bdDigest` +and `brokerDigest`. Host and Guest entries for one OS/architecture MUST use +identical daemon and broker digests. These fields are generated as +`placementContractFields` and `runtimeContractFields` in the catalog shape; +they are not free-form Provider selectors. + Selection is exact digest. No runtime marketplace, download, PATH scan, directory discovery, latest, or version-range solving. diff --git a/nixos-modules/generated/provider-catalog-shape.nix b/nixos-modules/generated/provider-catalog-shape.nix index e9a01aa0bb..5c09fd5587 100644 --- a/nixos-modules/generated/provider-catalog-shape.nix +++ b/nixos-modules/generated/provider-catalog-shape.nix @@ -134,4 +134,19 @@ "runtime-marketplace" "version-range-solving" ]; + + # Signed placement-contract fields carried by the Provider + # manifest and consumed by Core admission. + placementContractFields = [ + "instanceScope" + "supportedTargetKinds" + "targetCapabilities" + "placementAnchor" + ]; + + # Signed shared daemon and broker artifact fields. + runtimeContractFields = [ + "d2bdDigest" + "brokerDigest" + ]; } diff --git a/packages/d2b-contracts-provider/src/v3/provider.rs b/packages/d2b-contracts-provider/src/v3/provider.rs index 0cd37504db..81d29ee875 100644 --- a/packages/d2b-contracts-provider/src/v3/provider.rs +++ b/packages/d2b-contracts-provider/src/v3/provider.rs @@ -40,7 +40,9 @@ use d2b_contracts_resource::v3::{ ResourceTypeName, SchemaFingerprint, resource::ResourceEnvelope, - resource_schema::{CanonicalJsonObject, ExtensionSchemaId, SchemaVersion}, + resource_schema::{ + CanonicalJsonObject, ExtensionSchemaId, PlacementAnchor, SchemaVersion, + }, volume::{MAX_VIEWS, ViewRight, ViewSpec}, volume_state::{MigrationPolicy, PersistenceClass, SensitivityClass, VolumeStateSchema, VolumeStateSchemaId}, }; @@ -65,7 +67,12 @@ pub const MAX_COMPONENT_RESOURCE_TYPES: usize = 8; /// Maximum methods one component exports. pub const MAX_COMPONENT_METHODS: usize = 32; - +/// Maximum required EffectPort classes on one target capability. +pub const MAX_COMPONENT_EFFECT_CLASSES: usize = 16; +/// Maximum target capability declarations on one component. +pub const MAX_COMPONENT_TARGET_CAPABILITIES: usize = 3; +/// Maximum signed component cardinality. +pub const MAX_COMPONENT_CARDINALITY: u32 = 256; /// Maximum entries in one signed standard capability matrix. pub const MAX_CAPABILITY_MATRIX_ENTRIES: usize = 32; @@ -140,6 +147,30 @@ pub enum ProviderContractError { PlacementHostCustodyViolation, /// The schema category requires Guest-local custody. GuestLocalRequired, + /// A controller role omitted its closed instance scope. + ControllerScopeMissing, + /// A controller role declared a target set incompatible with its scope. + ControllerScopeInvalid, + /// A target capability was declared more than once. + DuplicateTargetCapability, + /// A target capability does not carry a concrete signed artifact. + ComponentArtifactMissing, + /// A controller's Host and Guest target artifacts differ. + ComponentArtifactMismatch, + /// The component execution mode is not valid for its signed role. + ComponentExecutionInvalid, + /// A placement anchor does not match the registered ResourceType schema. + PlacementAnchorMismatch, + /// A ResourceApiBinding omitted its placement anchor. + PlacementAnchorMissing, + /// The shared daemon or broker artifact differs between Host and Guest. + SharedRuntimeArtifactMismatch, + /// A target-local component has no matching shared runtime artifacts. + RuntimeArtifactMissing, + /// The manifest advertises a target without a component capability. + TargetCapabilityMissing, + /// A target profile does not support a signed required EffectPort class. + EffectClassUnsupported, } impl core::fmt::Display for ProviderContractError { @@ -189,6 +220,18 @@ impl ProviderContractError { Self::ComponentQuotaTooSmall => "component-quota-too-small", Self::PlacementHostCustodyViolation => "placement-host-custody-violation", Self::GuestLocalRequired => "guest-local-required", + Self::ControllerScopeMissing => "controller-scope-missing", + Self::ControllerScopeInvalid => "controller-scope-invalid", + Self::DuplicateTargetCapability => "duplicate-target-capability", + Self::ComponentArtifactMissing => "component-artifact-missing", + Self::ComponentArtifactMismatch => "component-artifact-mismatch", + Self::ComponentExecutionInvalid => "component-execution-invalid", + Self::PlacementAnchorMismatch => "placement-anchor-mismatch", + Self::PlacementAnchorMissing => "placement-anchor-missing", + Self::SharedRuntimeArtifactMismatch => "shared-runtime-artifact-mismatch", + Self::RuntimeArtifactMissing => "runtime-artifact-missing", + Self::TargetCapabilityMissing => "target-capability-missing", + Self::EffectClassUnsupported => "effect-class-unsupported", } } } @@ -557,6 +600,196 @@ pub enum ComponentType { Worker, } +/// The closed instance cardinality model for a signed controller role. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "kebab-case")] +pub enum ControllerInstanceScope { + /// One controller for the containing Zone. + ZoneSingleton, + /// One controller at one fixed Host or Guest target. + FixedExecutionTarget, + /// One controller instance per resolved resource target. + PerResourceTarget, +} + +/// The target kinds a signed component may advertise. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "lowercase")] +pub enum ControllerTargetKind { + /// The containing Zone, used only by Zone-singleton roles. + Zone, + /// The physical Host execution target. + Host, + /// A workload or nested Guest execution target. + Guest, +} + +/// A closed Provider-neutral EffectPort class. +/// +/// These are capability classes, not broker operation names. A target profile +/// intersects the signed set before a component receives any effect port. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "kebab-case")] +pub enum EffectPortClass { + Runtime, + Transport, + Substrate, + Process, + Volume, + Storage, + Network, + Device, + Display, + Audio, + Credential, + Observability, +} + +/// A signed component artifact and its required effect classes for one target. +#[derive(Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ComponentTargetCapability { + target_kind: ControllerTargetKind, + artifact_digest: ArtifactDigest, + required_effect_classes: BTreeSet, +} + +impl ComponentTargetCapability { + /// Construct one target capability with a concrete signed artifact. + pub fn new( + target_kind: ControllerTargetKind, + artifact_digest: ArtifactDigest, + required_effect_classes: impl IntoIterator, + ) -> Result { + let required_effect_classes: BTreeSet<_> = required_effect_classes.into_iter().collect(); + if required_effect_classes.len() > MAX_COMPONENT_EFFECT_CLASSES { + return Err(ProviderContractError::BoundExceeded); + } + Ok(Self { + target_kind, + artifact_digest, + required_effect_classes, + }) + } + + /// Return the target kind this capability serves. + pub const fn target_kind(&self) -> ControllerTargetKind { + self.target_kind + } + + /// Borrow the concrete signed component artifact digest. + pub const fn artifact_digest(&self) -> &ArtifactDigest { + &self.artifact_digest + } + + /// Borrow the required EffectPort classes. + pub const fn required_effect_classes(&self) -> &BTreeSet { + &self.required_effect_classes + } +} + +impl core::fmt::Debug for ComponentTargetCapability { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("ComponentTargetCapability") + .field("target_kind", &self.target_kind) + .field("effect_class_count", &self.required_effect_classes.len()) + .finish_non_exhaustive() + } +} + +impl<'de> Deserialize<'de> for ComponentTargetCapability { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + struct Wire { + target_kind: ControllerTargetKind, + artifact_digest: ArtifactDigest, + #[serde(default)] + required_effect_classes: BTreeSet, + } + let wire = Wire::deserialize(deserializer)?; + Self::new( + wire.target_kind, + wire.artifact_digest, + wire.required_effect_classes, + ) + .map_err(serde::de::Error::custom) + } +} + +/// Shared daemon and broker artifacts selected for one target kind. +#[derive(Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct TargetRuntimeArtifacts { + target_kind: ControllerTargetKind, + d2bd_digest: ArtifactDigest, + broker_digest: ArtifactDigest, +} + +impl TargetRuntimeArtifacts { + /// Construct one target runtime artifact declaration. + pub fn new( + target_kind: ControllerTargetKind, + d2bd_digest: ArtifactDigest, + broker_digest: ArtifactDigest, + ) -> Result { + if matches!(target_kind, ControllerTargetKind::Zone) { + return Err(ProviderContractError::ControllerScopeInvalid); + } + Ok(Self { + target_kind, + d2bd_digest, + broker_digest, + }) + } + + /// Return the target kind. + pub const fn target_kind(&self) -> ControllerTargetKind { + self.target_kind + } + + /// Borrow the shared `d2bd` artifact digest. + pub const fn d2bd_digest(&self) -> &ArtifactDigest { + &self.d2bd_digest + } + + /// Borrow the shared broker artifact digest. + pub const fn broker_digest(&self) -> &ArtifactDigest { + &self.broker_digest + } +} + +impl core::fmt::Debug for TargetRuntimeArtifacts { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("TargetRuntimeArtifacts") + .field("target_kind", &self.target_kind) + .finish_non_exhaustive() + } +} + +impl<'de> Deserialize<'de> for TargetRuntimeArtifacts { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + struct Wire { + target_kind: ControllerTargetKind, + d2bd_digest: ArtifactDigest, + broker_digest: ArtifactDigest, + } + let wire = Wire::deserialize(deserializer)?; + Self::new(wire.target_kind, wire.d2bd_digest, wire.broker_digest) + .map_err(serde::de::Error::custom) + } +} + /// The closed dependency alias set a manifest may declare. /// /// Zone config binds each alias to an exact Provider ResourceRef and service @@ -1111,6 +1344,12 @@ pub struct ComponentDescriptor { exported_methods: BTreeSet, allowed_domains: BTreeSet, cardinality: u32, + #[serde(skip_serializing_if = "Option::is_none")] + instance_scope: Option, + #[serde(skip_serializing_if = "BTreeSet::is_empty")] + supported_target_kinds: BTreeSet, + #[serde(skip_serializing_if = "Vec::is_empty")] + target_capabilities: Vec, config_digest: ArtifactDigest, dependencies: BTreeSet, declares_state_volume: bool, @@ -1149,6 +1388,7 @@ impl ComponentDescriptor { || exported_methods.len() > MAX_COMPONENT_METHODS || allowed_domains.is_empty() || cardinality == 0 + || cardinality > MAX_COMPONENT_CARDINALITY { return Err(ProviderContractError::BoundExceeded); } @@ -1188,6 +1428,9 @@ impl ComponentDescriptor { exported_methods, allowed_domains, cardinality, + instance_scope: None, + supported_target_kinds: BTreeSet::new(), + target_capabilities: Vec::new(), config_digest, dependencies: dependency_set, declares_state_volume, @@ -1259,6 +1502,142 @@ impl ComponentDescriptor { self.cardinality } + /// Set the closed controller instance scope and supported target kinds. + pub fn with_controller_placement( + mut self, + instance_scope: ControllerInstanceScope, + supported_target_kinds: impl IntoIterator, + ) -> Result { + let supported_target_kinds: BTreeSet<_> = supported_target_kinds.into_iter().collect(); + if supported_target_kinds.is_empty() { + return Err(ProviderContractError::ControllerScopeMissing); + } + self.instance_scope = Some(instance_scope); + self.supported_target_kinds = supported_target_kinds; + Ok(self) + } + + /// Set the signed target-specific component artifacts and effect classes. + pub fn with_target_capabilities( + mut self, + target_capabilities: impl IntoIterator, + ) -> Result { + let target_capabilities: Vec<_> = target_capabilities.into_iter().collect(); + if target_capabilities.len() > MAX_COMPONENT_TARGET_CAPABILITIES { + return Err(ProviderContractError::BoundExceeded); + } + let mut targets = BTreeSet::new(); + for capability in &target_capabilities { + if !targets.insert(capability.target_kind()) { + return Err(ProviderContractError::DuplicateTargetCapability); + } + } + self.target_capabilities = target_capabilities; + Ok(self) + } + + /// Return the controller instance scope, if this is a controller role. + pub const fn instance_scope(&self) -> Option { + self.instance_scope + } + + /// Borrow the controller target kinds this descriptor advertises. + pub const fn supported_target_kinds(&self) -> &BTreeSet { + &self.supported_target_kinds + } + + /// Borrow target-specific signed component capabilities. + pub fn target_capabilities(&self) -> &[ComponentTargetCapability] { + &self.target_capabilities + } + + /// Find the target capability for one target kind. + pub fn target_capability( + &self, + target_kind: ControllerTargetKind, + ) -> Option<&ComponentTargetCapability> { + self.target_capabilities + .iter() + .find(|capability| capability.target_kind() == target_kind) + } + + fn validate_role_contract( + &self, + artifact_id: &ArtifactId, + ) -> Result<(), ProviderContractError> { + let capability_targets: BTreeSet<_> = self + .target_capabilities + .iter() + .map(ComponentTargetCapability::target_kind) + .collect(); + + match self.component_type { + ComponentType::Controller => { + if capability_targets != self.supported_target_kinds { + return Err(ProviderContractError::TargetCapabilityMissing); + } + let scope = self + .instance_scope + .ok_or(ProviderContractError::ControllerScopeMissing)?; + let valid_scope = match scope { + ControllerInstanceScope::ZoneSingleton => { + self.cardinality == 1 + && self.supported_target_kinds + == BTreeSet::from([ControllerTargetKind::Zone]) + } + ControllerInstanceScope::FixedExecutionTarget => { + self.cardinality == 1 + && self.supported_target_kinds.len() == 1 + && !self + .supported_target_kinds + .contains(&ControllerTargetKind::Zone) + } + ControllerInstanceScope::PerResourceTarget => { + self.cardinality > 0 + && !self.supported_target_kinds.is_empty() + && !self + .supported_target_kinds + .contains(&ControllerTargetKind::Zone) + } + }; + if !valid_scope { + return Err(ProviderContractError::ControllerScopeInvalid); + } + let host_artifact = self + .target_capability(ControllerTargetKind::Host) + .map(ComponentTargetCapability::artifact_digest); + let guest_artifact = self + .target_capability(ControllerTargetKind::Guest) + .map(ComponentTargetCapability::artifact_digest); + if let (Some(host), Some(guest)) = (host_artifact, guest_artifact) { + if host != guest { + return Err(ProviderContractError::ComponentArtifactMismatch); + } + } + if matches!(self.execution, ComponentExecution::InProcessBootstrap) + && !matches!( + artifact_id.as_str(), + "system-core" | "system-minijail" + ) + { + return Err(ProviderContractError::ComponentExecutionInvalid); + } + } + ComponentType::Service | ComponentType::Worker => { + if self.instance_scope.is_some() || !self.supported_target_kinds.is_empty() { + return Err(ProviderContractError::ControllerScopeInvalid); + } + if self.target_capabilities.is_empty() { + return Err(ProviderContractError::TargetCapabilityMissing); + } + if matches!(self.execution, ComponentExecution::InProcessBootstrap) { + return Err(ProviderContractError::ComponentExecutionInvalid); + } + } + } + Ok(()) + } + /// The digest of this component's config projection. pub const fn config_digest(&self) -> &ArtifactDigest { &self.config_digest @@ -1310,6 +1689,12 @@ impl<'de> Deserialize<'de> for ComponentDescriptor { exported_methods: BTreeSet, allowed_domains: BTreeSet, cardinality: u32, + #[serde(default)] + instance_scope: Option, + #[serde(default)] + supported_target_kinds: BTreeSet, + #[serde(default)] + target_capabilities: Vec, config_digest: ArtifactDigest, #[serde(default)] dependencies: BTreeSet, @@ -1343,6 +1728,16 @@ impl<'de> Deserialize<'de> for ComponentDescriptor { false, ) .map(|descriptor| descriptor.with_execution(execution)) + .and_then(|descriptor| { + let descriptor = if let Some(scope) = wire.instance_scope { + descriptor.with_controller_placement(scope, wire.supported_target_kinds)? + } else if !wire.supported_target_kinds.is_empty() { + return Err(ProviderContractError::ControllerScopeInvalid); + } else { + descriptor + }; + descriptor.with_target_capabilities(wire.target_capabilities) + }) .and_then(|descriptor| descriptor.with_state_namespaces(wire.state_namespaces)) .map_err(serde::de::Error::custom) } @@ -1466,6 +1861,8 @@ redacted_debug!(ExtensionSchemaRegistration); #[serde(rename_all = "camelCase")] pub struct ResourceApiBinding { resource_type: ResourceTypeName, + #[serde(skip_serializing_if = "Option::is_none")] + placement_anchor: Option, base_spec_version: SchemaVersion, base_spec_fingerprint: SchemaFingerprint, base_status_version: SchemaVersion, @@ -1490,6 +1887,67 @@ impl ResourceApiBinding { spec_extension: Option, status_extension: Option, ) -> Result { + let placement_anchor = PlacementAnchor::canonical_for(&resource_type); + Self::new_inner( + resource_type, + placement_anchor, + base_spec_version, + base_spec_fingerprint, + base_status_version, + base_status_fingerprint, + capability_matrix, + spec_extension, + status_extension, + ) + } + + /// Construct a ResourceApiBinding with an explicit closed placement + /// anchor. + #[allow(clippy::too_many_arguments)] + pub fn new_with_placement( + resource_type: ResourceTypeName, + base_spec_version: SchemaVersion, + base_spec_fingerprint: SchemaFingerprint, + base_status_version: SchemaVersion, + base_status_fingerprint: SchemaFingerprint, + capability_matrix: StandardCapabilityMatrix, + spec_extension: Option, + status_extension: Option, + placement_anchor: PlacementAnchor, + ) -> Result { + placement_anchor + .validate_for(&resource_type) + .map_err(|_| ProviderContractError::PlacementAnchorMismatch)?; + Self::new_inner( + resource_type, + Some(placement_anchor), + base_spec_version, + base_spec_fingerprint, + base_status_version, + base_status_fingerprint, + capability_matrix, + spec_extension, + status_extension, + ) + } + + #[allow(clippy::too_many_arguments)] + fn new_inner( + resource_type: ResourceTypeName, + placement_anchor: Option, + base_spec_version: SchemaVersion, + base_spec_fingerprint: SchemaFingerprint, + base_status_version: SchemaVersion, + base_status_fingerprint: SchemaFingerprint, + capability_matrix: StandardCapabilityMatrix, + spec_extension: Option, + status_extension: Option, + ) -> Result { + if let Some(placement_anchor) = placement_anchor { + placement_anchor + .validate_for(&resource_type) + .map_err(|_| ProviderContractError::PlacementAnchorMismatch)?; + } for registration in [spec_extension.as_ref(), status_extension.as_ref()] .into_iter() .flatten() @@ -1500,6 +1958,7 @@ impl ResourceApiBinding { } Ok(Self { resource_type, + placement_anchor, base_spec_version, base_spec_fingerprint, base_status_version, @@ -1515,6 +1974,12 @@ impl ResourceApiBinding { &self.resource_type } + /// Return the registered placement anchor, if this binding was built + /// through the legacy constructor for an unknown qualified type. + pub const fn placement_anchor(&self) -> Option<&PlacementAnchor> { + self.placement_anchor.as_ref() + } + /// The base spec schema version this binding implements. pub const fn base_spec_version(&self) -> SchemaVersion { self.base_spec_version @@ -1570,6 +2035,8 @@ impl<'de> Deserialize<'de> for ResourceApiBinding { #[serde(rename_all = "camelCase", deny_unknown_fields)] struct Wire { resource_type: ResourceTypeName, + #[serde(default)] + placement_anchor: Option, base_spec_version: SchemaVersion, base_spec_fingerprint: SchemaFingerprint, base_status_version: SchemaVersion, @@ -1582,8 +2049,9 @@ impl<'de> Deserialize<'de> for ResourceApiBinding { status_extension: Option, } let wire = Wire::deserialize(deserializer)?; - Self::new( + Self::new_inner( wire.resource_type, + wire.placement_anchor, wire.base_spec_version, wire.base_spec_fingerprint, wire.base_status_version, @@ -1936,6 +2404,8 @@ pub struct ProviderManifest { components: Vec, api_bindings: Vec, projection_factories: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + runtime_artifacts: Vec, upgrade_policy: UpgradePolicy, } @@ -1987,6 +2457,9 @@ impl ProviderManifest { } let mut bound_types = BTreeSet::new(); for binding in &api_bindings { + if binding.placement_anchor().is_none() { + return Err(ProviderContractError::PlacementAnchorMissing); + } if !bound_types.insert(binding.resource_type().clone()) { return Err(ProviderContractError::DuplicateDeclaration); } @@ -1994,6 +2467,11 @@ impl ProviderManifest { return Err(ProviderContractError::MissingRequiredField); } } + if owned_types.iter().any(|resource_type| { + !bound_types.contains(resource_type) + }) { + return Err(ProviderContractError::MissingRequiredField); + } for factory in &projection_factories { if !bound_types.contains(factory.service_type()) { return Err(ProviderContractError::ProjectionFactoryInvalid); @@ -2017,10 +2495,134 @@ impl ProviderManifest { components, api_bindings, projection_factories, + runtime_artifacts: Vec::new(), upgrade_policy, }) } + /// Validate the shared Host and Guest daemon/broker artifact declarations. + pub fn validate_runtime_artifacts( + entries: impl IntoIterator, + ) -> Result<(), ProviderContractError> { + let entries: Vec<_> = entries.into_iter().collect(); + if entries.len() > 3 { + return Err(ProviderContractError::BoundExceeded); + } + let mut seen = BTreeSet::new(); + for entry in &entries { + if !seen.insert(entry.target_kind()) { + return Err(ProviderContractError::DuplicateDeclaration); + } + } + let host = entries + .iter() + .find(|entry| entry.target_kind() == ControllerTargetKind::Host); + let guest = entries + .iter() + .find(|entry| entry.target_kind() == ControllerTargetKind::Guest); + if let (Some(host), Some(guest)) = (host, guest) + && (host.d2bd_digest() != guest.d2bd_digest() + || host.broker_digest() != guest.broker_digest()) + { + return Err(ProviderContractError::SharedRuntimeArtifactMismatch); + } + Ok(()) + } + + /// Attach the signed shared daemon and broker artifacts to this manifest. + pub fn with_target_runtime_artifacts( + mut self, + entries: impl IntoIterator, + ) -> Result { + let mut entries: Vec<_> = entries.into_iter().collect(); + Self::validate_runtime_artifacts(entries.clone())?; + entries.sort_by_key(TargetRuntimeArtifacts::target_kind); + self.runtime_artifacts = entries; + Ok(self) + } + + /// Validate every placement, component artifact, effect, and shared + /// runtime declaration needed before installation. + pub fn validate_installation_contract(&self) -> Result<(), ProviderContractError> { + for component in &self.components { + component.validate_role_contract(&self.artifact_id)?; + if component.component_type() == ComponentType::Controller { + let scope = component + .instance_scope() + .ok_or(ProviderContractError::ControllerScopeMissing)?; + let expected_anchor = match scope { + ControllerInstanceScope::ZoneSingleton => PlacementAnchor::Zone, + ControllerInstanceScope::FixedExecutionTarget + | ControllerInstanceScope::PerResourceTarget => PlacementAnchor::ExecutionRef, + }; + for resource_type in component.exported_resource_types() { + let binding = self + .binding_for(resource_type) + .ok_or(ProviderContractError::MissingRequiredField)?; + let placement_anchor = binding + .placement_anchor() + .ok_or(ProviderContractError::PlacementAnchorMissing)?; + if PlacementAnchor::canonical_for(resource_type) + .is_some_and(|canonical_anchor| canonical_anchor != *placement_anchor) + { + return Err(ProviderContractError::PlacementAnchorMismatch); + } + if *placement_anchor != expected_anchor { + return Err(ProviderContractError::ControllerScopeInvalid); + } + } + } + } + let mut required_targets = BTreeSet::new(); + for component in &self.components { + for capability in component.target_capabilities() { + if matches!( + capability.target_kind(), + ControllerTargetKind::Host | ControllerTargetKind::Guest + ) { + required_targets.insert(capability.target_kind()); + } + } + } + Self::validate_runtime_artifacts(self.runtime_artifacts.clone())?; + for target in required_targets { + if !self + .runtime_artifacts + .iter() + .any(|entry| entry.target_kind() == target) + { + return Err(ProviderContractError::RuntimeArtifactMissing); + } + } + Ok(()) + } + + /// Validate the signed EffectPort requirements against one target + /// profile's supported classes. + pub fn validate_target_effects( + &self, + target_kind: ControllerTargetKind, + supported_effect_classes: &BTreeSet, + ) -> Result<(), ProviderContractError> { + let mut advertised = false; + for component in &self.components { + let Some(capability) = component.target_capability(target_kind) else { + continue; + }; + advertised = true; + if !capability + .required_effect_classes() + .is_subset(supported_effect_classes) + { + return Err(ProviderContractError::EffectClassUnsupported); + } + } + if !advertised { + return Err(ProviderContractError::TargetCapabilityMissing); + } + Ok(()) + } + /// The artifact identifier a Provider spec selects this manifest with. pub const fn artifact_id(&self) -> &ArtifactId { &self.artifact_id @@ -2056,6 +2658,11 @@ impl ProviderManifest { &self.projection_factories } + /// Borrow the shared daemon and broker artifacts by target kind. + pub fn runtime_artifacts(&self) -> &[TargetRuntimeArtifacts] { + &self.runtime_artifacts + } + /// The upgrade, drain, and restart policy. pub const fn upgrade_policy(&self) -> &UpgradePolicy { &self.upgrade_policy @@ -2091,6 +2698,7 @@ impl ProviderManifest { required_fingerprint: &SchemaFingerprint, ) -> Result<(), ProviderContractError> { self.trust.admit()?; + self.validate_installation_contract()?; self.compatibility .admits(required_major, required_minor, required_fingerprint) } @@ -2120,6 +2728,8 @@ impl<'de> Deserialize<'de> for ProviderManifest { api_bindings: Vec, #[serde(default)] projection_factories: Vec, + #[serde(default)] + runtime_artifacts: Vec, upgrade_policy: UpgradePolicy, } let wire = Wire::deserialize(deserializer)?; @@ -2133,6 +2743,9 @@ impl<'de> Deserialize<'de> for ProviderManifest { wire.projection_factories, wire.upgrade_policy, ) + .and_then(|manifest| { + manifest.with_target_runtime_artifacts(wire.runtime_artifacts) + }) .map_err(serde::de::Error::custom) } } @@ -2219,7 +2832,7 @@ mod tests { "sha256:0000000000000000000000000000000000000000000000000000000000000002"; const MINIMAL_PROVIDER_SPEC: &[u8] = br#"{"artifactId":"provider-wayland","config":{}}"#; - const STATEFUL_COMPONENT: &[u8] = br#"{"allowedDomains":["system"],"cardinality":1,"componentId":"volume-controller","componentType":"controller","configDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","declaresStateVolume":true,"dependencies":[{"alias":"volume","required":true}],"exportedMethods":["assess-update"],"exportedResourceTypes":["Volume"],"stateNamespaces":[{"id":"main-state","kind":"state","migrationPolicy":"pre-launch-required","persistenceClass":"persistent","placementMode":"guest-local","quotaBytes":4096,"schemaDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","schemaId":"example-provider.d2bus.org/controller/main-state","schemaVersion":"1.0","sealingRequired":false,"sensitivityClass":"private","storageNeed":"secret","views":{"main":{"rights":["read","write","create","delete","traverse"]}}}]}"#; + const STATEFUL_COMPONENT: &[u8] = br#"{"allowedDomains":["system"],"binaryRef":"volume-controller","cardinality":1,"componentId":"volume-controller","componentType":"controller","configDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","declaresStateVolume":true,"dependencies":[{"alias":"volume","required":true}],"exportedMethods":["assess-update"],"exportedResourceTypes":["Volume"],"instanceScope":"per-resource-target","stateNamespaces":[{"id":"main-state","kind":"state","migrationPolicy":"pre-launch-required","persistenceClass":"persistent","placementMode":"guest-local","quotaBytes":4096,"schemaDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","schemaId":"example-provider.d2bus.org/controller/main-state","schemaVersion":"1.0","sealingRequired":false,"sensitivityClass":"private","storageNeed":"secret","views":{"main":{"rights":["read","write","create","delete","traverse"]}}}],"supportedTargetKinds":["host","guest"],"targetCapabilities":[{"artifactDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","requiredEffectClasses":["storage"],"targetKind":"host"},{"artifactDigest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","requiredEffectClasses":["storage"],"targetKind":"guest"}]}"#; fn fingerprint(hex_tail: &str) -> SchemaFingerprint { SchemaFingerprint::parse(format!("sha256:{}{hex_tail}", "0".repeat(63))).unwrap() @@ -2278,6 +2891,29 @@ mod tests { false, ) .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("volume-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) + .unwrap() } fn state_views() -> BTreeMap { @@ -2328,7 +2964,7 @@ mod tests { } fn binding() -> ResourceApiBinding { - ResourceApiBinding::new( + ResourceApiBinding::new_with_placement( ResourceTypeName::parse("Volume").unwrap(), SchemaVersion::new(1, 0).unwrap(), fingerprint("2"), @@ -2341,6 +2977,7 @@ mod tests { .unwrap(), None, None, + PlacementAnchor::ExecutionRef, ) .unwrap() } @@ -2411,6 +3048,21 @@ mod tests { }, ) .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(), + ]) + .unwrap() } fn manifest_with_projection_factory( @@ -2429,7 +3081,23 @@ mod tests { false, ) .unwrap(); - let binding = ResourceApiBinding::new( + let controller = controller + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("semantic-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::ZoneSingleton, + [ControllerTargetKind::Zone], + ) + .unwrap() + .with_target_capabilities([ComponentTargetCapability::new( + ControllerTargetKind::Zone, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Runtime], + ) + .unwrap()]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( service_type, SchemaVersion::new(1, 0).unwrap(), fingerprint("2"), @@ -2438,6 +3106,7 @@ mod tests { StandardCapabilityMatrix::default(), None, None, + PlacementAnchor::Zone, ) .unwrap(); ProviderManifest::new( @@ -2456,6 +3125,244 @@ mod tests { ) } + #[test] + fn controller_placement_contract_round_trips_and_requires_one_instance_scope() { + let component = ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [ + ResourceTypeName::parse("Process").unwrap(), + ResourceTypeName::parse("EphemeralProcess").unwrap(), + ], + [BoundedToken::parse("assess-update").unwrap()], + [ExecutionDomain::System], + 32, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + + let bytes = canonical_json_bytes(&component).unwrap(); + let parsed: ComponentDescriptor = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(parsed, component); + assert_eq!( + parsed.instance_scope(), + Some(ControllerInstanceScope::PerResourceTarget) + ); + assert_eq!( + parsed.supported_target_kinds(), + &BTreeSet::from([ControllerTargetKind::Host, ControllerTargetKind::Guest]) + ); + assert_eq!(parsed.target_capabilities().len(), 2); + } + + #[test] + fn placement_anchor_is_registered_and_rejects_incompatible_resource_schema() { + let process = ResourceTypeName::parse("Process").unwrap(); + let valid = ResourceApiBinding::new_with_placement( + process.clone(), + SchemaVersion::new(1, 0).unwrap(), + fingerprint("2"), + SchemaVersion::new(1, 0).unwrap(), + fingerprint("3"), + StandardCapabilityMatrix::default(), + None, + None, + PlacementAnchor::ExecutionRef, + ) + .unwrap(); + assert_eq!(valid.placement_anchor(), Some(&PlacementAnchor::ExecutionRef)); + + assert_eq!( + ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + fingerprint("2"), + SchemaVersion::new(1, 0).unwrap(), + fingerprint("3"), + StandardCapabilityMatrix::default(), + None, + None, + PlacementAnchor::Zone, + ) + .unwrap_err(), + ProviderContractError::PlacementAnchorMismatch + ); + } + + #[test] + fn same_platform_runtime_artifacts_must_match_across_host_and_guest() { + let host = TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(); + let guest = TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_B).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(); + assert_eq!( + ProviderManifest::validate_runtime_artifacts([host, guest]), + Err(ProviderContractError::SharedRuntimeArtifactMismatch) + ); + } + + #[test] + fn same_platform_controller_components_use_one_artifact_digest() { + let mut subject = manifest(); + subject.components[0].target_capabilities[1].artifact_digest = + ArtifactDigest::parse(DIGEST_A).unwrap(); + + assert_eq!( + subject.validate_installation_contract(), + Err(ProviderContractError::ComponentArtifactMismatch) + ); + } + + #[test] + fn zone_singleton_cannot_own_execution_ref_resources() { + let mut subject = manifest(); + let process = ResourceTypeName::parse("Process").unwrap(); + subject.components[0].exported_resource_types = BTreeSet::from([process.clone()]); + subject.api_bindings[0].resource_type = process; + subject.api_bindings[0].placement_anchor = Some(PlacementAnchor::ExecutionRef); + let component = &mut subject.components[0]; + component.instance_scope = Some(ControllerInstanceScope::ZoneSingleton); + component.supported_target_kinds = BTreeSet::from([ControllerTargetKind::Zone]); + component.target_capabilities = vec![ + ComponentTargetCapability::new( + ControllerTargetKind::Zone, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [], + ) + .unwrap(), + ]; + + assert_eq!( + subject.validate_installation_contract(), + Err(ProviderContractError::ControllerScopeInvalid) + ); + } + + #[test] + fn per_resource_target_cannot_own_zone_resources() { + let mut subject = manifest(); + let wayland = + ResourceTypeName::parse("display-wayland.d2bus.org.WaylandSession").unwrap(); + subject.components[0].exported_resource_types = BTreeSet::from([wayland.clone()]); + subject.api_bindings[0].resource_type = wayland; + subject.api_bindings[0].placement_anchor = Some(PlacementAnchor::Zone); + subject.components[0].instance_scope = Some(ControllerInstanceScope::PerResourceTarget); + subject.components[0].supported_target_kinds = + BTreeSet::from([ControllerTargetKind::Host, ControllerTargetKind::Guest]); + subject.components[0].target_capabilities = vec![ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_B).unwrap(), + [], + ) + .unwrap(), + ]; + + assert_eq!( + subject.validate_installation_contract(), + Err(ProviderContractError::ControllerScopeInvalid) + ); + } + + #[test] + fn volume_cannot_bypass_controller_scope_admission() { + let mut subject = manifest(); + subject.api_bindings[0].placement_anchor = Some(PlacementAnchor::Zone); + + assert_eq!( + subject.validate_installation_contract(), + Err(ProviderContractError::ControllerScopeInvalid) + ); + } + + #[test] + fn installation_validation_fences_missing_scope_and_runtime_artifacts() { + let mut missing_runtime = manifest(); + missing_runtime.runtime_artifacts.clear(); + assert_eq!( + missing_runtime.validate_installation_contract(), + Err(ProviderContractError::RuntimeArtifactMissing) + ); + + let mut missing_scope = controller(); + missing_scope.instance_scope = None; + missing_scope.supported_target_kinds.clear(); + missing_scope.target_capabilities.clear(); + let invalid = ProviderManifest::new( + ArtifactId::parse("provider-volume-local").unwrap(), + digests(), + trusted(), + compatibility(), + [missing_scope], + [binding()], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_A).unwrap(), + ArtifactDigest::parse(DIGEST_A).unwrap(), + ) + .unwrap(), + ]) + .unwrap(); + assert_eq!( + invalid.validate_installation_contract(), + Err(ProviderContractError::ControllerScopeMissing) + ); + } + #[test] fn schema_vector_pins_the_minimal_provider_base_spec() { let spec = ProviderSpec::minimal(ArtifactId::parse("provider-wayland").unwrap()); @@ -2511,7 +3418,7 @@ mod tests { let parsed: ComponentDescriptor = serde_json::from_slice(&bytes).unwrap(); assert!(parsed.state_namespaces().is_empty()); assert!(!parsed.declares_state_volume()); - assert_eq!(parsed.execution(), &ComponentExecution::InProcessBootstrap); + assert!(parsed.execution().is_launchable()); } #[test] @@ -2919,6 +3826,24 @@ mod tests { ); } + #[test] + fn component_cardinality_is_bounded_before_installation() { + assert_eq!( + ComponentDescriptor::new( + BoundedToken::parse("bounded-controller").unwrap(), + ComponentType::Controller, + [ResourceTypeName::parse("Volume").unwrap()], + [], + [ExecutionDomain::System], + u32::MAX, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [], + false, + ), + Err(ProviderContractError::BoundExceeded) + ); + } + #[test] fn an_unlisted_optional_capability_is_not_supported() { let matrix = StandardCapabilityMatrix::new([ @@ -3240,6 +4165,30 @@ mod tests { false, ) .unwrap(); + let duplicate_controller = duplicate_controller + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("second-volume-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST_A).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) + .unwrap(); assert_eq!( ProviderManifest::new( ArtifactId::parse("provider-volume-local").unwrap(), @@ -3254,7 +4203,7 @@ mod tests { Err(ProviderContractError::DuplicateDeclaration) ); - let unowned_binding = ResourceApiBinding::new( + let unowned_binding = ResourceApiBinding::new_with_placement( ResourceTypeName::parse("Network").unwrap(), SchemaVersion::new(1, 0).unwrap(), fingerprint("2"), @@ -3263,6 +4212,7 @@ mod tests { StandardCapabilityMatrix::default(), None, None, + PlacementAnchor::Zone, ) .unwrap(); assert_eq!( diff --git a/packages/d2b-contracts-resource/src/v3/resource_schema.rs b/packages/d2b-contracts-resource/src/v3/resource_schema.rs index 1bae74e45e..08fa6a22e4 100644 --- a/packages/d2b-contracts-resource/src/v3/resource_schema.rs +++ b/packages/d2b-contracts-resource/src/v3/resource_schema.rs @@ -16,7 +16,7 @@ use sha2::{Digest, Sha256}; use unicode_normalization::UnicodeNormalization; use super::{ - ResourceEnvelope, ResourceName, ResourceRef, ResourceTypeName, SchemaFingerprint, + ResourceEnvelope, ResourceName, ResourceRef, ResourceTypeName, SchemaFingerprint, ZoneId, resource_status::ProviderStatusExtension, }; @@ -666,6 +666,134 @@ impl JsonSchema for SchemaVersion { } } +/// The closed set of registered ResourceType placement anchors. +/// +/// A placement anchor is a contract-owned selector, not a Provider-defined +/// field path. `Zone` resolves the containing Zone, while `ExecutionRef` +/// resolves the canonical `spec.executionRef` field to one Host or Guest. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "kebab-case")] +pub enum PlacementAnchor { + /// Place the resource at its containing Zone. + Zone, + /// Place the resource at its canonical `spec.executionRef` target. + ExecutionRef, +} + +impl PlacementAnchor { + /// Return the frozen anchor for a ResourceType whose base contract names + /// one. + pub fn canonical_for(resource_type: &ResourceTypeName) -> Option { + match resource_type.as_str() { + "Process" + | "EphemeralProcess" + | "activation-nixos.d2bus.org.NixosGeneration" + | "shell-terminal.d2bus.org.ShellSession" => Some(Self::ExecutionRef), + "display-wayland.d2bus.org.WaylandSession" => Some(Self::Zone), + _ => None, + } + } + + /// Reject an anchor that disagrees with a registered ResourceType base + /// contract. + pub fn validate_for(self, resource_type: &ResourceTypeName) -> Result<(), ResourceSchemaError> { + if Self::canonical_for(resource_type).is_some_and(|expected| expected != self) { + return Err(ResourceSchemaError::PlacementAnchorMismatch); + } + Ok(()) + } + + /// Resolve this anchor against one committed resource envelope. + pub fn resolve( + self, + envelope: &ResourceEnvelope, + ) -> Result { + match self { + Self::Zone => Ok(PlacementTarget::Zone(envelope.metadata().zone().clone())), + Self::ExecutionRef => { + let value = envelope + .spec() + .base() + .get("executionRef") + .ok_or(ResourceSchemaError::PlacementTargetMissing)?; + let CanonicalJsonValue::String(value) = value else { + return Err(ResourceSchemaError::PlacementTargetInvalid); + }; + let reference = ResourceRef::parse(value) + .map_err(|_| ResourceSchemaError::PlacementTargetInvalid)?; + let target_kind = match reference.resource_type().as_str() { + "Host" => PlacementTargetKind::Host, + "Guest" => PlacementTargetKind::Guest, + _ => return Err(ResourceSchemaError::PlacementTargetWrongType), + }; + Ok(PlacementTarget::Execution { + kind: target_kind, + reference, + }) + } + } + } +} + +/// The target kind resolved by an execution-reference placement anchor. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "lowercase")] +pub enum PlacementTargetKind { + /// The physical Host target. + Host, + /// A workload or nested Guest target. + Guest, +} + +/// The resolved value of a registered placement anchor. +#[derive(Clone, PartialEq, Eq)] +pub enum PlacementTarget { + /// The resource is reconciled by the Zone singleton. + Zone(ZoneId), + /// The resource is reconciled at one exact Host or Guest. + Execution { + /// The resolved target kind. + kind: PlacementTargetKind, + /// The exact execution target reference. + reference: ResourceRef, + }, +} + +impl core::fmt::Debug for PlacementTarget { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Zone(_) => formatter.write_str("PlacementTarget::Zone()"), + Self::Execution { kind, .. } => formatter + .debug_struct("PlacementTarget::Execution") + .field("kind", kind) + .finish_non_exhaustive(), + } + } +} + +impl PlacementTarget { + /// Return the target kind for an execution placement, or `None` for the + /// Zone singleton. + pub const fn execution_kind(&self) -> Option { + match self { + Self::Zone(_) => None, + Self::Execution { kind, .. } => Some(*kind), + } + } + + /// Borrow the exact execution target reference, if one was resolved. + pub fn execution_ref(&self) -> Option<&ResourceRef> { + match self { + Self::Zone(_) => None, + Self::Execution { reference, .. } => Some(reference), + } + } +} + /// Extension-schema layer encoded in a schema ID. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ExtensionSchemaLayer { @@ -1185,6 +1313,10 @@ pub enum ResourceSchemaError { ProviderSchemaMismatch, ProviderFieldShadowsBase(String), ProviderExtensionNotMinimal, + PlacementAnchorMismatch, + PlacementTargetMissing, + PlacementTargetInvalid, + PlacementTargetWrongType, } impl core::fmt::Debug for ResourceSchemaError { @@ -1207,6 +1339,10 @@ impl core::fmt::Debug for ResourceSchemaError { Self::ProviderSchemaMismatch => "ProviderSchemaMismatch", Self::ProviderFieldShadowsBase(_) => "ProviderFieldShadowsBase", Self::ProviderExtensionNotMinimal => "ProviderExtensionNotMinimal", + Self::PlacementAnchorMismatch => "PlacementAnchorMismatch", + Self::PlacementTargetMissing => "PlacementTargetMissing", + Self::PlacementTargetInvalid => "PlacementTargetInvalid", + Self::PlacementTargetWrongType => "PlacementTargetWrongType", }; write!(f, "ResourceSchemaError::{kind}") } @@ -1254,6 +1390,18 @@ impl core::fmt::Display for ResourceSchemaError { Self::ProviderExtensionNotMinimal => { f.write_str("minimal base spec must not require a Provider extension") } + Self::PlacementAnchorMismatch => { + f.write_str("placement anchor does not match the registered ResourceType schema") + } + Self::PlacementTargetMissing => { + f.write_str("placement anchor field is missing from the committed resource") + } + Self::PlacementTargetInvalid => { + f.write_str("placement anchor value is not a valid target reference") + } + Self::PlacementTargetWrongType => { + f.write_str("placement anchor must resolve to a Host or Guest") + } } } } @@ -1514,6 +1662,34 @@ key":1}"#, ); } + #[test] + fn placement_anchor_registry_is_closed_and_resolves_execution_targets() { + let process = ResourceTypeName::parse("Process").unwrap(); + let generation = + ResourceTypeName::parse("activation-nixos.d2bus.org.NixosGeneration").unwrap(); + let shell = ResourceTypeName::parse("shell-terminal.d2bus.org.ShellSession").unwrap(); + let wayland = + ResourceTypeName::parse("display-wayland.d2bus.org.WaylandSession").unwrap(); + + assert_eq!( + PlacementAnchor::canonical_for(&process), + Some(PlacementAnchor::ExecutionRef) + ); + assert_eq!( + PlacementAnchor::canonical_for(&generation), + Some(PlacementAnchor::ExecutionRef) + ); + assert_eq!( + PlacementAnchor::canonical_for(&shell), + Some(PlacementAnchor::ExecutionRef) + ); + assert_eq!( + PlacementAnchor::canonical_for(&wayland), + Some(PlacementAnchor::Zone) + ); + assert!(serde_json::from_str::("\"field-path\"").is_err()); + } + #[test] fn minimal_base_and_base_binding_conformance_are_exact() { let contract = contract(); diff --git a/packages/d2b-core-controller/src/api_catalog.rs b/packages/d2b-core-controller/src/api_catalog.rs index 95608be0e0..2645ed002f 100644 --- a/packages/d2b-core-controller/src/api_catalog.rs +++ b/packages/d2b-core-controller/src/api_catalog.rs @@ -3,8 +3,16 @@ use std::collections::{BTreeMap, BTreeSet}; use d2b_contracts_provider::v3::{ + ArtifactDigest, ProviderManifest, + ComponentTargetCapability, + ControllerInstanceScope, + ControllerTargetKind, + EffectPortClass, ResourceApiBinding, + TargetRuntimeArtifacts, + BinaryRef, + ComponentExecution, }; use d2b_contracts_resource::v3::{ ResourceRef, @@ -272,6 +280,7 @@ use d2b_contracts_resource::v3::{ ArtifactId, SchemaVersion, execution_policy::{BoundedToken, ExecutionDomain}, + resource_schema::PlacementAnchor, }; use super::*; @@ -326,8 +335,31 @@ use d2b_contracts_resource::v3::{ [], false, ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest(), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest(), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) .unwrap()], - [ResourceApiBinding::new( + [ResourceApiBinding::new_with_placement( resource_type, SchemaVersion::new(1, 0).unwrap(), fingerprint('2'), @@ -336,6 +368,7 @@ use d2b_contracts_resource::v3::{ StandardCapabilityMatrix::default(), None, None, + PlacementAnchor::ExecutionRef, ) .unwrap()], [], @@ -346,6 +379,11 @@ use d2b_contracts_resource::v3::{ }, ) .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new(ControllerTargetKind::Host, digest(), digest()).unwrap(), + TargetRuntimeArtifacts::new(ControllerTargetKind::Guest, digest(), digest()).unwrap(), + ]) + .unwrap() } fn admission<'a>( diff --git a/packages/d2b-core-controller/src/providers.rs b/packages/d2b-core-controller/src/providers.rs index 76899803da..b5d4deece5 100644 --- a/packages/d2b-core-controller/src/providers.rs +++ b/packages/d2b-core-controller/src/providers.rs @@ -1,8 +1,14 @@ //! Provider lifecycle validation and child-resource planning. use d2b_contracts_provider::v3::{ + BinaryRef, + ComponentExecution, + ComponentTargetCapability, + ControllerTargetKind, + EffectPortClass, ComponentType, ProviderManifest, + TargetRuntimeArtifacts, }; use d2b_contracts_resource::v3::{ ResourceRef, @@ -135,6 +141,9 @@ impl ProviderHandler { required_descriptor_fingerprint, ) .map_err(|_| ProviderError::TrustOrCompatibilityDenied)?; + manifest + .validate_installation_contract() + .map_err(|_| ProviderError::GraphInvalid)?; if !observation.package_present { return Err(ProviderError::PackageUnavailable); } @@ -281,6 +290,24 @@ use d2b_contracts_resource::v3::{ [], false, ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("service").unwrap(), + }) + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest(), + [EffectPortClass::Runtime], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest(), + [EffectPortClass::Runtime], + ) + .unwrap(), + ]) .unwrap()], [], [], @@ -291,6 +318,11 @@ use d2b_contracts_resource::v3::{ }, ) .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new(ControllerTargetKind::Host, digest(), digest()).unwrap(), + TargetRuntimeArtifacts::new(ControllerTargetKind::Guest, digest(), digest()).unwrap(), + ]) + .unwrap() } fn observation() -> ProviderObservation { diff --git a/packages/d2b-provider-device-security-key/src/descriptor.rs b/packages/d2b-provider-device-security-key/src/descriptor.rs index 243367b680..d0ac5896de 100644 --- a/packages/d2b-provider-device-security-key/src/descriptor.rs +++ b/packages/d2b-provider-device-security-key/src/descriptor.rs @@ -195,7 +195,7 @@ pub fn security_key_factory_fingerprint() fn base_binding( member: &SemanticTypeContract, ) -> Result { - ResourceApiBinding::new( + ResourceApiBinding::new_with_placement( member.resource_type().clone(), member.spec().version(), member.spec().fingerprint().clone(), @@ -204,5 +204,6 @@ fn base_binding( StandardCapabilityMatrix::default(), None::, None::, + d2b_contracts_resource::v3::resource_schema::PlacementAnchor::Zone, ) } diff --git a/packages/d2b-provider-toolkit/src/bin/d2b-provider-toolkit.rs b/packages/d2b-provider-toolkit/src/bin/d2b-provider-toolkit.rs index 24b6204297..9c8d657636 100644 --- a/packages/d2b-provider-toolkit/src/bin/d2b-provider-toolkit.rs +++ b/packages/d2b-provider-toolkit/src/bin/d2b-provider-toolkit.rs @@ -59,6 +59,8 @@ fn emit_command(args: &mut impl Iterator) -> Result<( .map_err(|_| "failed to read the manifest from stdin".to_owned())?; let manifest = serde_json::from_slice::(&input) .map_err(|_| "failed to parse the Provider manifest from stdin".to_owned())?; + manifest::validate_for_installation(&manifest) + .map_err(|_| "Provider manifest failed placement admission".to_owned())?; let bytes = manifest::emit_canonical(&manifest); write_output(Path::new(&output), &bytes) } diff --git a/packages/d2b-provider-toolkit/src/lib.rs b/packages/d2b-provider-toolkit/src/lib.rs index 9f62a03af5..8e41b86343 100644 --- a/packages/d2b-provider-toolkit/src/lib.rs +++ b/packages/d2b-provider-toolkit/src/lib.rs @@ -82,7 +82,9 @@ pub use fixture::{ DeterministicClock, FakeProvider, Fixture, SampleLeaseRequest, sample_lease_request, }; pub use redaction::Redacted; -pub use registration::{ExactRegistration, ToolkitError, register_exact_instances}; +pub use registration::{ + ExactRegistration, ToolkitError, register_exact_instances, validate_manifest_registration, +}; pub use runtime::{ ProviderAdmission, ProviderEntrypoint, ProviderLifecycle, ProviderRuntimeError, ProviderSessionAdmission, diff --git a/packages/d2b-provider-toolkit/src/manifest.rs b/packages/d2b-provider-toolkit/src/manifest.rs index 9d0e168552..f31d63df26 100644 --- a/packages/d2b-provider-toolkit/src/manifest.rs +++ b/packages/d2b-provider-toolkit/src/manifest.rs @@ -5,7 +5,10 @@ //! used for both writing and checking rather than a general-purpose JSON //! formatter. -use d2b_contracts_provider::v3::ProviderManifest; +use d2b_contracts_provider::v3::{ + ProviderContractError, + ProviderManifest, +}; use d2b_contracts_resource::v3::canonical_json_bytes; use serde_json::Error as JsonError; @@ -75,6 +78,14 @@ pub fn emit_canonical(manifest: &ProviderManifest) -> Vec { canonical_json_bytes(manifest).expect("ProviderManifest must be canonicalizable") } +/// Validate the signed placement, target-artifact, and EffectPort contract +/// before a manifest enters a package or catalog. +pub fn validate_for_installation( + manifest: &ProviderManifest, +) -> Result<(), ProviderContractError> { + manifest.validate_installation_contract() +} + /// Verify that a serialized Provider manifest is already canonical. /// /// The same typed parse and canonical re-emission used by the CLI are exposed @@ -83,6 +94,7 @@ pub fn emit_canonical(manifest: &ProviderManifest) -> Vec { pub fn verify_canonical(bytes: &[u8]) -> Result<(), VerificationError> { let manifest = serde_json::from_slice::(bytes) .map_err(|_: JsonError| VerificationError::InvalidManifest)?; + validate_for_installation(&manifest).map_err(|_| VerificationError::InvalidManifest)?; let expected = emit_canonical(&manifest); if expected == bytes { return Ok(()); diff --git a/packages/d2b-provider-toolkit/src/registration.rs b/packages/d2b-provider-toolkit/src/registration.rs index 3679d5fd18..d50d979e0a 100644 --- a/packages/d2b-provider-toolkit/src/registration.rs +++ b/packages/d2b-provider-toolkit/src/registration.rs @@ -7,6 +7,7 @@ use std::{collections::BTreeSet, error::Error, fmt}; use d2b_provider::{ProviderDescriptor, ProviderRegistryBuilder, RegistryBuildError}; +use d2b_contracts_provider::v3::ProviderManifest; /// A value accepted by [`register_exact_instances`]. pub trait ExactRegistration { @@ -31,6 +32,8 @@ pub enum ToolkitError { DuplicateProvider, /// The runtime registry rejected the transaction. Registry(RegistryBuildError), + /// The signed manifest failed placement or target-artifact admission. + ManifestInvalid, } impl fmt::Display for ToolkitError { @@ -40,6 +43,7 @@ impl fmt::Display for ToolkitError { Self::DescriptorInvalid => "provider descriptor is invalid", Self::DuplicateProvider => "duplicate provider registration", Self::Registry(_) => "provider registry rejected registration", + Self::ManifestInvalid => "provider manifest failed placement admission", }) } } @@ -87,6 +91,13 @@ where Ok(()) } +/// Validate a signed Provider manifest before registration. +pub fn validate_manifest_registration(manifest: &ProviderManifest) -> Result<(), ToolkitError> { + manifest + .validate_installation_contract() + .map_err(|_| ToolkitError::ManifestInvalid) +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/d2b-provider-toolkit/tests/conformance.rs b/packages/d2b-provider-toolkit/tests/conformance.rs index e2c81f5eb1..50ac87a865 100644 --- a/packages/d2b-provider-toolkit/tests/conformance.rs +++ b/packages/d2b-provider-toolkit/tests/conformance.rs @@ -11,7 +11,13 @@ use std::{ }; use d2b_contracts_provider::v3::{ - provider::{ArtifactDigest, ArtifactDigestSet, CompatibilityRange, ComponentDescriptor, ComponentType, PolicyEvaluation, ProviderManifest, ResourceApiBinding, RevocationState, SignatureState, StandardCapabilityMatrix, TrustEvidence, UpgradeDisposition, UpgradePolicy}, + provider::{ + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, ProviderManifest, + ResourceApiBinding, RevocationState, SignatureState, StandardCapabilityMatrix, + TargetRuntimeArtifacts, TrustEvidence, UpgradeDisposition, UpgradePolicy, + }, }; use d2b_contracts_resource::v3::{ execution_policy::{BoundedToken, ExecutionDomain}, @@ -19,7 +25,7 @@ use d2b_contracts_resource::v3::{ use d2b_contracts_resource::v3::{ ArtifactId, identity::{ResourceTypeName, SchemaFingerprint}, - resource_schema::SchemaVersion, + resource_schema::{PlacementAnchor, SchemaVersion}, }; use d2b_provider::{ AdmissionOptions, CancellationToken, ProviderClass, ProviderMethodName, @@ -32,7 +38,7 @@ use d2b_provider_toolkit::{ use sha2::{Digest, Sha256}; const MANIFEST_DIGEST_VECTOR: &str = - "sha256:74fd4995e645525d106ff96cc147a9d7d596d96ae178a6d1f2db085dbb4db9fb"; + "sha256:2eecca84f4898bb0890c3fd4b80fc1394f5d28bd305c6b7e5c06bccec3086474"; const MANIFEST_DIGEST: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000001"; static TEMP_FILE_SEQUENCE: AtomicU64 = AtomicU64::new(0); @@ -232,8 +238,31 @@ fn test_manifest() -> ProviderManifest { [], false, ) - .expect("valid component"); - let binding = ResourceApiBinding::new( + .expect("valid component") + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("volume-controller").expect("valid binary ref"), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .expect("valid controller placement") + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest.clone(), + [EffectPortClass::Storage], + ) + .expect("valid host capability"), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest.clone(), + [EffectPortClass::Storage], + ) + .expect("valid guest capability"), + ]) + .expect("valid target capabilities"); + let binding = ResourceApiBinding::new_with_placement( ResourceTypeName::parse("Volume").expect("valid resource type"), SchemaVersion::new(1, 0).expect("valid schema version"), fingerprint("2"), @@ -242,6 +271,7 @@ fn test_manifest() -> ProviderManifest { StandardCapabilityMatrix::default(), None, None, + PlacementAnchor::ExecutionRef, ) .expect("valid binding"); ProviderManifest::new( @@ -252,7 +282,7 @@ fn test_manifest() -> ProviderManifest { manifest: digest.clone(), config: digest.clone(), schema: digest.clone(), - service: digest, + service: digest.clone(), }, TrustEvidence { publisher: BoundedToken::parse("first-party").expect("valid publisher"), @@ -284,6 +314,21 @@ fn test_manifest() -> ProviderManifest { }, ) .expect("valid manifest") + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + digest.clone(), + digest.clone(), + ) + .expect("valid host runtime artifacts"), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + digest.clone(), + digest.clone(), + ) + .expect("valid guest runtime artifacts"), + ]) + .expect("valid shared runtime artifacts") } fn fingerprint(tail: &str) -> SchemaFingerprint { diff --git a/packages/d2b-provider-toolkit/tests/fake_provider.rs b/packages/d2b-provider-toolkit/tests/fake_provider.rs index 8e06b0be72..e3886c558b 100644 --- a/packages/d2b-provider-toolkit/tests/fake_provider.rs +++ b/packages/d2b-provider-toolkit/tests/fake_provider.rs @@ -15,15 +15,21 @@ use std::collections::BTreeMap; use d2b_contracts_provider::v3::{ ArtifactDigest, ArtifactDigestSet, + BinaryRef, CapabilitySupport, CompatibilityRange, ComponentDescriptor, + ComponentExecution, + ComponentTargetCapability, ComponentStateKind, ComponentStateNamespace, ComponentStateView, ComponentType, + ControllerInstanceScope, + ControllerTargetKind, DependencyAlias, DependencyDeclaration, + EffectPortClass, PolicyEvaluation, ProviderManifest, ProviderSpec, @@ -33,6 +39,7 @@ use d2b_contracts_provider::v3::{ StandardCapabilityMatrix, StorageNeed, TrustEvidence, + TargetRuntimeArtifacts, UpgradeDisposition, UpgradePolicy, }; @@ -44,7 +51,7 @@ use d2b_contracts_resource::v3::{ execution_policy::{BoundedToken, ExecutionDomain}, ResourceTypeName, SchemaFingerprint, - resource_schema::SchemaVersion, + resource_schema::{PlacementAnchor, SchemaVersion}, volume::ViewRight, volume_state::{MigrationPolicy, PersistenceClass, SensitivityClass, VolumeStateSchemaId}, }; @@ -110,6 +117,29 @@ pub fn controller() -> ComponentDescriptor { false, ) .expect("a controller owning one ResourceType is valid") + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("volume-controller").expect("valid binary ref"), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) + .expect("target capabilities are valid") } fn state_namespace() -> ComponentStateNamespace { @@ -141,7 +171,7 @@ fn state_namespace() -> ComponentStateNamespace { } pub fn binding() -> ResourceApiBinding { - ResourceApiBinding::new( + ResourceApiBinding::new_with_placement( ResourceTypeName::parse("Volume").expect("standard type"), SchemaVersion::new(1, 0).expect("valid version"), fingerprint("2"), @@ -154,6 +184,7 @@ pub fn binding() -> ResourceApiBinding { .expect("a single-entry matrix is valid"), None, None, + PlacementAnchor::ExecutionRef, ) .expect("a binding for an owned ResourceType is valid") } @@ -182,6 +213,22 @@ pub fn manifest_with( preserves_durable_state: true, }, ) + .and_then(|manifest| { + manifest.with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + ArtifactDigest::parse(DIGEST).expect("valid digest"), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + ArtifactDigest::parse(DIGEST).expect("valid digest"), + ) + .unwrap(), + ]) + }) } pub fn manifest() -> ProviderManifest { @@ -285,18 +332,7 @@ fn a_declared_state_volume_is_visible_and_a_stateless_provider_declares_none() { .expect("a stateful manifest is valid"); assert!(manifest.declares_state_volume()); - let stateless = ComponentDescriptor::new( - BoundedToken::parse("volume-controller").expect("valid id"), - ComponentType::Controller, - [ResourceTypeName::parse("Volume").expect("standard type")], - [BoundedToken::parse("assess-update").expect("valid method")], - [ExecutionDomain::System], - 1, - ArtifactDigest::parse(DIGEST).expect("valid digest"), - [], - false, - ) - .expect("a stateless controller is valid"); + let stateless = controller(); let manifest = manifest_with(trusted(), vec![stateless], vec![binding()]) .expect("a stateless manifest is valid"); // An empty ProviderStateSet is a normal outcome, not a defect. diff --git a/packages/d2b-provider-toolkit/tests/malicious_provider.rs b/packages/d2b-provider-toolkit/tests/malicious_provider.rs index 5a1ab9ee01..a8c6e4a0d4 100644 --- a/packages/d2b-provider-toolkit/tests/malicious_provider.rs +++ b/packages/d2b-provider-toolkit/tests/malicious_provider.rs @@ -15,13 +15,19 @@ use d2b_contracts_provider::v3::semantic_services::catalog; use d2b_contracts_provider::v3::{ ArtifactDigest, ArtifactDigestSet, + BinaryRef, BindingTargetType, CapabilitySupport, CompatibilityRange, ComponentDescriptor, + ComponentExecution, + ComponentTargetCapability, ComponentType, + ControllerInstanceScope, + ControllerTargetKind, DependencyAlias, DependencyDeclaration, + EffectPortClass, Exportability, ExtensionSchemaRegistration, PolicyEvaluation, @@ -33,6 +39,7 @@ use d2b_contracts_provider::v3::{ SignatureState, StandardCapabilityMatrix, TrustEvidence, + TargetRuntimeArtifacts, UpgradeDisposition, UpgradePolicy, }; @@ -45,7 +52,7 @@ use d2b_contracts_resource::v3::{ execution_policy::{BoundedToken, ExecutionDomain}, ResourceTypeName, SchemaFingerprint, - resource_schema::{ExtensionSchemaId, SchemaVersion, canonical_json_bytes}, + resource_schema::{ExtensionSchemaId, PlacementAnchor, SchemaVersion, canonical_json_bytes}, }; use d2b_contracts_resource::v3::identity::{ Locality, @@ -104,6 +111,29 @@ fn controller(component_id: &str) -> ComponentDescriptor { false, ) .expect("a controller owning one ResourceType is valid") + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse(component_id).expect("valid binary ref"), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .expect("valid controller placement") + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + [EffectPortClass::Storage], + ) + .expect("valid host capability"), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + [EffectPortClass::Storage], + ) + .expect("valid guest capability"), + ]) + .expect("valid target capabilities") } fn controller_for_resource(resource_type: &str) -> ComponentDescriptor { @@ -119,11 +149,35 @@ fn controller_for_resource(resource_type: &str) -> ComponentDescriptor { false, ) .expect("a controller owning one ResourceType is valid") + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("semantic-controller").expect("valid binary ref"), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .expect("valid controller placement") + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + [EffectPortClass::Runtime], + ) + .expect("valid host capability"), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + [EffectPortClass::Runtime], + ) + .expect("valid guest capability"), + ]) + .expect("valid target capabilities") } fn binding_for(resource_type: &str) -> ResourceApiBinding { - ResourceApiBinding::new( - ResourceTypeName::parse(resource_type).expect("valid type"), + let resource_type = ResourceTypeName::parse(resource_type).expect("valid type"); + ResourceApiBinding::new_with_placement( + resource_type.clone(), SchemaVersion::new(1, 0).expect("valid version"), fingerprint("2"), SchemaVersion::new(1, 0).expect("valid version"), @@ -135,6 +189,7 @@ fn binding_for(resource_type: &str) -> ResourceApiBinding { .expect("a single-entry matrix is valid"), None, None, + PlacementAnchor::canonical_for(&resource_type).unwrap_or(PlacementAnchor::ExecutionRef), ) .expect("a binding is valid") } @@ -216,6 +271,22 @@ fn manifest_with( preserves_durable_state: true, }, ) + .and_then(|manifest| { + manifest.with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + ArtifactDigest::parse(DIGEST).expect("valid digest"), + ) + .expect("valid host runtime artifacts"), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST).expect("valid digest"), + ArtifactDigest::parse(DIGEST).expect("valid digest"), + ) + .expect("valid guest runtime artifacts"), + ]) + }) } fn manifest_for_factory( diff --git a/packages/d2b-provider/src/installation.rs b/packages/d2b-provider/src/installation.rs index c5b402a737..b66804af8e 100644 --- a/packages/d2b-provider/src/installation.rs +++ b/packages/d2b-provider/src/installation.rs @@ -25,7 +25,11 @@ //! no type here carries authority. An [`InstalledProvider`] is a decision //! that has already been reached, not a capability that can be presented. +use std::collections::BTreeSet; + use d2b_contracts_provider::v3::{ + ControllerTargetKind, + EffectPortClass, ProviderManifest, ProviderSpec, }; @@ -112,6 +116,50 @@ pub struct InstalledProvider { provider_ref: ResourceRef, } +/// The fixed EffectPort classes available from one Host or Guest target +/// profile during Provider installation. +#[derive(Clone, PartialEq, Eq)] +pub struct TargetInstallProfile { + target_kind: ControllerTargetKind, + supported_effect_classes: BTreeSet, +} + +impl TargetInstallProfile { + /// Construct a target profile from the fixed daemon composition. + pub fn new( + target_kind: ControllerTargetKind, + supported_effect_classes: impl IntoIterator, + ) -> Self { + Self { + target_kind, + supported_effect_classes: supported_effect_classes.into_iter().collect(), + } + } + + /// Return the target kind. + pub const fn target_kind(&self) -> ControllerTargetKind { + self.target_kind + } + + /// Borrow the profile's supported EffectPort classes. + pub const fn supported_effect_classes(&self) -> &BTreeSet { + &self.supported_effect_classes + } +} + +impl core::fmt::Debug for TargetInstallProfile { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("TargetInstallProfile") + .field("target_kind", &self.target_kind) + .field( + "effect_class_count", + &self.supported_effect_classes.len(), + ) + .finish_non_exhaustive() + } +} + impl InstalledProvider { /// The admitted `Provider/` reference. pub const fn provider_ref(&self) -> &ResourceRef { @@ -142,9 +190,13 @@ pub fn admit_installation( readiness: ProviderReadiness, ) -> Result { descriptor.validate()?; + manifest + .validate_installation_contract() + .map_err(|_| RegistryBuildError::ArtifactNotAdmissible)?; if spec.artifact_id() != manifest.artifact_id() { return Err(RegistryBuildError::ArtifactSelectionMismatch); } + manifest .admit( required_api.api_major(), @@ -163,6 +215,25 @@ pub fn admit_installation( }) } +/// Admit a Provider installation against one fixed target profile. +pub fn admit_installation_for_target( + spec: &ProviderSpec, + manifest: &ProviderManifest, + descriptor: &ProviderDescriptor, + required_api: &RequiredProviderApi, + readiness: ProviderReadiness, + profile: &TargetInstallProfile, +) -> Result { + let installed = admit_installation(spec, manifest, descriptor, required_api, readiness)?; + manifest + .validate_target_effects( + profile.target_kind(), + profile.supported_effect_classes(), + ) + .map_err(|_| RegistryBuildError::ArtifactNotAdmissible)?; + Ok(installed) +} + /// Whether every method the registry descriptor publishes appears in the /// signed component graph's exported method set. /// @@ -188,14 +259,21 @@ mod tests { use d2b_contracts_provider::v3::{ ArtifactDigest, ArtifactDigestSet, + BinaryRef, ComponentDescriptor, + ComponentExecution, + ComponentTargetCapability, ComponentType, + ControllerInstanceScope, + ControllerTargetKind, + EffectPortClass, PolicyEvaluation, ResourceApiBinding, RevocationState, SignatureState, StandardCapabilityMatrix, TrustEvidence, + TargetRuntimeArtifacts, UpgradeDisposition, UpgradePolicy, }; @@ -205,7 +283,7 @@ mod tests { ConfigurationGeneration, ResourceGeneration, ResourceTypeName, - resource_schema::SchemaVersion, + resource_schema::{PlacementAnchor, SchemaVersion}, }; use d2b_contracts_resource::v3::identity::ServiceName; use d2b_contracts_zone_session::v3::zone_routing::ZonePath; @@ -252,8 +330,31 @@ mod tests { [], false, ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("volume-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + ArtifactDigest::parse(DIGEST).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + ArtifactDigest::parse(DIGEST).unwrap(), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) .unwrap(); - let binding = ResourceApiBinding::new( + let binding = ResourceApiBinding::new_with_placement( ResourceTypeName::parse("Volume").unwrap(), SchemaVersion::new(1, 0).unwrap(), fingerprint("2"), @@ -262,6 +363,7 @@ mod tests { StandardCapabilityMatrix::default(), None, None, + PlacementAnchor::ExecutionRef, ) .unwrap(); let digest = ArtifactDigest::parse(DIGEST).unwrap(); @@ -273,7 +375,7 @@ mod tests { manifest: digest.clone(), config: digest.clone(), schema: digest.clone(), - service: digest, + service: digest.clone(), }, trust, d2b_contracts_provider::v3::provider::CompatibilityRange { @@ -292,6 +394,21 @@ mod tests { }, ) .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + digest.clone(), + digest.clone(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + digest.clone(), + digest.clone(), + ) + .unwrap(), + ]) + .unwrap() } fn descriptor(methods: &[&str]) -> ProviderDescriptor { @@ -338,6 +455,40 @@ mod tests { assert_eq!(format!("{installed:?}"), "InstalledProvider()"); } + #[test] + fn target_profile_must_support_every_signed_effect_class() { + let profile = TargetInstallProfile::new( + ControllerTargetKind::Host, + [EffectPortClass::Runtime], + ); + assert_eq!( + admit_installation_for_target( + &spec("provider-volume-local"), + &manifest(trusted()), + &descriptor(&["assess-update"]), + &required(), + ProviderReadiness::Ready, + &profile, + ) + .unwrap_err(), + RegistryBuildError::ArtifactNotAdmissible + ); + + let profile = TargetInstallProfile::new( + ControllerTargetKind::Host, + [EffectPortClass::Storage], + ); + assert!(admit_installation_for_target( + &spec("provider-volume-local"), + &manifest(trusted()), + &descriptor(&["assess-update"]), + &required(), + ProviderReadiness::Ready, + &profile, + ) + .is_ok()); + } + #[test] fn package_presence_is_not_installation() { for readiness in [ProviderReadiness::NotReady, ProviderReadiness::Quarantined] { diff --git a/packages/d2b-provider/src/lib.rs b/packages/d2b-provider/src/lib.rs index a6604bf0e7..a6a7767984 100644 --- a/packages/d2b-provider/src/lib.rs +++ b/packages/d2b-provider/src/lib.rs @@ -63,7 +63,8 @@ pub use identity::{ ProviderMethodName, }; pub use installation::{ - InstalledProvider, ProviderReadiness, RequiredProviderApi, admit_installation, + InstalledProvider, ProviderReadiness, RequiredProviderApi, TargetInstallProfile, + admit_installation, admit_installation_for_target, }; pub use registry::{ AdmissionOptions, AdmittedProvider, InFlightPermit, MAX_REGISTRY_DRAIN_MS, ProviderRegistry, diff --git a/packages/d2b-resource-compiler/tests/phase2.rs b/packages/d2b-resource-compiler/tests/phase2.rs index bf77276d2d..0fd4b3f8e4 100644 --- a/packages/d2b-resource-compiler/tests/phase2.rs +++ b/packages/d2b-resource-compiler/tests/phase2.rs @@ -9,21 +9,29 @@ use base64::{Engine as _, engine::general_purpose::STANDARD}; use d2b_contracts_provider::v3::{ ArtifactDigest, ArtifactDigestSet, + ComponentTargetCapability, CompatibilityRange, ComponentDescriptor, ComponentType, + ControllerInstanceScope, + ControllerTargetKind, + EffectPortClass, PolicyEvaluation, ProviderManifest, + ResourceApiBinding, RevocationState, SignatureState, + StandardCapabilityMatrix, TrustEvidence, - provider::{BinaryRef, ComponentExecution, UpgradeDisposition, UpgradePolicy}, + provider::{ + BinaryRef, ComponentExecution, TargetRuntimeArtifacts, UpgradeDisposition, UpgradePolicy, + }, }; use d2b_contracts_resource::v3::{ ArtifactId, CanonicalJsonValue, ResourceTypeName, canonical_json_bytes, execution_policy::{BoundedToken, ExecutionDomain}, identity::SchemaFingerprint, - resource_schema::SchemaVersion, + resource_schema::{PlacementAnchor, SchemaVersion}, }; use d2b_core::provider_artifact::{ AnchoredDir, Argv, Envp, ExecutableFile, LaunchError, LayoutDir, LayoutError, LayoutPath, @@ -247,12 +255,44 @@ fn manifest( false, ) .unwrap() - .with_execution(execution); + .with_execution(execution) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + config_digest.clone(), + [EffectPortClass::Storage], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + config_digest.clone(), + [EffectPortClass::Storage], + ) + .unwrap(), + ]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( + ResourceTypeName::parse("Volume").unwrap(), + SchemaVersion::new(1, 0).unwrap(), + fingerprint(), + SchemaVersion::new(1, 0).unwrap(), + fingerprint(), + StandardCapabilityMatrix::default(), + None, + None, + PlacementAnchor::ExecutionRef, + ) + .unwrap(); ProviderManifest::new( ArtifactId::parse(artifact_id).unwrap(), ArtifactDigestSet { package: package_digest, - executable: executable_digest, + executable: executable_digest.clone(), manifest: ArtifactDigest::parse( "sha256:0000000000000000000000000000000000000000000000000000000000000001", ) @@ -286,7 +326,7 @@ fn manifest( state_schema_version: SchemaVersion::new(1, 0).unwrap(), }, [component], - [], + [binding], [], UpgradePolicy { drain_before_upgrade: true, @@ -295,6 +335,21 @@ fn manifest( }, ) .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + executable_digest.clone(), + executable_digest.clone(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + executable_digest.clone(), + executable_digest.clone(), + ) + .unwrap(), + ]) + .unwrap() } fn pem(public_key: &[u8]) -> Vec { diff --git a/packages/xtask/src/provider_packaging.rs b/packages/xtask/src/provider_packaging.rs index 94f79a055f..6b336097d3 100644 --- a/packages/xtask/src/provider_packaging.rs +++ b/packages/xtask/src/provider_packaging.rs @@ -114,6 +114,18 @@ const EXCLUDED_MECHANISMS: &[&str] = &[ "version-range-solving", ]; +/// Placement fields emitted by the signed Provider manifest contract. +const PLACEMENT_CONTRACT_FIELDS: &[&str] = &[ + "instanceScope", + "supportedTargetKinds", + "targetCapabilities", + "placementAnchor", +]; + +/// Shared Host/Guest runtime artifact fields emitted by the signed manifest +/// contract. +const RUNTIME_CONTRACT_FIELDS: &[&str] = &["d2bdDigest", "brokerDigest"]; + fn nix_string(value: &str) -> String { let mut escaped = String::with_capacity(value.len() + 2); escaped.push('"'); @@ -202,6 +214,28 @@ fn generated_catalog_shape_module() -> String { ) )); + out.push_str("\n # Signed placement-contract fields carried by the Provider\n"); + out.push_str(" # manifest and consumed by Core admission.\n"); + out.push_str(&format!( + " placementContractFields = {};\n", + nix_string_list( + PLACEMENT_CONTRACT_FIELDS + .iter() + .map(|field| (*field).to_owned()), + 2 + ) + )); + out.push_str("\n # Signed shared daemon and broker artifact fields.\n"); + out.push_str(&format!( + " runtimeContractFields = {};\n", + nix_string_list( + RUNTIME_CONTRACT_FIELDS + .iter() + .map(|field| (*field).to_owned()), + 2 + ) + )); + out.push_str("}\n"); out } @@ -284,6 +318,20 @@ mod tests { assert_eq!(EXCLUDED_MECHANISMS, sorted.as_slice()); } + #[test] + fn signed_contract_field_groups_are_closed_and_deterministic() { + assert_eq!( + PLACEMENT_CONTRACT_FIELDS, + &[ + "instanceScope", + "supportedTargetKinds", + "targetCapabilities", + "placementAnchor", + ] + ); + assert_eq!(RUNTIME_CONTRACT_FIELDS, &["d2bdDigest", "brokerDigest"]); + } + #[test] fn the_rendered_module_is_byte_identical_across_repeated_renders() { let first = generated_catalog_shape_module(); From cf740d181410386d95d166607823e4286df04b84 Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 11:20:29 -0700 Subject: [PATCH 02/56] broker: unify host and guest profiles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- .github/workflows/release-host-binaries.yml | 24 +- AGENTS.md | 4 +- BUILD.bazel | 2 +- CONTRIBUTING.md | 2 +- Cargo.lock | 52 +- Cargo.toml | 4 +- Makefile | 2 +- README.md | 2 +- SECURITY.md | 4 +- STRATEGY.md | 2 +- TODO.md | 8 +- bazel/checks/nix/BUILD.bazel | 11 + bazel/checks/rust/BUILD.bazel | 39 +- changelog.d/adr046-u11-broker-profiles.md | 3 + docs/contributing/architecture.md | 8 +- docs/contributing/critical-subsystems.md | 12 +- docs/contributing/workflow.md | 2 +- docs/explanation/daemon-experimental.md | 4 +- docs/explanation/daemon-lifecycle.md | 4 +- docs/explanation/design.md | 10 +- docs/explanation/host-prepare.md | 2 +- .../usb-security-key-architecture.md | 6 +- docs/reference/cgroup-delegation.md | 6 +- docs/reference/cli-contract.md | 4 +- docs/reference/components-graphics.md | 2 +- docs/reference/components-usb-security-key.md | 2 +- docs/reference/components-video.md | 2 +- docs/reference/daemon-api.md | 279 ++++---- docs/reference/desktop-wrapper.md | 2 +- docs/reference/doctor.md | 2 +- docs/reference/error-codes.md | 2 +- docs/reference/inet-d2b-chains.md | 6 +- docs/reference/kernel-module-check.md | 2 +- docs/reference/manifest-schema.md | 2 +- docs/reference/naming-conventions.md | 2 +- docs/reference/privileges.md | 25 +- docs/reference/provider-capability-matrix.md | 2 +- docs/reference/provider-managed-sandboxes.md | 8 +- docs/reference/realm-controller-config.md | 2 +- docs/reference/remote-full-host-nodes.md | 8 +- docs/reference/runtime-provider-selection.md | 2 +- docs/reference/store-sync.md | 14 +- docs/reference/tap-dag-contract.md | 4 +- docs/reference/tracing-contract.md | 4 +- docs/reference/wave6-foundation-manifest.json | 14 +- examples/graphics-workstation/README.md | 2 +- flake.nix | 4 +- labs/wlattach/deny.toml | 2 +- labs/wlattach/src/serve/sys.rs | 2 +- nix/prebuilt.json | 5 +- nix/prebuilt.nix | 27 +- nixos-modules/default.nix | 2 +- nixos-modules/guest-broker.nix | 112 ++++ nixos-modules/host-broker.nix | 39 +- nixos-modules/host-daemon.nix | 12 +- nixos-modules/host-polkit.nix | 6 +- nixos-modules/host.nix | 4 +- nixos-modules/index.nix | 4 +- nixos-modules/minijail-profiles.nix | 2 +- nixos-modules/options-site.nix | 8 +- nixos-modules/prebuilt-packages.nix | 15 +- nixos-modules/rust-host-tools.nix | 10 +- nixos-modules/vm-evaluator.nix | 9 +- nixos-modules/vm-guest-base.nix | 2 +- nixos-modules/vm-submodule.nix | 4 +- .../BUILD.bazel | 267 +++++--- .../Cargo.toml | 2 +- .../src/audit.rs | 2 +- .../src/bootstrap.rs | 0 .../src/fd_passing.rs | 0 .../src/lib.rs | 4 +- .../src/live_handlers.rs | 2 +- .../src/main.rs | 2 +- .../src/ops/audit_op.rs | 5 +- .../src/ops/cgroup.rs | 0 .../src/ops/cutover_artifacts.rs | 2 +- .../src/ops/device.rs | 0 .../src/ops/disk_init.rs | 0 .../src/ops/exec_reconcile.rs | 0 .../src/ops/gpu.rs | 0 .../src/ops/host_generation_handoff.rs | 10 +- .../src/ops/hosts.rs | 0 .../src/ops/media.rs | 0 .../src/ops/mod.rs | 0 .../src/ops/modprobe.rs | 0 .../src/ops/network.rs | 5 +- .../src/ops/nft.rs | 9 +- .../src/ops/nm.rs | 2 +- .../src/ops/pidfd.rs | 0 .../src/ops/route.rs | 0 .../src/ops/security_key.rs | 0 .../src/ops/spawn_runner.rs | 0 .../src/ops/state_dir.rs | 0 .../src/ops/storage_contract.rs | 4 +- .../src/ops/store_sync.rs | 0 .../src/ops/store_sync_audit.rs | 0 .../src/ops/store_sync_export.rs | 0 .../src/ops/store_verify.rs | 0 .../src/ops/store_view_farm.rs | 0 .../src/ops/store_view_posture.rs | 0 .../src/ops/swtpm_dir.rs | 0 .../src/ops/swtpm_migration.rs | 0 .../src/ops/sysctl.rs | 0 .../src/ops/systemd.rs | 10 +- .../src/ops/tap.rs | 4 +- .../src/ops/usbip_firewall.rs | 0 .../src/ops/usbip_host.rs | 0 .../src/ops/usbip_lock.rs | 0 .../src/ops/zone_store.rs | 4 +- .../src/protocol.rs | 0 .../src/runtime.rs | 607 ++++++++++++------ .../src/seccomp_compile_tests.rs | 0 .../src/sys.rs | 0 .../tests/bridge_lifecycle.rs | 2 +- .../tests/broker_export_audit.rs | 0 .../tests/broker_protocol_compatibility.rs | 0 .../tests/broker_socket_acl.rs | 0 .../tests/bundle_tampered_broker.rs | 4 +- .../tests/common/mod.rs | 24 +- packages/d2b-broker/tests/guest_profile.rs | 146 +++++ packages/d2b-broker/tests/host_profile.rs | 31 + .../tests/kernel_surface.rs | 2 +- .../tests/persistent_tap_lifecycle.rs | 2 +- .../tests/pidfd_handoff_scm_rights.rs | 12 +- .../tests/pidfd_real_spawner.rs | 2 +- .../d2b-broker/tests/profile_separation.rs | 144 +++++ .../tests/security_key_broker.rs | 4 +- .../tests/socket_activation.rs | 8 +- .../tests/w12_fd_passing_response.rs | 4 +- .../tests/w15_install_migrate.rs | 6 +- .../tests/zone_store.rs | 6 +- .../policy-doc-links/docs/README.md | 1 + .../policy-doc-links/escape.md | 1 + .../d2b-contract-tests/policy-doc-outside.md | 1 + .../tests/host_json_drift.rs | 2 +- .../tests/minijail_roles.rs | 2 +- .../tests/policy_bazel_layout.rs | 2 +- .../tests/policy_broker_dispositions.rs | 6 +- .../tests/policy_broker_schema.rs | 4 +- .../tests/policy_contracts.rs | 4 +- .../d2b-contract-tests/tests/policy_daemon.rs | 2 +- .../d2b-contract-tests/tests/policy_docs.rs | 8 +- .../d2b-contract-tests/tests/policy_guest.rs | 6 +- .../tests/policy_host_realm_relay.rs | 2 +- .../d2b-contract-tests/tests/policy_misc.rs | 6 +- .../tests/policy_modules.rs | 12 +- .../tests/policy_observability.rs | 6 +- .../tests/policy_production_closure.rs | 2 +- .../tests/policy_provider_crates.rs | 62 +- .../d2b-contract-tests/tests/policy_source.rs | 59 +- .../d2b-contract-tests/tests/policy_units.rs | 25 + .../d2b-contracts-broker/src/broker_wire.rs | 282 +++++++- packages/d2b-core/src/bundle_resolver.rs | 2 +- packages/d2b-host/Cargo.toml | 4 +- packages/d2b-host/src/devices.rs | 2 +- packages/d2b-host/src/ioctl_policy.rs | 2 +- packages/d2b-host/src/lib.rs | 6 +- packages/d2b-host/src/modules.rs | 2 +- packages/d2b-host/src/routes.rs | 2 +- packages/d2b-host/src/seccomp.rs | 2 +- .../src/wayland_proxy_argv.rs | 2 +- packages/d2b-provider-supervisor/BUILD.bazel | 8 +- .../d2b-provider-supervisor/src/broker.rs | 46 +- .../d2b-provider-supervisor/src/systemd.rs | 25 +- .../d2b-provider-system-minijail/src/lib.rs | 2 +- .../nix/storage-json.nix | 124 ++-- .../d2b-provider-volume-local/nix/store.nix | 4 +- .../nix/sync-json.nix | 12 +- packages/d2b/src/doctor.rs | 4 +- packages/d2b/src/host.rs | 2 +- packages/d2b/src/legacy.rs | 14 +- packages/d2bd-runtime/src/broker_transport.rs | 8 +- packages/d2bd-runtime/src/supervisor/state.rs | 4 +- packages/d2bd-runtime/src/typed_error.rs | 4 +- packages/d2bd/src/composition.rs | 2 +- packages/d2bd/src/security_key.rs | 2 +- .../broker-default-tests/policy/Cargo.lock | 54 +- .../broker-default-tests/policy/closure.json | 296 ++++----- .../broker-default-tests/policy/metadata.json | 6 +- .../production/Cargo.lock | 54 +- .../production/closure.json | 248 +++---- .../production/metadata.json | 6 +- .../policy/Cargo.lock | 54 +- .../policy/closure.json | 296 ++++----- .../policy/metadata.json | 6 +- .../production/Cargo.lock | 54 +- .../production/closure.json | 248 +++---- .../production/metadata.json | 6 +- .../policy/Cargo.lock | 54 +- .../policy/closure.json | 296 ++++----- .../policy/metadata.json | 6 +- .../production/Cargo.lock | 54 +- .../production/closure.json | 248 +++---- .../production/metadata.json | 6 +- .../broker-production/policy/Cargo.lock | 54 +- .../broker-production/policy/closure.json | 296 ++++----- .../broker-production/policy/metadata.json | 6 +- .../broker-production/production/Cargo.lock | 54 +- .../broker-production/production/closure.json | 248 +++---- .../production/metadata.json | 6 +- .../main-product/policy/closure.json | 2 +- .../main-product/policy/metadata.json | 2 +- .../main-product/production/closure.json | 2 +- .../main-product/production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-default-tests/policy/Cargo.lock | 54 +- .../broker-default-tests/policy/closure.json | 296 ++++----- .../broker-default-tests/policy/metadata.json | 6 +- .../production/Cargo.lock | 54 +- .../production/closure.json | 248 +++---- .../production/metadata.json | 6 +- .../policy/Cargo.lock | 54 +- .../policy/closure.json | 296 ++++----- .../policy/metadata.json | 6 +- .../production/Cargo.lock | 54 +- .../production/closure.json | 248 +++---- .../production/metadata.json | 6 +- .../policy/Cargo.lock | 54 +- .../policy/closure.json | 296 ++++----- .../policy/metadata.json | 6 +- .../production/Cargo.lock | 54 +- .../production/closure.json | 248 +++---- .../production/metadata.json | 6 +- .../broker-production/policy/Cargo.lock | 54 +- .../broker-production/policy/closure.json | 296 ++++----- .../broker-production/policy/metadata.json | 6 +- .../broker-production/production/Cargo.lock | 54 +- .../broker-production/production/closure.json | 248 +++---- .../production/metadata.json | 6 +- .../main-product/policy/closure.json | 2 +- .../main-product/policy/metadata.json | 2 +- .../main-product/production/closure.json | 2 +- .../main-product/production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- packages/xtask/src/process_marker_pin.rs | 2 +- packages/xtask/src/production_closure.rs | 8 +- packages/xtask/tests/policy_workspace.rs | 6 +- tests/fixtures/gen-w3-cli-goldens.py | 2 +- .../host-check-tap-creation-denied.json | 2 +- .../host-check-tap-creation-denied.txt | 2 +- .../host-install-dry-run-json.golden | 2 +- tests/golden/pinned/broker-export-audit.txt | 2 +- tests/golden/pinned/broker-scm-rights.txt | 2 +- tests/golden/pinned/broker-socket-acl.txt | 2 +- tests/golden/pinned/host-prepare-network.txt | 6 +- tests/golden/pinned/kernel-canaries.txt | 2 +- .../pinned/path-safety-violation-fs.txt | 2 +- .../pinned/process-marker-legacy-paths.json | 12 +- .../golden/pinned/usbip-firewall-skeleton.txt | 4 +- tests/host-integration/cutover-rehearsal.nix | 6 +- tests/host-integration/daemon-smoke.nix | 8 +- .../host-integration/host-realm-isolation.nix | 6 +- tests/host-integration/lib.nix | 4 +- tests/host-integration/privilege-oracle.nix | 14 +- .../resource-operator-activation.nix | 6 +- .../host-integration/unsafe-local-helper.nix | 4 +- tests/integration/containers/README.md | 2 +- tests/integration/containers/lib.sh | 2 +- tests/integration/live/live-vm-smoke.sh | 4 +- tests/migration-ledger.toml | 40 +- .../agents-md-rewrite-eval.toml | 2 +- .../broker-default-features-build.toml | 2 +- .../broker-export-audit.toml | 2 +- .../broker-scm-rights-fd-lifecycle.toml | 2 +- .../migration-state.d/broker-socket-acl.toml | 2 +- .../broker-systemd-unit-eval.toml | 2 +- .../broker-validate-bundle.toml | 2 +- .../cgroup-delegation-oracle.toml | 2 +- .../migration-state.d/d2bd-startup-smoke.toml | 2 +- .../migration-state.d/device-node-matrix.toml | 2 +- .../host-prepare-network.toml | 2 +- tests/migration-state.d/ioctl-negative.toml | 2 +- .../kernel-module-matrix.toml | 2 +- .../l1c-privilege-oracle.toml | 2 +- .../minijail-validator-wayland-proxy.toml | 2 +- tests/migration-state.d/nft-coexistence.toml | 2 +- .../otel-acl-migration-eval.toml | 2 +- .../path-safety-violation-fs.toml | 2 +- .../polkit-allowlist-eval.toml | 2 +- .../usbip-firewall-skeleton.toml | 2 +- tests/static.sh | 4 +- tests/tools/layer1-jobs.py | 6 +- tests/tools/tier0-first-pass.sh | 2 +- tests/unit/gates/BUILD.bazel | 4 +- tests/unit/gates/performance-budgets.sh | 2 +- tests/unit/nix/BUILD.bazel | 3 + tests/unit/nix/cases/broker-caps.nix | 4 +- .../unit/nix/cases/broker-service-posture.nix | 4 +- .../nix/cases/broker-socket-activation.nix | 12 +- tests/unit/nix/cases/d2bd-startup-smoke.nix | 8 +- tests/unit/nix/cases/daemon-autostart.nix | 4 +- tests/unit/nix/cases/guest-broker.nix | 156 +++++ tests/unit/nix/cases/prebuilt-broker.nix | 29 + tests/unit/nix/cases/realms.nix | 18 +- tests/unit/nix/surfaces/broker-profiles.nix | 10 + tests/unit/smoke/smoke-eval-tpm.nix | 2 +- 303 files changed, 5093 insertions(+), 3705 deletions(-) create mode 100644 changelog.d/adr046-u11-broker-profiles.md create mode 100644 nixos-modules/guest-broker.nix rename packages/{d2b-priv-broker => d2b-broker}/BUILD.bazel (64%) rename packages/{d2b-priv-broker => d2b-broker}/Cargo.toml (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/audit.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/bootstrap.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/fd_passing.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/lib.rs (95%) rename packages/{d2b-priv-broker => d2b-broker}/src/live_handlers.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/main.rs (95%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/audit_op.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/cgroup.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/cutover_artifacts.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/device.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/disk_init.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/exec_reconcile.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/gpu.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/host_generation_handoff.rs (98%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/hosts.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/media.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/mod.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/modprobe.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/network.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/nft.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/nm.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/pidfd.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/route.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/security_key.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/spawn_runner.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/state_dir.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/storage_contract.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/store_sync.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/store_sync_audit.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/store_sync_export.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/store_verify.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/store_view_farm.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/store_view_posture.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/swtpm_dir.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/swtpm_migration.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/sysctl.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/systemd.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/tap.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/usbip_firewall.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/usbip_host.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/usbip_lock.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/ops/zone_store.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/src/protocol.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/runtime.rs (97%) rename packages/{d2b-priv-broker => d2b-broker}/src/seccomp_compile_tests.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/src/sys.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/tests/bridge_lifecycle.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/tests/broker_export_audit.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/tests/broker_protocol_compatibility.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/tests/broker_socket_acl.rs (100%) rename packages/{d2b-priv-broker => d2b-broker}/tests/bundle_tampered_broker.rs (98%) rename packages/{d2b-priv-broker => d2b-broker}/tests/common/mod.rs (92%) create mode 100644 packages/d2b-broker/tests/guest_profile.rs create mode 100644 packages/d2b-broker/tests/host_profile.rs rename packages/{d2b-priv-broker => d2b-broker}/tests/kernel_surface.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/tests/persistent_tap_lifecycle.rs (99%) rename packages/{d2b-priv-broker => d2b-broker}/tests/pidfd_handoff_scm_rights.rs (89%) rename packages/{d2b-priv-broker => d2b-broker}/tests/pidfd_real_spawner.rs (98%) create mode 100644 packages/d2b-broker/tests/profile_separation.rs rename packages/{d2b-priv-broker => d2b-broker}/tests/security_key_broker.rs (97%) rename packages/{d2b-priv-broker => d2b-broker}/tests/socket_activation.rs (96%) rename packages/{d2b-priv-broker => d2b-broker}/tests/w12_fd_passing_response.rs (96%) rename packages/{d2b-priv-broker => d2b-broker}/tests/w15_install_migrate.rs (98%) rename packages/{d2b-priv-broker => d2b-broker}/tests/zone_store.rs (95%) create mode 120000 packages/d2b-contract-tests/policy-doc-links/docs/README.md create mode 120000 packages/d2b-contract-tests/policy-doc-links/escape.md create mode 100644 packages/d2b-contract-tests/policy-doc-outside.md create mode 100644 tests/unit/nix/cases/guest-broker.nix create mode 100644 tests/unit/nix/cases/prebuilt-broker.nix create mode 100644 tests/unit/nix/surfaces/broker-profiles.nix diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4ede0ce1fa..bc57096829 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,7 +3,7 @@ /Cargo.toml @vicondoa /Cargo.lock @vicondoa /packages/Cargo.guest.lock @vicondoa -/packages/d2b-priv-broker/Cargo.toml @vicondoa +/packages/d2b-broker/Cargo.toml @vicondoa /packages/d2b-guest-shell-runner/Cargo.toml @vicondoa /packages/policy-inputs/** @vicondoa /packages/policy-inputs/advisory-policy.json @vicondoa diff --git a/.github/workflows/release-host-binaries.yml b/.github/workflows/release-host-binaries.yml index 61183a9b69..3aeaea9b1d 100644 --- a/.github/workflows/release-host-binaries.yml +++ b/.github/workflows/release-host-binaries.yml @@ -105,7 +105,7 @@ jobs: packages/d2b-wayland-proxy/Cargo.toml \ packages/d2b-unsafe-local-helper/Cargo.toml \ packages/d2b-host/Cargo.toml \ - packages/d2b-priv-broker/Cargo.toml + packages/d2b-broker/Cargo.toml do package_version=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1) [ "$package_version" = "$VERSION" ] || { @@ -126,7 +126,7 @@ jobs: } manifest_version=$(jq -r 'if .version == null then "null" else (.version | strings) end' nix/prebuilt.json) - expected_keys='["d2b","d2b-activation-helper","d2b-priv-broker","d2b-unsafe-local-helper","d2b-wayland-proxy","d2bd"]' + expected_keys='["d2b","d2b-activation-helper","d2b-broker","d2b-unsafe-local-helper","d2b-wayland-proxy","d2bd"]' if [ "$manifest_version" = "null" ]; then jq -e '.version == null and .system == "x86_64-linux" and .binaries == {}' \ nix/prebuilt.json >/dev/null || { @@ -145,7 +145,7 @@ jobs: for binary in \ d2b \ d2b-activation-helper \ - d2b-priv-broker \ + d2b-broker \ d2b-unsafe-local-helper \ d2b-wayland-proxy \ d2bd @@ -246,7 +246,7 @@ jobs: packages/d2b-wayland-proxy/Cargo.toml \ packages/d2b-unsafe-local-helper/Cargo.toml \ packages/d2b-host/Cargo.toml \ - packages/d2b-priv-broker/Cargo.toml + packages/d2b-broker/Cargo.toml do package_version=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1) [ "$package_version" = "$VERSION" ] || { @@ -341,7 +341,7 @@ jobs: --package d2b-host --bin d2b-activation-helper rustup run "$PINNED" cargo build --release --locked \ --manifest-path Cargo.toml \ - --package d2b-priv-broker --bin d2b-priv-broker --no-default-features + --package d2b-broker --bin d2b-broker --no-default-features rm -rf dist mkdir -p dist/bin @@ -350,12 +350,12 @@ jobs: cp target/release/d2b-wayland-proxy dist/bin/ cp target/release/d2b-unsafe-local-helper dist/bin/ cp target/release/d2b-activation-helper dist/bin/ - cp target/release/d2b-priv-broker dist/bin/ + cp target/release/d2b-broker dist/bin/ for binary in \ d2b \ d2b-activation-helper \ - d2b-priv-broker \ + d2b-broker \ d2b-unsafe-local-helper \ d2b-wayland-proxy \ d2bd @@ -461,7 +461,7 @@ jobs: packages/d2b-wayland-proxy/Cargo.toml \ packages/d2b-unsafe-local-helper/Cargo.toml \ packages/d2b-host/Cargo.toml \ - packages/d2b-priv-broker/Cargo.toml + packages/d2b-broker/Cargo.toml do package_version=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1) [ "$package_version" = "$VERSION" ] || { @@ -473,7 +473,7 @@ jobs: flake_versions=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' flake.nix | sort -u) [ "$(printf '%s\n' "$flake_versions" | awk -v expected="$VERSION" '$0 != expected && NF { count++ } END { print count + 0 }')" -eq 0 ] - expected_keys='["d2b","d2b-activation-helper","d2b-priv-broker","d2b-unsafe-local-helper","d2b-wayland-proxy","d2bd"]' + expected_keys='["d2b","d2b-activation-helper","d2b-broker","d2b-unsafe-local-helper","d2b-wayland-proxy","d2bd"]' manifest_version=$(jq -r 'if .version == null then "null" else (.version | strings) end' nix/prebuilt.json) if [ "$manifest_version" = "null" ]; then jq -e '.version == null and .system == "x86_64-linux" and .binaries == {}' nix/prebuilt.json >/dev/null @@ -492,7 +492,7 @@ jobs: for binary in \ d2b \ d2b-activation-helper \ - d2b-priv-broker \ + d2b-broker \ d2b-unsafe-local-helper \ d2b-wayland-proxy \ d2bd @@ -612,7 +612,7 @@ jobs: asset_names=( "d2b-${VERSION}-x86_64-linux.tar.gz" "d2b-activation-helper-${VERSION}-x86_64-linux.tar.gz" - "d2b-priv-broker-${VERSION}-x86_64-linux.tar.gz" + "d2b-broker-${VERSION}-x86_64-linux.tar.gz" "d2b-unsafe-local-helper-${VERSION}-x86_64-linux.tar.gz" "d2b-wayland-proxy-${VERSION}-x86_64-linux.tar.gz" "d2bd-${VERSION}-x86_64-linux.tar.gz" @@ -703,7 +703,7 @@ jobs: case "$1" in "d2b-${VERSION}-x86_64-linux.tar.gz" \ | "d2b-activation-helper-${VERSION}-x86_64-linux.tar.gz" \ - | "d2b-priv-broker-${VERSION}-x86_64-linux.tar.gz" \ + | "d2b-broker-${VERSION}-x86_64-linux.tar.gz" \ | "d2b-unsafe-local-helper-${VERSION}-x86_64-linux.tar.gz" \ | "d2b-wayland-proxy-${VERSION}-x86_64-linux.tar.gz" \ | "d2bd-${VERSION}-x86_64-linux.tar.gz" \ diff --git a/AGENTS.md b/AGENTS.md index 652f84cac5..5a04ce1672 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ configuration, start with [`README.md`](README.md). d2b is an opinionated NixOS desktop microVM framework for a single trusted Wayland host and untrusted, isolated workloads. Its daemon-only control plane -is `d2bd` plus `d2b-priv-broker`; its declarative modules own networking, +is `d2bd` plus `d2b-broker`; its declarative modules own networking, per-VM store views, mediated devices, and versioned bundle contracts. Product direction is in [`STRATEGY.md`](./STRATEGY.md); the threat model is in [`docs/explanation/design.md`](./docs/explanation/design.md). Binding @@ -202,7 +202,7 @@ Read the relevant section before changing any of these: ## Daemon-only end-state (P6 onward) d2b declares exactly three root-visible units: `d2bd.service`, -`d2b-priv-broker.socket`, and `d2b-priv-broker.service`. The binding decision +`d2b-broker.socket`, and `d2b-broker.service`. The binding decision is [ADR 0015](./docs/adr/0015-daemon-only-clean-break.md). - `d2bd` supervises every per-VM DAG. The broker dispatches audited host diff --git a/BUILD.bazel b/BUILD.bazel index c431e9a0ac..4354f4b8b2 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -307,7 +307,7 @@ filegroup( "//packages/d2b-guestd:cargo_workspace_sources", "//packages/d2b-host:cargo_workspace_sources", "//packages/d2b-host-activation-helper:cargo_workspace_sources", - "//packages/d2b-priv-broker:cargo_workspace_sources", + "//packages/d2b-broker:cargo_workspace_sources", "//packages/d2b-process:cargo_workspace_sources", "//packages/d2b-process-conformance:cargo_workspace_sources", "//packages/d2b-provider:cargo_workspace_sources", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84d7fcb19f..9c74c03ab7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,7 +138,7 @@ When docs disagree with committed, passing code, the code wins. Update the docs ## Host-prepare gates Contributors touching anything in `packages/d2b-host/`, -`packages/d2b-priv-broker/src/ops/`, or the host-prepare +`packages/d2b-broker/src/ops/`, or the host-prepare docs (`docs/how-to/host-prepare.md`, `docs/how-to/host-prepare.d/*.md`, `docs/reference/{cgroup-delegation,inet-d2b-chains,privileges,support-matrix}.md`) diff --git a/Cargo.lock b/Cargo.lock index 0fa0f7edac..d5bb29173d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1016,6 +1016,32 @@ dependencies = [ "serde_json", ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + [[package]] name = "d2b-bus" version = "0.0.0-bootstrap" @@ -1324,32 +1350,6 @@ dependencies = [ "libc", ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - [[package]] name = "d2b-process" version = "0.0.0-bootstrap" diff --git a/Cargo.toml b/Cargo.toml index bd96103260..3c143719ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ members = [ "packages/d2b-contract-tests", "packages/d2b-cutover", "packages/xtask", - "packages/d2b-priv-broker", + "packages/d2b-broker", "packages/d2b-guest-shell-runner", ] @@ -139,7 +139,7 @@ schemars = { version = "0.8", features = ["derive"] } # so scope agents have everything they need to pull these crates in. rustix = { version = "0.38", features = ["fs", "process", "net", "pipe", "system", "mount", "thread", "pty", "termios", "stdio"] } # W3 s2: declared in workspace.dependencies so future scope work (and -# the eventual production netlink backend in d2b-priv-broker) can +# the eventual production netlink backend in d2b-broker) can # opt into rtnetlink via `{ workspace = true }`. Not yet consumed by # any workspace member, so the resolved Cargo.lock is unchanged. The # trait-based netlink backend in `d2b_host::netlink` lets the L1c diff --git a/Makefile b/Makefile index 9e25f13c48..d04471abee 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,7 @@ D2B_BAZEL_TEST_TAG_FILTERS ?= -manual,-gpu,-kvm D2B_BAZEL_MAIN_TARGETS = \ //packages/... \ //bazel/checks/rust/... \ - -//packages/d2b-priv-broker/... \ + -//packages/d2b-broker/... \ -//packages/d2b-guest-shell-runner/... \ -//packages/xtask:policy_ci \ -//packages/xtask:policy_workspace \ diff --git a/README.md b/README.md index bec75cb252..0a9695a85b 100644 --- a/README.md +++ b/README.md @@ -417,7 +417,7 @@ profile). Operators elevate with `sudo` inside the session. Run-state ships in `/var/lib/d2b/`; per-host config emitted by the NixOS module ships in `/etc/d2b/` (bundle + privileges + -processes JSON files consumed by `d2bd` / `d2b-priv-broker`). +processes JSON files consumed by `d2bd` / `d2b-broker`). For typed exit codes and JSON envelopes, see [`docs/reference/cli-contract.md`](docs/reference/cli-contract.md). diff --git a/SECURITY.md b/SECURITY.md index 51d60aabe0..a1d77bf8d9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -76,7 +76,7 @@ See [the unsafe-local provider contract](docs/reference/unsafe-local-provider.md ### Portability roadmap The portability work introduces a non-root `d2bd` daemon plus a -minimal root-owned `d2b-priv-broker` (see ADRs 0001-0008 under +minimal root-owned `d2b-broker` (see ADRs 0001-0008 under [`docs/adr/`](docs/adr/)). The new trust boundaries the daemon work will introduce are: @@ -164,7 +164,7 @@ The new trust-boundary statements are: NetworkManager unmanaged config, and `modprobe` state on behalf of `d2bd`, gated entirely by the closed broker enum plus the trusted bundle. Every operation has a typed handler under - `packages/d2b-priv-broker/src/ops/` and re-derives its + `packages/d2b-broker/src/ops/` and re-derives its operating paths from the bundle, never from caller input. - Compromise of `d2bd` cannot escalate to arbitrary host mutation beyond the declared broker enum variants. Unknown diff --git a/STRATEGY.md b/STRATEGY.md index 7d28ef5544..129619e071 100644 --- a/STRATEGY.md +++ b/STRATEGY.md @@ -18,7 +18,7 @@ applications remain integrated with the host desktop. ## Ownership and architecture d2b owns its microVM substrate end to end. The daemon-only control plane is -`d2bd` plus `d2b-priv-broker`: the daemon supervises lifecycle DAGs and the +`d2bd` plus `d2b-broker`: the daemon supervises lifecycle DAGs and the privileged broker performs audited host mutations. The framework declares no per-VM systemd templates and keeps runner processes behind typed broker operations. diff --git a/TODO.md b/TODO.md index 9854d6ddc5..961dfd2bb1 100644 --- a/TODO.md +++ b/TODO.md @@ -335,7 +335,7 @@ alongside the per-op privileges matrix enforcement. ## Per-op privileges-matrix enforcement at broker (Phase 6 security-hardening) -`packages/d2b-priv-broker/src/runtime.rs` documents the Phase A +`packages/d2b-broker/src/runtime.rs` documents the Phase A runner-control trust model above the `SignalRunner` and `DeregisterRunnerPidfd` handlers: ADR 0015 treats `d2bd` as part of the daemon-only TCB, `envelope.caller_role` is audit-only at the @@ -418,7 +418,7 @@ All in comments / doc-strings, e.g. `packages/d2b-host/src/virtiofsd_argv.rs`, `packages/d2b-host/src/swtpm_argv.rs`, `packages/d2b-host/src/gpu_argv.rs`, -`packages/d2b-priv-broker/src/ops/spawn_runner.rs`, +`packages/d2b-broker/src/ops/spawn_runner.rs`, `packages/d2bd/src/pidfs_probe.rs`, `packages/d2bd/src/ch_stats.rs`, `packages/d2b-core/src/bundle_resolver.rs` (e.g. line 2265 @@ -577,7 +577,7 @@ inconsistency). The supervisor decision logic needs to either: **Files to start from.** -- `packages/d2b-priv-broker/src/sys.rs` - the reaper claims to +- `packages/d2b-broker/src/sys.rs` - the reaper claims to use `waitid(P_PIDFD)` + signalfd; verify it runs. - `packages/d2bd/src/supervisor/pidfd.rs` - `PidfdTable::snapshot` writes the file; check where entries are @@ -664,7 +664,7 @@ operator's login was a hard prerequisite for invoking the CLI. **Files / code paths to start from.** -- `packages/d2b-priv-broker/src/live_handlers.rs` - `SpawnRunner` +- `packages/d2b-broker/src/live_handlers.rs` - `SpawnRunner` handler; the audio policy ref is `w1-audio`. - `packages/d2bd/src/supervisor/pidfd.rs` - pidfd lifecycle; this is where the respawn-on-death watchdog needs to land. diff --git a/bazel/checks/nix/BUILD.bazel b/bazel/checks/nix/BUILD.bazel index bb4afc94ba..75f7787d76 100644 --- a/bazel/checks/nix/BUILD.bazel +++ b/bazel/checks/nix/BUILD.bazel @@ -16,6 +16,17 @@ _SURFACES = { "nixos-modules/processes-json.nix", ], }, + "broker-profiles": { + "cases": ["guest-broker", "prebuilt-broker"], + "files": [ + "nix/prebuilt.json", + "nix/prebuilt.nix", + "nixos-modules/guest-broker.nix", + "nixos-modules/host-broker.nix", + "nixos-modules/prebuilt-packages.nix", + ], + "modules": [], + }, "guest-control": { "cases": [], "files": [ diff --git a/bazel/checks/rust/BUILD.bazel b/bazel/checks/rust/BUILD.bazel index ea0fb751d5..509e8d0b3d 100644 --- a/bazel/checks/rust/BUILD.bazel +++ b/bazel/checks/rust/BUILD.bazel @@ -3,24 +3,27 @@ package(default_visibility = ["//visibility:public"]) test_suite( name = "portable_rust_broker", tests = [ - "//packages/d2b-priv-broker:d2b_priv_broker_doc_test", - "//packages/d2b-priv-broker:bridge_lifecycle", - "//packages/d2b-priv-broker:broker_export_audit", - "//packages/d2b-priv-broker:broker_protocol_compatibility", - "//packages/d2b-priv-broker:broker_socket_acl", - "//packages/d2b-priv-broker:bundle_tampered_broker", - "//packages/d2b-priv-broker:d2b_priv_broker_fake_backends_test", - "//packages/d2b-priv-broker:d2b_priv_broker_layer1_bootstrap_test", - "//packages/d2b-priv-broker:d2b_priv_broker_test", - "//packages/d2b-priv-broker:kernel_surface", - "//packages/d2b-priv-broker:persistent_tap_lifecycle", - "//packages/d2b-priv-broker:pidfd_handoff_scm_rights", - "//packages/d2b-priv-broker:pidfd_real_spawner", - "//packages/d2b-priv-broker:security_key_broker", - "//packages/d2b-priv-broker:socket_activation", - "//packages/d2b-priv-broker:w12_fd_passing_response", - "//packages/d2b-priv-broker:w15_install_migrate", - "//packages/d2b-priv-broker:zone_store", + "//packages/d2b-broker:d2b_broker_doc_test", + "//packages/d2b-broker:bridge_lifecycle", + "//packages/d2b-broker:broker_export_audit", + "//packages/d2b-broker:broker_protocol_compatibility", + "//packages/d2b-broker:broker_socket_acl", + "//packages/d2b-broker:bundle_tampered_broker", + "//packages/d2b-broker:guest_profile", + "//packages/d2b-broker:host_profile", + "//packages/d2b-broker:d2b_broker_fake_backends_test", + "//packages/d2b-broker:d2b_broker_layer1_bootstrap_test", + "//packages/d2b-broker:d2b_broker_test", + "//packages/d2b-broker:kernel_surface", + "//packages/d2b-broker:persistent_tap_lifecycle", + "//packages/d2b-broker:pidfd_handoff_scm_rights", + "//packages/d2b-broker:pidfd_real_spawner", + "//packages/d2b-broker:profile_separation", + "//packages/d2b-broker:security_key_broker", + "//packages/d2b-broker:socket_activation", + "//packages/d2b-broker:w12_fd_passing_response", + "//packages/d2b-broker:w15_install_migrate", + "//packages/d2b-broker:zone_store", ], ) diff --git a/changelog.d/adr046-u11-broker-profiles.md b/changelog.d/adr046-u11-broker-profiles.md new file mode 100644 index 0000000000..fd0d18b4a9 --- /dev/null +++ b/changelog.d/adr046-u11-broker-profiles.md @@ -0,0 +1,3 @@ +- Rename the privileged broker package and executable to `d2b-broker` and add + fixed Host and Guest profiles with separate socket, state, audit, caller, + and authority bindings. diff --git a/docs/contributing/architecture.md b/docs/contributing/architecture.md index e357ef3611..5137a119f0 100644 --- a/docs/contributing/architecture.md +++ b/docs/contributing/architecture.md @@ -18,8 +18,8 @@ broker's `SpawnRunner` / `OpenPidfd` ops. | Resource | Pattern | | --------------------------------------- | -------------------------------------- | | Public daemon (supervisor) | `d2bd.service` | -| Privileged broker socket | `d2b-priv-broker.socket` | -| Privileged broker service | `d2b-priv-broker.service` | +| Privileged broker socket | `d2b-broker.socket` | +| Privileged broker service | `d2b-broker.service` | | Lifecycle permission group | `d2b` (singleton) | VM names are validated at eval time: @@ -110,7 +110,7 @@ per-VM units are emitted): descendants; the restarted daemon re-adopts existing runners. The existing guarded `ExecStop` host-shutdown hook remains the all-VM teardown path and runs only when the system manager is stopping. -- `d2b-priv-broker.service` is socket-activated. It reloads the +- `d2b-broker.service` is socket-activated. It reloads the current bundle resolver for each accepted request so a running broker does not dispatch stale runner intents after a switch, and it never holds in-flight session state across requests. @@ -125,7 +125,7 @@ and the exact remediation command (`d2b vm restart ` for clean down+up, New per-VM work belongs **inside the daemon's DAG executor** (`packages/d2bd-runtime/src/supervisor/`), with privileged effects routed through a -typed `d2b-priv-broker` op declared in `packages/d2b-contracts/` and audited in +typed `d2b-broker` op declared in `packages/d2b-contracts/` and audited in `/var/lib/d2b/audit/broker-.jsonl`. Do not introduce a `systemd.services.*` declaration in `nixos-modules/` for per-VM work. Denylist coverage lives in diff --git a/docs/contributing/critical-subsystems.md b/docs/contributing/critical-subsystems.md index 06a9c12ce8..d0c122157d 100644 --- a/docs/contributing/critical-subsystems.md +++ b/docs/contributing/critical-subsystems.md @@ -35,9 +35,9 @@ daemon owns sync; there is no per-VM `store-sync` unit. ## TPM persistence (per-VM swtpm) -**Where:** `/var/lib/d2b/vms//swtpm/`; spawned via broker `SpawnRunner` from `packages/d2b-provider-device-tpm/src/swtpm_argv.rs` and supervised by `d2bd` as a child of the VM's DAG. The broker **provisions + hardens** this dir on first start (`packages/d2b-priv-broker/src/ops/swtpm_dir.rs`, gated on `seccomp_policy_ref == "w1-swtpm"`): fd-safe create (owner `d2b--swtpm`, mode 0700, inherited ACLs cleared), reconcile-in-place on a correct-owner existing dir, fail-closed on owner/type/symlink mismatch, ancestor `--x` traverse ACL, stale `tpm.sock` unlink - emitting the path-free `PrepareSwtpmDir` audit op. +**Where:** `/var/lib/d2b/vms//swtpm/`; spawned via broker `SpawnRunner` from `packages/d2b-provider-device-tpm/src/swtpm_argv.rs` and supervised by `d2bd` as a child of the VM's DAG. The broker **provisions + hardens** this dir on first start (`packages/d2b-broker/src/ops/swtpm_dir.rs`, gated on `seccomp_policy_ref == "w1-swtpm"`): fd-safe create (owner `d2b--swtpm`, mode 0700, inherited ACLs cleared), reconcile-in-place on a correct-owner existing dir, fail-closed on owner/type/symlink mismatch, ancestor `--x` traverse ACL, stale `tpm.sock` unlink - emitting the path-free `PrepareSwtpmDir` audit op. -Holds the per-VM TPM 2.0 NVRAM + EK seed. **Wiping it looks like device tampering to any IdP** (Entra ID, Intune, Bitlocker-style policies) and forces re-enrollment. Never zero it casually. The per-VM state root is `3770` (setgid **+ sticky**) so a non-owner role UID cannot rename/replace the `swtpm/` entry; an identity-bound, root-owned marker at `/var/lib/d2b/swtpm-markers/` makes a *previously-provisioned-then-missing/replaced* dir **fail the VM start closed** (`previously-provisioned-swtpm-state-missing`) rather than silently re-creating an empty TPM. The state directory's ACLs are asserted by `tests/unit/smoke/smoke-eval-tpm.nix`; the broker hardening by `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` tests. +Holds the per-VM TPM 2.0 NVRAM + EK seed. **Wiping it looks like device tampering to any IdP** (Entra ID, Intune, Bitlocker-style policies) and forces re-enrollment. Never zero it casually. The per-VM state root is `3770` (setgid **+ sticky**) so a non-owner role UID cannot rename/replace the `swtpm/` entry; an identity-bound, root-owned marker at `/var/lib/d2b/swtpm-markers/` makes a *previously-provisioned-then-missing/replaced* dir **fail the VM start closed** (`previously-provisioned-swtpm-state-missing`) rather than silently re-creating an empty TPM. The state directory's ACLs are asserted by `tests/unit/smoke/smoke-eval-tpm.nix`; the broker hardening by `packages/d2b-broker/src/ops/swtpm_dir.rs` tests. ## USBIP passthrough @@ -157,11 +157,11 @@ Version-pinned via `manifestVersion`. Adding, removing, or renaming a per-VM fie Sensitive bundle artifacts install at `root:d2bd` 0640 and ground every broker/sandbox/runner behaviour. `d2b-core` DTOs are canonical; `d2b._bundle` is the typed internal artifact table that owns JSON data, install names, classifications, and `/etc/d2b` materialization for every bundle artifact. Add new bundle artifacts through `nixos-modules/bundle-artifacts.nix` instead of hand-writing parallel install logic in each emitter. Committed schemas under `docs/reference/schemas/v2/` ARE the contract and `//packages/xtask:gen_schemas_drift` enforces their owner-local generator through `make test-drift`. Breaking the schema without an intentional `bundleVersion`/`schemaVersion` bump silently breaks every downstream consumer. -## Control plane - `d2bd` + `d2b-priv-broker` +## Control plane - `d2bd` + `d2b-broker` -**Where:** `packages/d2b-contracts/**` + `packages/d2b-core/**` + `packages/d2bd/**` + `packages/d2b-priv-broker/**` (sibling workspace; `unsafe_code = "deny"` with quarantined `src/sys.rs` for fd-passing FFI) + `packages/d2b/**` + `docs/reference/{cli-contract,daemon-api,error-codes,privileges}.md` + the daemon Layer-1 gate set in `tests/static.sh` +**Where:** `packages/d2b-contracts/**` + `packages/d2b-core/**` + `packages/d2bd/**` + `packages/d2b-broker/**` (sibling workspace; `unsafe_code = "deny"` with quarantined `src/sys.rs` for fd-passing FFI) + `packages/d2b/**` + `docs/reference/{cli-contract,daemon-api,error-codes,privileges}.md` + the daemon Layer-1 gate set in `tests/static.sh` -The **only** persistent root surfaces the framework declares. `d2b-priv-broker.socket` is socket-activated: systemd creates/binds/listens/sets-ACL before the broker starts; the broker adopts fd 3 via `SD_LISTEN_FDS` and MUST NOT self-bind, self-fchmod, or self-fchown when `SD_LISTEN_FDS=1`. `d2bd.service` carries `Wants=d2b-priv-broker.socket` (not `Requires=`) so the daemon keeps serving while the broker is idle. The broker reloads the current bundle resolver per accepted request so it does not dispatch stale runner intents after a switch. The broker drops to the `d2bd` group and uses `SO_PEERCRED` at accept time for authz (launcher / admin / deny). Every host mutation flows through a typed broker op (cgroup v2 delegation, TAP/bridge lifecycle, `ApplyNftables`, `ApplyNmUnmanaged`, `ApplySysctl`, `UpdateHostsFile`, `ModprobeIfAllowed`, `UsbipBindFirewallRule`, `SpawnRunner`, `OpenPidfd`) and is recorded as an `OpAuditRecord` in `/var/lib/d2b/audit/broker-.jsonl` (root-owned `0640 root:d2bd`, append-only `O_APPEND`, daily rotation, 14-day default retention overridable via `d2b.site.audit.retentionDays`). Relevant enforcing coverage includes `tests/unit/nix/cases/broker-socket-activation.nix`, `tests/unit/nix/cases/broker-caps.nix`, and daemon startup integration tests under `packages/d2bd/tests/`. The legacy-unit policy lives in `packages/d2b-contract-tests/tests/policy_units.rs` and runs in the enforcing fixture-contract lane. See [ADR 0015](../adr/0015-daemon-only-clean-break.md). +The **only** persistent root surfaces the framework declares. `d2b-broker.socket` is socket-activated: systemd creates/binds/listens/sets-ACL before the broker starts; the broker adopts fd 3 via `SD_LISTEN_FDS` and MUST NOT self-bind, self-fchmod, or self-fchown when `SD_LISTEN_FDS=1`. `d2bd.service` carries `Wants=d2b-broker.socket` (not `Requires=`) so the daemon keeps serving while the broker is idle. The broker reloads the current bundle resolver per accepted request so it does not dispatch stale runner intents after a switch. The broker drops to the `d2bd` group and uses `SO_PEERCRED` at accept time for authz (launcher / admin / deny). Every host mutation flows through a typed broker op (cgroup v2 delegation, TAP/bridge lifecycle, `ApplyNftables`, `ApplyNmUnmanaged`, `ApplySysctl`, `UpdateHostsFile`, `ModprobeIfAllowed`, `UsbipBindFirewallRule`, `SpawnRunner`, `OpenPidfd`) and is recorded as an `OpAuditRecord` in `/var/lib/d2b/audit/broker-.jsonl` (root-owned `0640 root:d2bd`, append-only `O_APPEND`, daily rotation, 14-day default retention overridable via `d2b.site.audit.retentionDays`). Relevant enforcing coverage includes `tests/unit/nix/cases/broker-socket-activation.nix`, `tests/unit/nix/cases/broker-caps.nix`, and daemon startup integration tests under `packages/d2bd/tests/`. The legacy-unit policy lives in `packages/d2b-contract-tests/tests/policy_units.rs` and runs in the enforcing fixture-contract lane. See [ADR 0015](../adr/0015-daemon-only-clean-break.md). ## Storage lifecycle / restart / synchronization @@ -201,7 +201,7 @@ The framework owns `${cfg.site.keysDir}/_ed25519`. `d2b keys rotate` MUST NO ## virtiofsd sandbox model -**Where:** `nixos-modules/minijail-profiles.nix` (virtiofsdProfiles), `packages/d2b-priv-broker/src/sys.rs` (`clone3_spawn_runner` user-NS path), `nixos-modules/processes-json.nix` (argv emit) +**Where:** `nixos-modules/minijail-profiles.nix` (virtiofsdProfiles), `packages/d2b-broker/src/sys.rs` (`clone3_spawn_runner` user-NS path), `nixos-modules/processes-json.nix` (argv emit) virtiofsd profiles MUST declare zero host capabilities (`capabilities = []`), `requiresStartRoot = false`, and a `userNamespace` block mapping in-NS UID/GID 0 to the per-share principal. Normal VM shares map to `d2b--runner`; the guest-control token share (`d2b-gctl`) maps to the narrower `d2b--gctlfs` principal. The broker pre-establishes the user namespace via `clone3(CLONE_NEWUSER)` + `pipe2` sync + `/proc//uid_map` writes BEFORE virtiofsd's first instruction runs. virtiofsd argv MUST include `--sandbox=chroot --inode-file-handles=never` and `--readonly` for every `readOnly` share (`ro-store`, `d2b-gctl`). Reintroducing host caps, `requiresStartRoot=true`, or `--sandbox=namespace` violates [ADR 0021](../adr/0021-broker-user-namespace-for-virtiofsd.md). Rendered profile and argv coverage lives in `packages/d2b-contract-tests/tests/minijail_roles.rs` and runs in the enforcing fixture-contract lane. diff --git a/docs/contributing/workflow.md b/docs/contributing/workflow.md index 1c5d70918d..303bd4b248 100644 --- a/docs/contributing/workflow.md +++ b/docs/contributing/workflow.md @@ -250,7 +250,7 @@ sessions. Its focused operating detail is worktrees comes from `sccache` (`$SCCACHE_DIR`, default `~/.cache/d2b-sccache`), wired by the `[build] rustc-wrapper` lines in `.cargo/config.toml` and the sibling-workspace configs under - `packages/d2b-priv-broker/`, `packages/d2b-guest-shell-runner/`, and + `packages/d2b-broker/`, `packages/d2b-guest-shell-runner/`, and `packages/d2b-core/fuzz/`. A shared target dir is deliberately avoided: cargo's target-dir lock is workspace-wide, so two worktrees building concurrently at different SHAs would serialize pessimistically diff --git a/docs/explanation/daemon-experimental.md b/docs/explanation/daemon-experimental.md index 6938efb12e..c6c36b2241 100644 --- a/docs/explanation/daemon-experimental.md +++ b/docs/explanation/daemon-experimental.md @@ -3,7 +3,7 @@ ## v1.0 (ADR 0015): on by default In v1.0 the daemon-only end-state landed: every mutating verb dispatches -through `d2bd` → `d2b-priv-broker`, and the historical bash CLI +through `d2bd` → `d2b-broker`, and the historical bash CLI (`nixos-modules/cli.nix`) and the three-mode bridge (`default daemon-first` / `D2B_NATIVE_ONLY=1` / `D2B_LEGACY_BASH_OPT_IN=1`) were retired in v1.0. @@ -18,7 +18,7 @@ Enabling it adds the v1.0 daemon surface to the host: - the `d2bd` system user/group; - the `d2bd.service` unit plus the public/private sockets; -- the `d2b-priv-broker.{service,socket}` units (socket-activated; +- the `d2b-broker.{service,socket}` units (socket-activated; see ADR 0015); - the Rust CLI + manpages/completions; - `/etc/d2b/{bundle,host,processes,privileges}.json` emitted at diff --git a/docs/explanation/daemon-lifecycle.md b/docs/explanation/daemon-lifecycle.md index b0ff70c168..4e7c07048e 100644 --- a/docs/explanation/daemon-lifecycle.md +++ b/docs/explanation/daemon-lifecycle.md @@ -155,8 +155,8 @@ primary VMM stop/cleanup decision. ## Host shutdown and reboot integration NixOS still declares only the three ADR-0015 root-visible units: -`d2bd.service`, `d2b-priv-broker.socket`, and -`d2b-priv-broker.service`. There is no per-VM or extra guest-shutdown +`d2bd.service`, `d2b-broker.socket`, and +`d2b-broker.service`. There is no per-VM or extra guest-shutdown systemd unit. Instead, `d2bd.service` has an `ExecStop` hook that first checks the systemd manager state with absolute systemd helper paths. It runs the all-VM shutdown hook only when the system manager is stopping for host diff --git a/docs/explanation/design.md b/docs/explanation/design.md index 17126debfc..e60eae3d76 100644 --- a/docs/explanation/design.md +++ b/docs/explanation/design.md @@ -475,7 +475,7 @@ Everything else is derived. > `nixos-modules/host-wrapper.nix` were deleted in v1.0. The section > below documents the legacy architecture for historical context; > in v1.0 the per-VM lifecycle is fully owned by `d2bd`'s -> supervisor DAG dispatched through `d2b-priv-broker`'s +> supervisor DAG dispatched through `d2b-broker`'s > `SpawnRunner` / `SignalRunner` ops, and runner lifecycle-of-record > is the broker-registered pidfd table. See > [§ Launcher authorisation](#launcher-authorisation-v10-so_peercred--d2b-group) @@ -527,7 +527,7 @@ audit and backup scripts can reason about. ### Per-VM sidecars > **v1.0 status (ADR 0015):** in v1.0 daemon-only, the per-VM sidecars -> listed below are spawned by `d2b-priv-broker` via the supervisor +> listed below are spawned by `d2b-broker` via the supervisor > DAG (`SpawnRunner` requests) and registered in the pidfd table for > lifecycle ownership. Historically, they were emitted as > independent systemd units under `nixos-modules/host-sidecars.nix`; in @@ -737,7 +737,7 @@ is the daily-driver interface. Verbs include `list`, `status`, `keys`, `host`, and `audit`. The CLI is the Rust binary, full stop: the pre-v1.0 bash CLI (and the generated `nixos-modules/cli.nix` shell script) was retired in v1.0. Host mutations dispatch through -`d2bd` → `d2b-priv-broker`; live guest activation dispatches +`d2bd` → `d2b-broker`; live guest activation dispatches through `d2bd` → authenticated guest-control and only uses the broker to publish/commit host-owned generation metadata (see [ADR 0015](../adr/0015-daemon-only-clean-break.md) and @@ -841,7 +841,7 @@ patterns, see [the naming conventions reference](../reference/naming-conventions | VM name (`d2b.vms.`) | `^[a-z][a-z0-9-]*$`, ≤ ... no `sys-` prefix, not `launcher` | assertions.nix | | Env name (`d2b.envs.`) | `^[a-z][a-z0-9-]*$`, length ≤ 8 (IFNAMSIZ-1=15 minus `br--lan` = 7) | network.nix | | `d2bd.service` | non-root daemon (v1.0; per ADR 0015 the only persistent user-facing d2b unit besides the broker) | host-daemon.nix | -| `d2b-priv-broker.{socket,service}` | socket-activated privileged broker (v1.0) | host-broker.nix | +| `d2b-broker.{socket,service}` | socket-activated privileged broker (v1.0) | host-broker.nix | | `microvm@.service` | upstream microvm.nix unit (still emitted for direct-debug bypass; not the lifecycle-of-record in v1.0) | microvm.nix | | `d2b.slice//` | per-VM broker-spawned runner leaves (v1.0 replaces legacy `d2b--{gpu,video,snd,swtpm,store-sync,usbip}.service`) | broker SpawnRunner | | `d2b.slice/sys-/usbipd-{backend,proxy}` | per-env USBIP runner leaves (v1.0 replaces legacy `d2b-sys--usbipd-{backend,proxy}.service`) | broker SpawnRunner | @@ -949,7 +949,7 @@ uid/gid, refuses requests from peers outside the `d2b` group / mutating verbs to the admin role (`d2b.site.adminUsers`, the *role* gate). Per-verb authorisation lives in the daemon's `dispatch_request` table. Most host-mutating verbs route through -`d2b-priv-broker`, where each host mutation is recorded as an +`d2b-broker`, where each host mutation is recorded as an audited `OpAuditRecord` in `broker-.jsonl`. The guest-control verbs are the exception: `readGuestConfig` (config sync) reads the guest's config over the typed guest-control diff --git a/docs/explanation/host-prepare.md b/docs/explanation/host-prepare.md index 7d96973a58..3ff25a4aec 100644 --- a/docs/explanation/host-prepare.md +++ b/docs/explanation/host-prepare.md @@ -40,7 +40,7 @@ prepare` can answer four questions before touching the command: `d2bd` runs as an unprivileged system user. It performs no privileged mutation itself; instead, every mutating step is forwarded -to `d2b-priv-broker` over a `0600` private Unix socket +to `d2b-broker` over a `0600` private Unix socket (`priv.sock`). The broker: - only ever speaks a **closed enum** of typed operations (see diff --git a/docs/explanation/usb-security-key-architecture.md b/docs/explanation/usb-security-key-architecture.md index 0a2a15d6ed..ab1144722d 100644 --- a/docs/explanation/usb-security-key-architecture.md +++ b/docs/explanation/usb-security-key-architecture.md @@ -103,7 +103,7 @@ d2bd (host daemon) - lease serializer │ queues, timeouts, cancellation, audit log ▼ broker op UsbSecurityKeyOpenDevice → SCM_RIGHTS fd - │ d2b-priv-broker resolves stable selector → opens /dev/hidrawN + │ d2b-broker resolves stable selector → opens /dev/hidrawN ▼ physical YubiKey (host hidraw node) │ CTAP HID reports, user touch, responses @@ -150,7 +150,7 @@ physical key: ### Privileged device access Opening `/dev/hidrawN` for a FIDO-class device requires elevated privilege. The -broker dispatches a `UsbSecurityKeyOpenDevice` operation to `d2b-priv-broker`, +broker dispatches a `UsbSecurityKeyOpenDevice` operation to `d2b-broker`, which: 1. Resolves the configured stable selector to a `/dev/hidrawN` path. @@ -159,7 +159,7 @@ which: 3. Opens the path and returns the file descriptor to `d2bd` via `SCM_RIGHTS`. 4. Logs the operation to the d2b audit log. -`d2b-priv-broker` does not hold the file descriptor after the transfer; it is +`d2b-broker` does not hold the file descriptor after the transfer; it is owned exclusively by `d2bd` for the lifetime of the active ceremony. --- diff --git a/docs/reference/cgroup-delegation.md b/docs/reference/cgroup-delegation.md index 5244602915..147f3d89a8 100644 --- a/docs/reference/cgroup-delegation.md +++ b/docs/reference/cgroup-delegation.md @@ -52,7 +52,7 @@ split into two categories: The host systemd posture is part of that contract: `d2b.slice` is delegated by systemd with the `cpu`, `memory`, `pids`, `io`, and `cpuset` controllers, and -`d2b-priv-broker.service` runs inside that slice with +`d2b-broker.service` runs inside that slice with `Delegate=true` and `KillMode=process`. Broker restarts therefore stop only the broker process; runner teardown stays on the audited pidfd/`CgroupKill` path instead of a service-wide systemd kill. @@ -158,7 +158,7 @@ cpuset enable is the only one with a preceding inheritance step. ## Fd ownership table The daemon owns one `Arc` per registered pidfd in -[`d2b_priv_broker` → +[`d2b_broker` → `d2bd_runtime::supervisor::pidfd::PidfdTable`](../../packages/d2bd-runtime/src/supervisor/pidfd.rs). | Layer | What it holds | Lifetime | @@ -195,7 +195,7 @@ path-safety contract: never from caller input. The canary matrices in `packages/d2b-host/src/cgroup.rs` and -`packages/d2b-priv-broker/src/ops/cgroup.rs` exercise the refusal paths. Both +`packages/d2b-broker/src/ops/cgroup.rs` exercise the refusal paths. Both are included in `test-rust`. ## Audit records diff --git a/docs/reference/cli-contract.md b/docs/reference/cli-contract.md index aff78a41b9..d97be46b8a 100644 --- a/docs/reference/cli-contract.md +++ b/docs/reference/cli-contract.md @@ -2501,7 +2501,7 @@ cutover-only cleanup candidates, and fail-closed hazards. `--apply` and "preflightRequirements": [ "all d2b VMs stopped", "d2bd.service stopped", - "d2b-priv-broker.service stopped", + "d2b-broker.service stopped", "net VMs stopped; guest routing, TAP connectivity, and dependent bridge traffic will be interrupted" ], "preserve": [ @@ -2538,7 +2538,7 @@ rollback command: d2b host migrate-storage --rollback --from-checkpoint storage- preflight requirements: - all d2b VMs stopped - d2bd.service stopped - - d2b-priv-broker.service stopped + - d2b-broker.service stopped - net VMs stopped; guest routing, TAP connectivity, and dependent bridge traffic will be interrupted ``` diff --git a/docs/reference/components-graphics.md b/docs/reference/components-graphics.md index 63c983a4e0..7119580eb5 100644 --- a/docs/reference/components-graphics.md +++ b/docs/reference/components-graphics.md @@ -23,7 +23,7 @@ When `graphics.crossDomainTrusted = true` and the guest socket, while the host-side `d2b-wayland-proxy` runs as a broker-spawned `wayland-proxy` role and mediates access to the real host compositor. `d2bd` supervises the daemon-owned process DAG and asks -`d2b-priv-broker` to spawn the wayland proxy, GPU sidecar +`d2b-broker` to spawn the wayland proxy, GPU sidecar (`crosvm device gpu`), and cloud-hypervisor runner as pidfd-tracked runners. The GPU sidecar runs as the dedicated per-VM `d2b--gpu` system user, not as the operator's Wayland user. diff --git a/docs/reference/components-usb-security-key.md b/docs/reference/components-usb-security-key.md index a346a0fbe7..7a33f0175a 100644 --- a/docs/reference/components-usb-security-key.md +++ b/docs/reference/components-usb-security-key.md @@ -96,7 +96,7 @@ stable label, USB vendor/product IDs, and an optional serial: The host broker owns the following runtime resources. They are **not** declared as NixOS systemd services or static units; they are created and torn down by -the `d2bd`/`d2b-priv-broker` pipeline at runtime: +the `d2bd`/`d2b-broker` pipeline at runtime: | Resource | Owner | Path / name | |----------|-------|-------------| diff --git a/docs/reference/components-video.md b/docs/reference/components-video.md index 84bea8a2e8..3c21b7b188 100644 --- a/docs/reference/components-video.md +++ b/docs/reference/components-video.md @@ -54,7 +54,7 @@ Video decode is distinct from display streaming and GPU acceleration; see ## Host-side resources When enabled, the daemon-owned process DAG adds a `video` node. There is no -per-VM systemd unit; `d2bd` asks `d2b-priv-broker` to spawn the runner +per-VM systemd unit; `d2bd` asks `d2b-broker` to spawn the runner with `SpawnRunner { role: Video }` and tracks it by pidfd. | Resource | Shape | diff --git a/docs/reference/daemon-api.md b/docs/reference/daemon-api.md index 633cac371f..04ab8a931b 100644 --- a/docs/reference/daemon-api.md +++ b/docs/reference/daemon-api.md @@ -84,9 +84,9 @@ compatibility surface. | `HelloOk` | struct | [`HelloOk`](../../packages/d2b-contracts/src/lib.rs#L142) | struct { `server_version`: `Version`; `selected_version`: `Version`; `capabilities`: `Vec` } | | `HelloRejected` | struct | [`HelloRejected`](../../packages/d2b-contracts/src/lib.rs#L150) | struct { `reason`: `HelloRejectedReason` } | | `HelloRejectedReason` | enum | [`HelloRejectedReason`](../../packages/d2b-contracts/src/lib.rs#L156) | `VersionMismatch`; `CapabilityNegotiationFailed`; `InternalError` | -| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1443) | struct { `client_version`: `String`; `supported_features`: `Vec` } | +| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1637) | struct { `client_version`: `String`; `supported_features`: `Vec` } | | `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-control/src/guest_wire.rs#L581) | struct { `metadata`: `GuestRequestMetadata`; `host_nonce`: `GuestNonce`; `transcript_version`: `u32` } | -| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1582) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | +| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1776) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | | `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-control/src/guest_wire.rs#L589) | struct { `guest_nonce`: `GuestNonce`; `guest_boot_id`: `GuestBootId`; `protocol_version`: `u32` } | | `KnownFeatureFlag` | enum | [`KnownFeatureFlag`](../../packages/d2b-contracts/src/lib.rs#L104) | `TypedErrors`; `ManifestV04`; `StatusCheckBridges`; `ExportBrokerAudit`; `ConfiguredLaunchV1`; `UnsafeLocalProviderV1`; `CutoverRunnerV1` | | `SemverRange` | struct | [`SemverRange`](../../packages/d2b-contracts/src/error.rs#L1135) | empty struct | @@ -289,78 +289,78 @@ host reboot. | `CutoverVerificationRequest` | struct | [`CutoverVerificationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L506) | struct { `expected_zone_ids`: `Vec` } | | `CutoverAdmissionRequest` | struct | [`CutoverAdmissionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L543) | struct { `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option` } | | `CutoverEffectRequest` | struct | [`CutoverEffectRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L604) | struct { `operation_id`: `BundleOpId`; `authority`: `CutoverEffectAuthority`; `phase`: `u8`; `effect_id`: `BundleOpId`; `effect`: `CutoverEffectKind`; `replay_class`: `CutoverReplayClass`; `request_digest`: `CanonicalAuditDigest`; `capability_digest`: `CanonicalAuditDigest`; `identity`: `Option`; `handoff`: `Option`; `payload`: `Option` } | -| `RunHostInstallRequest` | struct | [`RunHostInstallRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1282) | struct { `bundle_installer_intent_ref`: `BundleOpId`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool`; `tracing_span_id`: `Option` } | -| `RunMigrateRequest` | struct | [`RunMigrateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1306) | struct { `bundle_migrate_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `RunActivationRequest` | struct | [`RunActivationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1339) | struct { `bundle_activation_intent_ref`: `BundleOpId`; `mode`: `ActivationMode`; `system_artifact_id`: `Option`; `phase`: `ActivationPhase`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunGcRequest` | struct | [`RunGcRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1366) | struct { `bundle_gc_intent_ref`: `BundleOpId`; `keep_generations`: `Option`; `tracing_span_id`: `Option` } | -| `RunKeysRotateRequest` | struct | [`RunKeysRotateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1385) | struct { `bundle_keys_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunHostKeyTrustRequest` | struct | [`RunHostKeyTrustRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1402) | struct { `bundle_trust_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunRotateKnownHostRequest` | struct | [`RunRotateKnownHostRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1420) | struct { `bundle_rotate_known_host_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1443) | struct { `client_version`: `String`; `supported_features`: `Vec` } | -| `ApplyNftablesRequest` | struct | [`ApplyNftablesRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1594) | struct { `bundle_nft_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `desired_hash`: `Option`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplyNftablesProjectionRequest` | struct | [`ApplyNftablesProjectionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1625) | struct { `bundle_nft_projection_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `action`: `NftablesProjectionAction`; `expected_generation_id`: `ResourceBundleGenerationId`; `desired_hash`: `Option`; `tracing_span_id`: `Option` } | -| `ApplyNmUnmanagedRequest` | struct | [`ApplyNmUnmanagedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1639) | struct { `bundle_nm_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplyRouteRequest` | struct | [`ApplyRouteRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1650) | struct { `bundle_route_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplySysctlRequest` | struct | [`ApplySysctlRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1661) | struct { `bundle_sysctl_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `BindUnixSocketRequest` | struct | [`BindUnixSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1672) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `CreateOrReconcileUsersGroupsRequest` | struct | [`CreateOrReconcileUsersGroupsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1682) | struct { `subject_ids`: `Vec`; `tracing_span_id`: `Option` } | -| `CreatePersistentTapRequest` | struct | [`CreatePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1696) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `attachment_id`: `Option`; `network_generation`: `Option`; `attachment_generation`: `Option`; `tracing_span_id`: `Option` } | -| `DeletePersistentTapRequest` | struct | [`DeletePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1716) | struct { `attachment_id`: `ResourceUid`; `expected_network_generation`: `ResourceGeneration`; `expected_attachment_generation`: `ResourceGeneration`; `tracing_span_id`: `Option` } | -| `CreateBridgeRequest` | struct | [`CreateBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1730) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `DeleteBridgeRequest` | struct | [`DeleteBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1741) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `CreateTapFdRequest` | struct | [`CreateTapFdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1752) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `DelegateCgroupV2Request` | struct | [`DelegateCgroupV2Request`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1764) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `ExportBrokerAuditRequest` | struct | [`ExportBrokerAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1772) | struct { `filter`: `Option`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | -| `GuestControlSignRequest` | struct | [`GuestControlSignRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1882) | struct { `vm_id`: `VmId`; `role`: `GuestControlProofRole`; `protocol_version`: `u32`; `direction`: `GuestControlDirection`; `purpose`: `GuestControlAuthPurpose`; `guest_control_port`: `u32`; `peer_cid`: `Option`; `host_nonce`: `Vec`; `guest_nonce`: `Vec`; `guest_boot_id`: `GuestBootIdWire`; `capabilities_hash`: `Option`; `tracing_span_id`: `Option` } | -| `SecretByIdRequest` | struct | [`SecretByIdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1937) | struct { `opaque_id`: `String`; `tracing_span_id`: `Option` } | -| `LaunchMinijailChildRequest` | struct | [`LaunchMinijailChildRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1949) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `ModprobeIfAllowedRequest` | struct | [`ModprobeIfAllowedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1963) | struct { `module_name`: `String`; `tracing_span_id`: `Option` } | -| `OpenCgroupDirRequest` | struct | [`OpenCgroupDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1973) | struct { `scope_id`: `ScopeId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | -| `OpenDeviceRequest` | struct | [`OpenDeviceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1982) | struct { `role_id`: `RoleId`; `device_class`: `String`; `tracing_span_id`: `Option` } | -| `OpenKvmRequest` | struct | [`OpenKvmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1991) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `QemuMediaEnrollRequest` | struct | [`QemuMediaEnrollRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2005) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `bus_id`: `String`; `tracing_span_id`: `Option` } | -| `QemuMediaRefreshRegistryRequest` | struct | [`QemuMediaRefreshRegistryRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2026) | struct { `tracing_span_id`: `Option` } | -| `QemuMediaBootRequest` | struct | [`QemuMediaBootRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2048) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `QemuMediaLifecycleRequest` | struct | [`QemuMediaLifecycleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2056) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `QemuMediaQueryStatusRequest` | struct | [`QemuMediaQueryStatusRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2064) | struct { `vm_id`: `VmId`; `shutdown_context`: `bool`; `tracing_span_id`: `Option` } | -| `QemuMediaHotplugRequest` | struct | [`QemuMediaHotplugRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2129) | struct { `vm_id`: `VmId`; `bus_id`: `String`; `tracing_span_id`: `Option` } | -| `OpenPidfdRequest` | struct | [`OpenPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2187) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `expected_start_time_ticks`: `u64`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `OpenPeerPidfdFromAcceptedSocketRequest` | struct | [`OpenPeerPidfdFromAcceptedSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2230) | empty struct | -| `ObserveRunnerRequest` | struct | [`ObserveRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2245) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `PipeWireAudioRequest` | struct | [`PipeWireAudioRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2295) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `bundle_runner_intent_ref`: `BundleOpId`; `channel`: `PipeWireAudioChannel`; `action`: `PipeWireAudioAction`; `tracing_span_id`: `Option` } | -| `SystemdUnitRequest` | struct | [`SystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2362) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `bundle_content_identity`: `String`; `provider_identity`: `[u8; 32]`; `template_identity`: `[u8; 32]`; `generation`: `u64`; `domain`: `SystemdUnitDomain`; `execution_ref`: `Option`; `user_ref`: `Option`; `sandbox_plan`: `Option`; `tracing_span_id`: `Option` } | -| `OpenSystemdUnitPidfdRequest` | struct | [`OpenSystemdUnitPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2415) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity` } | -| `StopSystemdUnitRequest` | struct | [`StopSystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2426) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity`; `class`: `SystemdStopClass` } | -| `OpenZoneStoreRequest` | struct | [`OpenZoneStoreRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2502) | struct { `zone_store_id`: `ZoneStoreId` } | -| `OpenVhostNetRequest` | struct | [`OpenVhostNetRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2536) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `OpenFuseRequest` | struct | [`OpenFuseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2544) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `OpenHidrawSecurityKeyRequest` | struct | [`OpenHidrawSecurityKeyRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2556) | struct { `vm_id`: `VmId`; `selector_id`: `String`; `device_ref`: `ResourceRef`; `authority_key`: `String`; `tracing_span_id`: `Option` } | -| `PrepareDirRequest` | struct | [`PrepareDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2599) | struct { `vm_id`: `VmId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | -| `MigrateLegacySwtpmStateRequest` | struct | [`MigrateLegacySwtpmStateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2609) | struct { `bundle_legacy_swtpm_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `probe_only`: `bool`; `tracing_span_id`: `Option` } | -| `PrepareStoreViewRequest` | struct | [`PrepareStoreViewRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2662) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `StoreSyncRequest` | struct | [`StoreSyncRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2682) | struct { `vm_id`: `VmId`; `bundle_closure_ref`: `BundleClosureRef`; `generation_token`: `u32`; `tracing_span_id`: `Option` } | -| `SetBridgePortFlagsRequest` | struct | [`SetBridgePortFlagsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2726) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `SetSocketAclRequest` | struct | [`SetSocketAclRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2735) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `SetupMountNamespaceRequest` | struct | [`SetupMountNamespaceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2745) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `UpdateHostsFileRequest` | struct | [`UpdateHostsFileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2756) | struct { `bundle_hosts_intent_ref`: `BundleOpId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `UsbipBindRequest` | struct | [`UsbipBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2769) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `UsbipBindFirewallRuleRequest` | struct | [`UsbipBindFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2786) | struct { `bundle_usbip_firewall_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `UsbipProxyReconcileRequest` | struct | [`UsbipProxyReconcileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2794) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `UsbipUnbindRequest` | struct | [`UsbipUnbindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2802) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `preserve_durable_claim`: `bool`; `tracing_span_id`: `Option` } | -| `UsbipExplicitBindRequest` | struct | [`UsbipExplicitBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2824) | struct { `bus_id`: `String`; `vm`: `String`; `env`: `String`; `tracing_span_id`: `Option` } | -| `UsbipExplicitFirewallRuleRequest` | struct | [`UsbipExplicitFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2842) | struct { `bus_id`: `String`; `env`: `String`; `host_uplink_ip`: `String`; `net_uplink_ip`: `String`; `tracing_span_id`: `Option` } | -| `ResourceActivationAuditRequest` | struct | [`ResourceActivationAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2934) | struct { `audit_join`: `AuditJoinContext` } | -| `SignalRunnerRequest` | struct | [`SignalRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2965) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `signal`: `RunnerSignal`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `CgroupKillRequest` | struct | [`CgroupKillRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2992) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `DeregisterRunnerPidfdRequest` | struct | [`DeregisterRunnerPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3001) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `SpawnRunnerRequest` | struct | [`SpawnRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3120) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `bundle_content_identity`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `sandbox_plan`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `runtime_allocations`: `Vec`; `tracing_span_id`: `Option`; `workload_identity`: `Option` } | -| `SeedDnsmasqLeaseRequest` | struct | [`SeedDnsmasqLeaseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3395) | struct { `vm_id`: `VmId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `BindMountFromHardlinkFarmRequest` | struct | [`BindMountFromHardlinkFarmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3407) | struct { `vm_id`: `VmId`; `bundle_store_view_intent_ref`: `Option`; `tracing_span_id`: `Option` } | -| `OwnershipMatrixCheckRequest` | struct | [`OwnershipMatrixCheckRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3422) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `SshHostKeyPreflightRequest` | struct | [`SshHostKeyPreflightRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3433) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `ReconcileStorageScopeRequest` | struct | [`ReconcileStorageScopeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3447) | struct { `storage_ref`: `BundleOpId`; `apply`: `bool`; `tracing_span_id`: `Option` } | -| `ValidateLockSpecRequest` | struct | [`ValidateLockSpecRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3484) | struct { `lock_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `DiskInitRequest` | struct | [`DiskInitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3518) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `RunHostInstallRequest` | struct | [`RunHostInstallRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1476) | struct { `bundle_installer_intent_ref`: `BundleOpId`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool`; `tracing_span_id`: `Option` } | +| `RunMigrateRequest` | struct | [`RunMigrateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1500) | struct { `bundle_migrate_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `RunActivationRequest` | struct | [`RunActivationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1533) | struct { `bundle_activation_intent_ref`: `BundleOpId`; `mode`: `ActivationMode`; `system_artifact_id`: `Option`; `phase`: `ActivationPhase`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunGcRequest` | struct | [`RunGcRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1560) | struct { `bundle_gc_intent_ref`: `BundleOpId`; `keep_generations`: `Option`; `tracing_span_id`: `Option` } | +| `RunKeysRotateRequest` | struct | [`RunKeysRotateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1579) | struct { `bundle_keys_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunHostKeyTrustRequest` | struct | [`RunHostKeyTrustRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1596) | struct { `bundle_trust_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunRotateKnownHostRequest` | struct | [`RunRotateKnownHostRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1614) | struct { `bundle_rotate_known_host_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1637) | struct { `client_version`: `String`; `supported_features`: `Vec` } | +| `ApplyNftablesRequest` | struct | [`ApplyNftablesRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1788) | struct { `bundle_nft_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `desired_hash`: `Option`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplyNftablesProjectionRequest` | struct | [`ApplyNftablesProjectionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1819) | struct { `bundle_nft_projection_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `action`: `NftablesProjectionAction`; `expected_generation_id`: `ResourceBundleGenerationId`; `desired_hash`: `Option`; `tracing_span_id`: `Option` } | +| `ApplyNmUnmanagedRequest` | struct | [`ApplyNmUnmanagedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1833) | struct { `bundle_nm_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplyRouteRequest` | struct | [`ApplyRouteRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1844) | struct { `bundle_route_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplySysctlRequest` | struct | [`ApplySysctlRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1855) | struct { `bundle_sysctl_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `BindUnixSocketRequest` | struct | [`BindUnixSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1866) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `CreateOrReconcileUsersGroupsRequest` | struct | [`CreateOrReconcileUsersGroupsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1876) | struct { `subject_ids`: `Vec`; `tracing_span_id`: `Option` } | +| `CreatePersistentTapRequest` | struct | [`CreatePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1890) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `attachment_id`: `Option`; `network_generation`: `Option`; `attachment_generation`: `Option`; `tracing_span_id`: `Option` } | +| `DeletePersistentTapRequest` | struct | [`DeletePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1910) | struct { `attachment_id`: `ResourceUid`; `expected_network_generation`: `ResourceGeneration`; `expected_attachment_generation`: `ResourceGeneration`; `tracing_span_id`: `Option` } | +| `CreateBridgeRequest` | struct | [`CreateBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1924) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `DeleteBridgeRequest` | struct | [`DeleteBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1935) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `CreateTapFdRequest` | struct | [`CreateTapFdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1946) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `DelegateCgroupV2Request` | struct | [`DelegateCgroupV2Request`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1958) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `ExportBrokerAuditRequest` | struct | [`ExportBrokerAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1966) | struct { `filter`: `Option`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | +| `GuestControlSignRequest` | struct | [`GuestControlSignRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2076) | struct { `vm_id`: `VmId`; `role`: `GuestControlProofRole`; `protocol_version`: `u32`; `direction`: `GuestControlDirection`; `purpose`: `GuestControlAuthPurpose`; `guest_control_port`: `u32`; `peer_cid`: `Option`; `host_nonce`: `Vec`; `guest_nonce`: `Vec`; `guest_boot_id`: `GuestBootIdWire`; `capabilities_hash`: `Option`; `tracing_span_id`: `Option` } | +| `SecretByIdRequest` | struct | [`SecretByIdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2131) | struct { `opaque_id`: `String`; `tracing_span_id`: `Option` } | +| `LaunchMinijailChildRequest` | struct | [`LaunchMinijailChildRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2143) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `ModprobeIfAllowedRequest` | struct | [`ModprobeIfAllowedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2157) | struct { `module_name`: `String`; `tracing_span_id`: `Option` } | +| `OpenCgroupDirRequest` | struct | [`OpenCgroupDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2167) | struct { `scope_id`: `ScopeId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | +| `OpenDeviceRequest` | struct | [`OpenDeviceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2176) | struct { `role_id`: `RoleId`; `device_class`: `String`; `tracing_span_id`: `Option` } | +| `OpenKvmRequest` | struct | [`OpenKvmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2185) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `QemuMediaEnrollRequest` | struct | [`QemuMediaEnrollRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2199) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `bus_id`: `String`; `tracing_span_id`: `Option` } | +| `QemuMediaRefreshRegistryRequest` | struct | [`QemuMediaRefreshRegistryRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2220) | struct { `tracing_span_id`: `Option` } | +| `QemuMediaBootRequest` | struct | [`QemuMediaBootRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2242) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `QemuMediaLifecycleRequest` | struct | [`QemuMediaLifecycleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2250) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `QemuMediaQueryStatusRequest` | struct | [`QemuMediaQueryStatusRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2258) | struct { `vm_id`: `VmId`; `shutdown_context`: `bool`; `tracing_span_id`: `Option` } | +| `QemuMediaHotplugRequest` | struct | [`QemuMediaHotplugRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2323) | struct { `vm_id`: `VmId`; `bus_id`: `String`; `tracing_span_id`: `Option` } | +| `OpenPidfdRequest` | struct | [`OpenPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2381) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `expected_start_time_ticks`: `u64`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | +| `OpenPeerPidfdFromAcceptedSocketRequest` | struct | [`OpenPeerPidfdFromAcceptedSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2424) | empty struct | +| `ObserveRunnerRequest` | struct | [`ObserveRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2439) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | +| `PipeWireAudioRequest` | struct | [`PipeWireAudioRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2489) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `bundle_runner_intent_ref`: `BundleOpId`; `channel`: `PipeWireAudioChannel`; `action`: `PipeWireAudioAction`; `tracing_span_id`: `Option` } | +| `SystemdUnitRequest` | struct | [`SystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2556) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `bundle_content_identity`: `String`; `provider_identity`: `[u8; 32]`; `template_identity`: `[u8; 32]`; `generation`: `u64`; `domain`: `SystemdUnitDomain`; `execution_ref`: `Option`; `user_ref`: `Option`; `sandbox_plan`: `Option`; `tracing_span_id`: `Option` } | +| `OpenSystemdUnitPidfdRequest` | struct | [`OpenSystemdUnitPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2609) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity` } | +| `StopSystemdUnitRequest` | struct | [`StopSystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2620) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity`; `class`: `SystemdStopClass` } | +| `OpenZoneStoreRequest` | struct | [`OpenZoneStoreRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2696) | struct { `zone_store_id`: `ZoneStoreId` } | +| `OpenVhostNetRequest` | struct | [`OpenVhostNetRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2730) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `OpenFuseRequest` | struct | [`OpenFuseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2738) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `OpenHidrawSecurityKeyRequest` | struct | [`OpenHidrawSecurityKeyRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2750) | struct { `vm_id`: `VmId`; `selector_id`: `String`; `device_ref`: `ResourceRef`; `authority_key`: `String`; `tracing_span_id`: `Option` } | +| `PrepareDirRequest` | struct | [`PrepareDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2793) | struct { `vm_id`: `VmId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | +| `MigrateLegacySwtpmStateRequest` | struct | [`MigrateLegacySwtpmStateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2803) | struct { `bundle_legacy_swtpm_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `probe_only`: `bool`; `tracing_span_id`: `Option` } | +| `PrepareStoreViewRequest` | struct | [`PrepareStoreViewRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2856) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `StoreSyncRequest` | struct | [`StoreSyncRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2876) | struct { `vm_id`: `VmId`; `bundle_closure_ref`: `BundleClosureRef`; `generation_token`: `u32`; `tracing_span_id`: `Option` } | +| `SetBridgePortFlagsRequest` | struct | [`SetBridgePortFlagsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2920) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `SetSocketAclRequest` | struct | [`SetSocketAclRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2929) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `SetupMountNamespaceRequest` | struct | [`SetupMountNamespaceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2939) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `UpdateHostsFileRequest` | struct | [`UpdateHostsFileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2950) | struct { `bundle_hosts_intent_ref`: `BundleOpId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `UsbipBindRequest` | struct | [`UsbipBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2963) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `UsbipBindFirewallRuleRequest` | struct | [`UsbipBindFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2980) | struct { `bundle_usbip_firewall_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `UsbipProxyReconcileRequest` | struct | [`UsbipProxyReconcileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2988) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `UsbipUnbindRequest` | struct | [`UsbipUnbindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2996) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `preserve_durable_claim`: `bool`; `tracing_span_id`: `Option` } | +| `UsbipExplicitBindRequest` | struct | [`UsbipExplicitBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3018) | struct { `bus_id`: `String`; `vm`: `String`; `env`: `String`; `tracing_span_id`: `Option` } | +| `UsbipExplicitFirewallRuleRequest` | struct | [`UsbipExplicitFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3036) | struct { `bus_id`: `String`; `env`: `String`; `host_uplink_ip`: `String`; `net_uplink_ip`: `String`; `tracing_span_id`: `Option` } | +| `ResourceActivationAuditRequest` | struct | [`ResourceActivationAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3128) | struct { `audit_join`: `AuditJoinContext` } | +| `SignalRunnerRequest` | struct | [`SignalRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3159) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `signal`: `RunnerSignal`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | +| `CgroupKillRequest` | struct | [`CgroupKillRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3186) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `DeregisterRunnerPidfdRequest` | struct | [`DeregisterRunnerPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3195) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | +| `SpawnRunnerRequest` | struct | [`SpawnRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3322) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `bundle_content_identity`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `sandbox_plan`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `runtime_allocations`: `Vec`; `tracing_span_id`: `Option`; `workload_identity`: `Option` } | +| `SeedDnsmasqLeaseRequest` | struct | [`SeedDnsmasqLeaseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3597) | struct { `vm_id`: `VmId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `BindMountFromHardlinkFarmRequest` | struct | [`BindMountFromHardlinkFarmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3609) | struct { `vm_id`: `VmId`; `bundle_store_view_intent_ref`: `Option`; `tracing_span_id`: `Option` } | +| `OwnershipMatrixCheckRequest` | struct | [`OwnershipMatrixCheckRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3624) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `SshHostKeyPreflightRequest` | struct | [`SshHostKeyPreflightRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3635) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `ReconcileStorageScopeRequest` | struct | [`ReconcileStorageScopeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3649) | struct { `storage_ref`: `BundleOpId`; `apply`: `bool`; `tracing_span_id`: `Option` } | +| `ValidateLockSpecRequest` | struct | [`ValidateLockSpecRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3686) | struct { `lock_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `DiskInitRequest` | struct | [`DiskInitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3720) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | ### Console and audio wire types @@ -430,47 +430,47 @@ see the auto-generated tables above for the committed Rust variants. | `CutoverVerificationResponse` | struct | [`CutoverVerificationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L521) | struct { `zones`: `Vec`; `sources_preserved`: `bool`; `identity_digests_match`: `bool`; `candidate_current`: `bool` } | | `CutoverAdmissionResponse` | struct | [`CutoverAdmissionResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L533) | struct { `candidate_current`: `bool`; `markers_valid`: `bool`; `ownership_valid`: `bool`; `predicates_hold`: `bool` } | | `CutoverEffectResponse` | struct | [`CutoverEffectResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L634) | struct { `outcome`: `CutoverEffectOutcome`; `identity`: `Option`; `verification`: `Option`; `admission`: `Option`; `audit_record_id`: `CanonicalAuditDigest` } | -| `RunHostInstallResponse` | struct | [`RunHostInstallResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1296) | struct { `installed`: `bool`; `enabled`: `bool`; `started`: `bool`; `artifacts_written`: `Vec` } | -| `RunMigrateResponse` | struct | [`RunMigrateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1314) | struct { `migrated_vm_count`: `u32`; `notes`: `Vec` } | -| `RunActivationResponse` | struct | [`RunActivationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1354) | struct { `mode`: `ActivationMode`; `vm`: `String`; `generation_number`: `Option`; `guest_switch_script_path`: `Option`; `summary`: `String` } | -| `RunGcResponse` | struct | [`RunGcResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1376) | struct { `keep_generations`: `Option`; `retained_store_path_count`: `u32`; `summary`: `String` } | -| `RunKeysRotateResponse` | struct | [`RunKeysRotateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1394) | struct { `vm`: `String`; `key_path`: `String`; `public_key_fingerprint`: `String` } | -| `RunHostKeyTrustResponse` | struct | [`RunHostKeyTrustResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1411) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `updated`: `bool` } | -| `RunRotateKnownHostResponse` | struct | [`RunRotateKnownHostResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1429) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `removed`: `bool` } | -| `BrokerResponse` | enum | [`BrokerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1451) | `ApplyHostGenerationHandoff` - (ApplyHostGenerationHandoffResponse); `LaunchCutoverRunner` - (LaunchCutoverRunnerResponse); `CutoverAudit` - (CutoverAuditResponse); `CutoverEffect` - (CutoverEffectResponse); `Ack` - (AckResponse); `CreatePersistentTap` - (TapReadyResponse); `CreateTapFd` - (TapReadyResponse); `Error` - (BrokerErrorResponse); `ExportBrokerAudit` - (ExportBrokerAuditResponse); `RunHostInstall` - (RunHostInstallResponse); `RunMigrate` - (RunMigrateResponse); `RunActivation` - (RunActivationResponse); `RunGc` - (RunGcResponse); `RunKeysRotate` - (RunKeysRotateResponse); `RunHostKeyTrust` - (RunHostKeyTrustResponse); `RunRotateKnownHost` - (RunRotateKnownHostResponse); `Hello` - (HelloResponse); `GuestControlSign` - (GuestControlSignResponse); `QemuMediaEnroll` - (QemuMediaEnrollResponse); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryResponse); `QemuMediaBoot` - (QemuMediaHotplugResponse); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleResponse); `QemuMediaQueryStatus` - (QemuMediaQueryStatusResponse); `QemuMediaQuit` - (QemuMediaLifecycleResponse); `QemuMediaAttach` - (QemuMediaHotplugResponse); `QemuMediaDetach` - (QemuMediaHotplugResponse); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyResponse); `OpenPidfd` - (OpenPidfdResponse); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketResponse); `ObserveRunner` - (ObserveRunnerResponse); `PipeWireAudio` - (PipeWireAudioResponse); `StartSystemdUnit` - (StartSystemdUnitResponse); `CheckSystemdUserManager` - (CheckSystemdUserManagerResponse); `ObserveSystemdUnit` - (ObserveSystemdUnitResponse); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdResponse); `StopSystemdUnit` - (StopSystemdUnitResponse); `OpenZoneStore` - (OpenZoneStoreResponse); `ResourceActivationAudit` - (ResourceActivationAuditResponse); `PollChildReaped` - (PollChildReapedResponse); `ReconcileStorageScope` - (ReconcileStorageScopeResponse); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateResponse); `SetBridgePortFlags` - (BridgePortFlagsResponse); `SignalRunner` - (SignalRunnerResponse); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdResponse); `SpawnRunner` - (SpawnRunnerResponse); `StoreSync` - (StoreSyncResponse); `StoreVerify` - (StoreVerifyResponse); `ValidateLockSpec` - (ValidateLockSpecResponse); `ValidateBundle` - (ValidateBundleResponse) | -| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1555) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | -| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1582) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | -| `GuestControlSignResponse` | struct | [`GuestControlSignResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1930) | struct { `tag`: `Vec` } | -| `QemuMediaEnrollResponse` | struct | [`QemuMediaEnrollResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2015) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `read_only`: `bool`; `enrolled`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | -| `QemuMediaRefreshRegistryResponse` | struct | [`QemuMediaRefreshRegistryResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2033) | struct { `record_count`: `u32`; `redacted_index_written`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | -| `QemuMediaLifecycleResponse` | struct | [`QemuMediaLifecycleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2109) | struct { `vm_id`: `VmId`; `command`: `QemuMediaLifecycleAction` } | -| `QemuMediaQueryStatusResponse` | struct | [`QemuMediaQueryStatusResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2116) | struct { `vm_id`: `VmId`; `status`: `QemuMediaVmStatus` } | -| `QemuMediaHotplugResponse` | struct | [`QemuMediaHotplugResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2159) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `slot`: `String`; `read_only`: `bool`; `qmp_commands`: `Vec`; `events`: `Vec` } | -| `OpenPidfdResponse` | struct | [`OpenPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2214) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `verified_start_time_ticks`: `u64`; `pidfd_index`: `u32` } | -| `OpenPeerPidfdFromAcceptedSocketResponse` | struct | [`OpenPeerPidfdFromAcceptedSocketResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2235) | struct { `pidfd_index`: `u32` } | -| `ObserveRunnerResponse` | struct | [`ObserveRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2262) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `pid`: `i32`; `start_time_ticks`: `u64`; `cgroup_verified`: `bool`; `executable_verified`: `bool` } | -| `PipeWireAudioResponse` | struct | [`PipeWireAudioResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2314) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `applied`: `bool`; `host_ready`: `bool`; `node_present`: `bool` } | -| `StartTransientUnitResponse` | struct | [`StartTransientUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2449) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | -| `CheckSystemdUserManagerResponse` | struct | [`CheckSystemdUserManagerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2461) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `available`: `bool` } | -| `ObserveSystemdUnitResponse` | struct | [`ObserveSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2470) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `identity`: `Option` } | -| `OpenSystemdUnitPidfdResponse` | struct | [`OpenSystemdUnitPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2481) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | -| `StopSystemdUnitResponse` | struct | [`StopSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2491) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `stopped`: `bool` } | -| `OpenZoneStoreResponse` | struct | [`OpenZoneStoreResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2527) | struct { `zone_store_id`: `ZoneStoreId`; `store_identity`: `String`; `disposition`: `ZoneStoreDisposition`; `fd_index`: `u32` } | -| `OpenHidrawSecurityKeyResponse` | struct | [`OpenHidrawSecurityKeyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2586) | struct { `selector_resolved`: `String`; `device_class`: `String` } | -| `MigrateLegacySwtpmStateResponse` | struct | [`MigrateLegacySwtpmStateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2656) | struct { `outcome`: `LegacySwtpmMigrationOutcome` } | -| `StoreSyncResponse` | struct | [`StoreSyncResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2703) | struct { `vm`: `String`; `generation_id`: `String`; `generation_token`: `u32`; `hardlink_farm_path`: `String`; `closure_count`: `u32`; `retained_generations`: `Vec`; `swept_count`: `u32`; `cleanup_deferred`: `bool` } | -| `AckResponse` | struct | [`AckResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2857) | struct { `accepted`: `bool`; `operation`: `String` } | -| `TapReadyResponse` | struct | [`TapReadyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2864) | struct { `bridge`: `Option`; `tap`: `IfName` } | -| `ExportBrokerAuditResponse` | struct | [`ExportBrokerAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2871) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | -| `BridgePortFlagsResponse` | struct | [`BridgePortFlagsResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2916) | struct { `bridge`: `IfName`; `isolated`: `bool`; `neigh_suppress`: `bool`; `port`: `IfName` } | -| `ValidateBundleResponse` | struct | [`ValidateBundleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2925) | struct { `valid`: `bool` } | -| `ResourceActivationAuditResponse` | struct | [`ResourceActivationAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2947) | struct { `recorded`: `bool` } | -| `SignalRunnerResponse` | struct | [`SignalRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2984) | struct { `signaled`: `bool`; `vm_id`: `VmId`; `role_id`: `RoleId` } | -| `DeregisterRunnerPidfdResponse` | struct | [`DeregisterRunnerPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3020) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `removed`: `bool` } | -| `SpawnRunnerResponse` | struct | [`SpawnRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3222) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `bundle_content_identity`: `Option`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `u32`; `console_fd_index`: `Option` } | -| `ReconcileStorageScopeResponse` | struct | [`ReconcileStorageScopeResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3468) | struct { `storage_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `status`: `StorageReconcileStatus`; `applied`: `bool`; `path_hash`: `String` } | -| `ValidateLockSpecResponse` | struct | [`ValidateLockSpecResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3492) | struct { `lock_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `cloexec_required`: `bool`; `fd_passing_mechanism`: `String`; `order_key`: `String` } | -| `PollChildReapedResponse` | struct | [`PollChildReapedResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3579) | struct { `notifications`: `Vec` } | +| `RunHostInstallResponse` | struct | [`RunHostInstallResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1490) | struct { `installed`: `bool`; `enabled`: `bool`; `started`: `bool`; `artifacts_written`: `Vec` } | +| `RunMigrateResponse` | struct | [`RunMigrateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1508) | struct { `migrated_vm_count`: `u32`; `notes`: `Vec` } | +| `RunActivationResponse` | struct | [`RunActivationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1548) | struct { `mode`: `ActivationMode`; `vm`: `String`; `generation_number`: `Option`; `guest_switch_script_path`: `Option`; `summary`: `String` } | +| `RunGcResponse` | struct | [`RunGcResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1570) | struct { `keep_generations`: `Option`; `retained_store_path_count`: `u32`; `summary`: `String` } | +| `RunKeysRotateResponse` | struct | [`RunKeysRotateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1588) | struct { `vm`: `String`; `key_path`: `String`; `public_key_fingerprint`: `String` } | +| `RunHostKeyTrustResponse` | struct | [`RunHostKeyTrustResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1605) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `updated`: `bool` } | +| `RunRotateKnownHostResponse` | struct | [`RunRotateKnownHostResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1623) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `removed`: `bool` } | +| `BrokerResponse` | enum | [`BrokerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1645) | `ApplyHostGenerationHandoff` - (ApplyHostGenerationHandoffResponse); `LaunchCutoverRunner` - (LaunchCutoverRunnerResponse); `CutoverAudit` - (CutoverAuditResponse); `CutoverEffect` - (CutoverEffectResponse); `Ack` - (AckResponse); `CreatePersistentTap` - (TapReadyResponse); `CreateTapFd` - (TapReadyResponse); `Error` - (BrokerErrorResponse); `ExportBrokerAudit` - (ExportBrokerAuditResponse); `RunHostInstall` - (RunHostInstallResponse); `RunMigrate` - (RunMigrateResponse); `RunActivation` - (RunActivationResponse); `RunGc` - (RunGcResponse); `RunKeysRotate` - (RunKeysRotateResponse); `RunHostKeyTrust` - (RunHostKeyTrustResponse); `RunRotateKnownHost` - (RunRotateKnownHostResponse); `Hello` - (HelloResponse); `GuestControlSign` - (GuestControlSignResponse); `QemuMediaEnroll` - (QemuMediaEnrollResponse); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryResponse); `QemuMediaBoot` - (QemuMediaHotplugResponse); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleResponse); `QemuMediaQueryStatus` - (QemuMediaQueryStatusResponse); `QemuMediaQuit` - (QemuMediaLifecycleResponse); `QemuMediaAttach` - (QemuMediaHotplugResponse); `QemuMediaDetach` - (QemuMediaHotplugResponse); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyResponse); `OpenPidfd` - (OpenPidfdResponse); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketResponse); `ObserveRunner` - (ObserveRunnerResponse); `PipeWireAudio` - (PipeWireAudioResponse); `StartSystemdUnit` - (StartSystemdUnitResponse); `CheckSystemdUserManager` - (CheckSystemdUserManagerResponse); `ObserveSystemdUnit` - (ObserveSystemdUnitResponse); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdResponse); `StopSystemdUnit` - (StopSystemdUnitResponse); `OpenZoneStore` - (OpenZoneStoreResponse); `ResourceActivationAudit` - (ResourceActivationAuditResponse); `PollChildReaped` - (PollChildReapedResponse); `ReconcileStorageScope` - (ReconcileStorageScopeResponse); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateResponse); `SetBridgePortFlags` - (BridgePortFlagsResponse); `SignalRunner` - (SignalRunnerResponse); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdResponse); `SpawnRunner` - (SpawnRunnerResponse); `StoreSync` - (StoreSyncResponse); `StoreVerify` - (StoreVerifyResponse); `ValidateLockSpec` - (ValidateLockSpecResponse); `ValidateBundle` - (ValidateBundleResponse) | +| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1749) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | +| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1776) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | +| `GuestControlSignResponse` | struct | [`GuestControlSignResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2124) | struct { `tag`: `Vec` } | +| `QemuMediaEnrollResponse` | struct | [`QemuMediaEnrollResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2209) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `read_only`: `bool`; `enrolled`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | +| `QemuMediaRefreshRegistryResponse` | struct | [`QemuMediaRefreshRegistryResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2227) | struct { `record_count`: `u32`; `redacted_index_written`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | +| `QemuMediaLifecycleResponse` | struct | [`QemuMediaLifecycleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2303) | struct { `vm_id`: `VmId`; `command`: `QemuMediaLifecycleAction` } | +| `QemuMediaQueryStatusResponse` | struct | [`QemuMediaQueryStatusResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2310) | struct { `vm_id`: `VmId`; `status`: `QemuMediaVmStatus` } | +| `QemuMediaHotplugResponse` | struct | [`QemuMediaHotplugResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2353) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `slot`: `String`; `read_only`: `bool`; `qmp_commands`: `Vec`; `events`: `Vec` } | +| `OpenPidfdResponse` | struct | [`OpenPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2408) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `verified_start_time_ticks`: `u64`; `pidfd_index`: `u32` } | +| `OpenPeerPidfdFromAcceptedSocketResponse` | struct | [`OpenPeerPidfdFromAcceptedSocketResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2429) | struct { `pidfd_index`: `u32` } | +| `ObserveRunnerResponse` | struct | [`ObserveRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2456) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `pid`: `i32`; `start_time_ticks`: `u64`; `cgroup_verified`: `bool`; `executable_verified`: `bool` } | +| `PipeWireAudioResponse` | struct | [`PipeWireAudioResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2508) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `applied`: `bool`; `host_ready`: `bool`; `node_present`: `bool` } | +| `StartTransientUnitResponse` | struct | [`StartTransientUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2643) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | +| `CheckSystemdUserManagerResponse` | struct | [`CheckSystemdUserManagerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2655) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `available`: `bool` } | +| `ObserveSystemdUnitResponse` | struct | [`ObserveSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2664) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `identity`: `Option` } | +| `OpenSystemdUnitPidfdResponse` | struct | [`OpenSystemdUnitPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2675) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | +| `StopSystemdUnitResponse` | struct | [`StopSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2685) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `stopped`: `bool` } | +| `OpenZoneStoreResponse` | struct | [`OpenZoneStoreResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2721) | struct { `zone_store_id`: `ZoneStoreId`; `store_identity`: `String`; `disposition`: `ZoneStoreDisposition`; `fd_index`: `u32` } | +| `OpenHidrawSecurityKeyResponse` | struct | [`OpenHidrawSecurityKeyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2780) | struct { `selector_resolved`: `String`; `device_class`: `String` } | +| `MigrateLegacySwtpmStateResponse` | struct | [`MigrateLegacySwtpmStateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2850) | struct { `outcome`: `LegacySwtpmMigrationOutcome` } | +| `StoreSyncResponse` | struct | [`StoreSyncResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2897) | struct { `vm`: `String`; `generation_id`: `String`; `generation_token`: `u32`; `hardlink_farm_path`: `String`; `closure_count`: `u32`; `retained_generations`: `Vec`; `swept_count`: `u32`; `cleanup_deferred`: `bool` } | +| `AckResponse` | struct | [`AckResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3051) | struct { `accepted`: `bool`; `operation`: `String` } | +| `TapReadyResponse` | struct | [`TapReadyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3058) | struct { `bridge`: `Option`; `tap`: `IfName` } | +| `ExportBrokerAuditResponse` | struct | [`ExportBrokerAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3065) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | +| `BridgePortFlagsResponse` | struct | [`BridgePortFlagsResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3110) | struct { `bridge`: `IfName`; `isolated`: `bool`; `neigh_suppress`: `bool`; `port`: `IfName` } | +| `ValidateBundleResponse` | struct | [`ValidateBundleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3119) | struct { `valid`: `bool` } | +| `ResourceActivationAuditResponse` | struct | [`ResourceActivationAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3141) | struct { `recorded`: `bool` } | +| `SignalRunnerResponse` | struct | [`SignalRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3178) | struct { `signaled`: `bool`; `vm_id`: `VmId`; `role_id`: `RoleId` } | +| `DeregisterRunnerPidfdResponse` | struct | [`DeregisterRunnerPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3214) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `removed`: `bool` } | +| `SpawnRunnerResponse` | struct | [`SpawnRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3424) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `bundle_content_identity`: `Option`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `u32`; `console_fd_index`: `Option` } | +| `ReconcileStorageScopeResponse` | struct | [`ReconcileStorageScopeResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3670) | struct { `storage_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `status`: `StorageReconcileStatus`; `applied`: `bool`; `path_hash`: `String` } | +| `ValidateLockSpecResponse` | struct | [`ValidateLockSpecResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3694) | struct { `lock_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `cloexec_required`: `bool`; `fd_passing_mechanism`: `String`; `order_key`: `String` } | +| `PollChildReapedResponse` | struct | [`PollChildReapedResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3781) | struct { `notifications`: `Vec` } | ## Per-VM lifecycle state @@ -550,29 +550,30 @@ running live guest activation. | `CutoverEffectPayload` | enum | [`CutoverEffectPayload`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L474) | `None`; `ApplyAdmission` - (CutoverAdmissionRequest); `Storage` - (ReconcileStorageScopeRequest); `ZoneStore` - (OpenZoneStoreRequest); `StoreSync` - (StoreSyncRequest); `StoreVerify` - (StoreVerifyRequest); `Verification` - (CutoverVerificationRequest); `Activation` - (RunActivationRequest); `Systemd` - (Box); `Quarantine` - struct { `staged_id`: `BundleOpId`; `source_id`: `BundleOpId`; `marker_digest`: `CanonicalAuditDigest` }; `Finalization` - struct { `artifacts`: `Vec`; `disposition_digest`: `CanonicalAuditDigest`; `consent_digest`: `CanonicalAuditDigest` }; `DestroyDurableVolume` - struct { `storage_ref`: `BundleOpId`; `marker_digest`: `CanonicalAuditDigest`; `consent_digest`: `CanonicalAuditDigest` } | | `CutoverReplayClass` | enum | [`CutoverReplayClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L553) | `Repeatable`; `ReopenByJournaledIdentity`; `QuarantineOnly` | | `CutoverEffectOutcome` | enum | [`CutoverEffectOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L625) | `Succeeded`; `Failed`; `Ambiguous` | -| `ActivationMode` | enum | [`ActivationMode`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1321) | `Switch`; `Boot`; `Test`; `Rollback` | -| `ActivationPhase` | enum | [`ActivationPhase`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1330) | `Prepare`; `Commit`; `MetadataOnly` | -| `NftablesProjectionAction` | enum | [`NftablesProjectionAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1613) | `Apply`; `Remove` | -| `BrokerAuditSeverity` | enum | [`BrokerAuditSeverity`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1816) | `Info`; `Warning`; `Error`; `Denied` | -| `GuestControlProofRole` | enum | [`GuestControlProofRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1829) | `HostProof`; `GuestProof` | -| `GuestControlDirection` | enum | [`GuestControlDirection`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1836) | `HostToGuest` | -| `GuestControlAuthPurpose` | enum | [`GuestControlAuthPurpose`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1842) | `GuestControlAuthV1` | -| `QemuMediaLifecycleAction` | enum | [`QemuMediaLifecycleAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2078) | `SystemPowerdown`; `Quit` | -| `QemuMediaVmStatus` | enum | [`QemuMediaVmStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2085) | `Running`; `Paused`; `Shutdown`; `Suspended`; `Watchdog`; `Debug`; `Inmigrate`; `InternalError`; `IoError`; `Postmigrate`; `Prelaunch`; `FinishMigrate`; `RestoreVm`; `SaveVm`; `GuestPanicked`; `Colo`; `Preconfig`; `Unknown`; `ConnectionLostDuringShutdown` | -| `QemuMediaHotplugStatus` | enum | [`QemuMediaHotplugStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2138) | `IdentityResolved`; `QmpConnected`; `QmpCapabilities`; `FdAdded`; `BlockdevAdded`; `DeviceAdded`; `DeviceDeleted`; `BlockdevDeleted`; `FdRemoved`; `VmContinued` | -| `PipeWireAudioChannel` | enum | [`PipeWireAudioChannel`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2275) | `Speaker`; `Microphone` | -| `PipeWireAudioAction` | enum | [`PipeWireAudioAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2285) | `SetGrant` - struct { `on`: `bool` }; `SetLevel` - struct { `percent`: `u8` } | -| `SystemdUnitDomain` | enum | [`SystemdUnitDomain`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2329) | `System`; `User` | -| `SystemdStopClass` | enum | [`SystemdStopClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2439) | `Drain`; `Terminate` | -| `ZoneStoreDisposition` | enum | [`ZoneStoreDisposition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2509) | `Provisioned`; `Opened` | -| `LegacySwtpmMigrationOutcome` | enum | [`LegacySwtpmMigrationOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2624) | `Migrated`; `AlreadyMigrated`; `NotApplicable`; `Pending`; `Failed`; `Ambiguous`; `AdoptionRequired`; `NeverProvisioned` | -| `RunnerSignal` | enum | [`RunnerSignal`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2957) | `Term`; `Kill`; `Quit` | -| `RunnerRole` | enum | [`RunnerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3039) | `CloudHypervisor`; `QemuMedia`; `Virtiofsd`; `Swtpm`; `SwtpmFlush`; `Gpu`; `Audio`; `Video`; `VsockRelay`; `Usbip`; `OtelHostBridge`; `WaylandProxy` | -| `RunnerAllocationKind` | enum | [`RunnerAllocationKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3200) | `VsockCid`; `TapFdSlot`; `ApiSocketPath` | -| `BrokerCallerRole` | enum | [`BrokerCallerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3342) | `AdminUid` - struct { `uid`: `u32` }; `LauncherUid` - struct { `uid`: `u32` }; `RootUid` - struct { `uid`: `u32` }; `CutoverRunner` - struct { `operation_id`: `BundleOpId`; `capability_digest`: `CanonicalAuditDigest` }; `NotAuthorized` | -| `StorageReconcileStatus` | enum | [`StorageReconcileStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3457) | `Clean`; `Created`; `Reused`; `CheckedOnly`; `TemplateUnexpanded`; `Refused` | -| `ChildExitKind` | enum | [`ChildExitKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3531) | `Exited`; `Signaled`; `Killed` | -| `BrokerNotification` | enum | [`BrokerNotification`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3569) | `ChildReaped` - (ChildReapedNotification); `Unknown` | +| `BrokerProfile` | enum | [`BrokerProfile`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1287) | `Host`; `Guest` | +| `ActivationMode` | enum | [`ActivationMode`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1515) | `Switch`; `Boot`; `Test`; `Rollback` | +| `ActivationPhase` | enum | [`ActivationPhase`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1524) | `Prepare`; `Commit`; `MetadataOnly` | +| `NftablesProjectionAction` | enum | [`NftablesProjectionAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1807) | `Apply`; `Remove` | +| `BrokerAuditSeverity` | enum | [`BrokerAuditSeverity`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2010) | `Info`; `Warning`; `Error`; `Denied` | +| `GuestControlProofRole` | enum | [`GuestControlProofRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2023) | `HostProof`; `GuestProof` | +| `GuestControlDirection` | enum | [`GuestControlDirection`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2030) | `HostToGuest` | +| `GuestControlAuthPurpose` | enum | [`GuestControlAuthPurpose`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2036) | `GuestControlAuthV1` | +| `QemuMediaLifecycleAction` | enum | [`QemuMediaLifecycleAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2272) | `SystemPowerdown`; `Quit` | +| `QemuMediaVmStatus` | enum | [`QemuMediaVmStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2279) | `Running`; `Paused`; `Shutdown`; `Suspended`; `Watchdog`; `Debug`; `Inmigrate`; `InternalError`; `IoError`; `Postmigrate`; `Prelaunch`; `FinishMigrate`; `RestoreVm`; `SaveVm`; `GuestPanicked`; `Colo`; `Preconfig`; `Unknown`; `ConnectionLostDuringShutdown` | +| `QemuMediaHotplugStatus` | enum | [`QemuMediaHotplugStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2332) | `IdentityResolved`; `QmpConnected`; `QmpCapabilities`; `FdAdded`; `BlockdevAdded`; `DeviceAdded`; `DeviceDeleted`; `BlockdevDeleted`; `FdRemoved`; `VmContinued` | +| `PipeWireAudioChannel` | enum | [`PipeWireAudioChannel`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2469) | `Speaker`; `Microphone` | +| `PipeWireAudioAction` | enum | [`PipeWireAudioAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2479) | `SetGrant` - struct { `on`: `bool` }; `SetLevel` - struct { `percent`: `u8` } | +| `SystemdUnitDomain` | enum | [`SystemdUnitDomain`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2523) | `System`; `User` | +| `SystemdStopClass` | enum | [`SystemdStopClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2633) | `Drain`; `Terminate` | +| `ZoneStoreDisposition` | enum | [`ZoneStoreDisposition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2703) | `Provisioned`; `Opened` | +| `LegacySwtpmMigrationOutcome` | enum | [`LegacySwtpmMigrationOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2818) | `Migrated`; `AlreadyMigrated`; `NotApplicable`; `Pending`; `Failed`; `Ambiguous`; `AdoptionRequired`; `NeverProvisioned` | +| `RunnerSignal` | enum | [`RunnerSignal`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3151) | `Term`; `Kill`; `Quit` | +| `RunnerRole` | enum | [`RunnerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3233) | `CloudHypervisor`; `QemuMedia`; `Virtiofsd`; `Swtpm`; `SwtpmFlush`; `Gpu`; `Audio`; `Video`; `VsockRelay`; `Usbip`; `OtelHostBridge`; `WaylandProxy` | +| `RunnerAllocationKind` | enum | [`RunnerAllocationKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3402) | `VsockCid`; `TapFdSlot`; `ApiSocketPath` | +| `BrokerCallerRole` | enum | [`BrokerCallerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3544) | `AdminUid` - struct { `uid`: `u32` }; `LauncherUid` - struct { `uid`: `u32` }; `RootUid` - struct { `uid`: `u32` }; `CutoverRunner` - struct { `operation_id`: `BundleOpId`; `capability_digest`: `CanonicalAuditDigest` }; `NotAuthorized` | +| `StorageReconcileStatus` | enum | [`StorageReconcileStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3659) | `Clean`; `Created`; `Reused`; `CheckedOnly`; `TemplateUnexpanded`; `Refused` | +| `ChildExitKind` | enum | [`ChildExitKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3733) | `Exited`; `Signaled`; `Killed` | +| `BrokerNotification` | enum | [`BrokerNotification`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3771) | `ChildReaped` - (ChildReapedNotification); `Unknown` | | `HandoffCallerRole` | enum | [`HandoffCallerRole`](../../packages/d2b-contracts-broker/src/host_generation.rs#L131) | `Lifecycle`; `Admin` | | `HandoffState` | enum | [`HandoffState`](../../packages/d2b-contracts-broker/src/host_generation.rs#L175) | `Recorded`; `Validated`; `Mutating`; `Transferred`; `Completed`; `RolledBack`; `Refused` | | `AuthDirection` | enum | [`AuthDirection`](../../packages/d2b-contracts-control/src/guest_auth.rs#L13) | `HostToGuest` | @@ -740,7 +741,7 @@ the failure class, for example `host check`, `audit`, `status`, or | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1555) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | +| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1749) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | | `HandoffError` | enum | [`HandoffError`](../../packages/d2b-contracts-broker/src/host_generation.rs#L194) | `CompatibilityFloorInvalid`; `GenerationTooOld`; `TargetFingerprintMismatch`; `TargetGenerationMismatch`; `GenerationAncestryInvalid`; `InvalidTransition` | | `GuestControlError` | struct | [`GuestControlError`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1489) | struct { `kind`: `GuestControlErrorKind`; `remediation`: `HealthRemediation`; `retry_after_ms`: `Option` } | | `GuestControlErrorKind` | enum | [`GuestControlErrorKind`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1497) | `ProtocolError`; `MaxChunkExceeded`; `StdinBackpressure`; `StdinClosed`; `StdinNotOpen`; `StdinClosedByProcess`; `StdinOffsetMismatch`; `StdinByteBudgetExhausted`; `OffsetExpired`; `OffsetInFuture`; `OffsetExhausted`; `OutputLost`; `TtyStderrUnavailable`; `TtyRequired`; `ExecCapacityExceeded`; `ExecAttachCapacityExceeded`; `ExecNotFound`; `ExecAlreadyExited`; `GuestExecDisabled`; `GuestExecRootDenied`; `GuestExecUserDenied`; `CwdInvalid`; `CwdDenied`; `RetainedLogPathUnsafe`; `RetainedLogQuotaExceeded`; `ReadWaitCapacityExceeded`; `WaitCapacityExceeded`; `SupersededReadWait`; `RateLimited`; `RequestIdConflict`; `ControlSeqMismatch`; `SlowConsumerCancelled`; `StaleSession`; `GuestControlUnavailableOldGeneration`; `AuthFailed`; `TransportUnreachable`; `ExecExpired`; `FileNotFound`; `FileTooLarge`; `PathUnsafe`; `ReadDenied`; `InvalidProgram`; `UsbipUnavailable`; `UsbipCommandFailed`; `UsbipInvalidBusId`; `UsbipInvalidHost`; `GuestShellDisabled`; `ShellInvalidName`; `ShellCapacityExceeded`; `ShellAttachCapacityExceeded`; `ShellNotFound`; `ShellAlreadyAttached`; `ShellPoolUnavailable`; `ShellDaemonEpochMismatch`; `ShellOutputGap`; `UsbipCommandTimeout`; `UsbipInvalidOutput`; `ActivationInvalidId`; `ActivationInvalidPath`; `ActivationInvalidMode`; `ActivationNotFound`; `ActivationStatusUnavailable`; `ActivationTimedOut`; `ActivationSpawnFailed`; `AudioPipeWireUnavailable`; `AudioChannelUnknown`; `AudioLevelOutOfRange`; `AudioEnforcementFailed` | diff --git a/docs/reference/desktop-wrapper.md b/docs/reference/desktop-wrapper.md index ed2b50de7f..b68f10d0fb 100644 --- a/docs/reference/desktop-wrapper.md +++ b/docs/reference/desktop-wrapper.md @@ -10,7 +10,7 @@ Every graphics-enabled VM (`d2b.vms..graphics.enable = true`) gets an auto-generated `d2b-launch-.desktop` entry installed under `share/applications/`. The wrapper script the entry's `Exec` line points at is the **daemon path** - it drives the VM through -`d2bd → d2b-priv-broker → SpawnRunner`, not the legacy bash +`d2bd → d2b-broker → SpawnRunner`, not the legacy bash `d2b guest start` / `microvm@.service` chain. ## Why a typed contract diff --git a/docs/reference/doctor.md b/docs/reference/doctor.md index 7187975be0..53f10df4e4 100644 --- a/docs/reference/doctor.md +++ b/docs/reference/doctor.md @@ -195,7 +195,7 @@ interval. **Broker replay-buffer depth**: the D7 `ChildReaped` replay-buffer depth (in-memory ring of up to 256 events, used to handle d2bd disconnect/reconnect) is **not yet observable** via a stable CLI command -(`d2b-priv-broker --report-state` is not implemented in v1.2). The +(`d2b-broker --report-state` is not implemented in v1.2). The `data.bufferDepth` field in the JSON output is always `null` for v1.2. When D7 fully lands the IPC mechanism, this field will carry the actual depth and the probe will add Warn (buffer ≥ 200 of 256) and Fail (buffer diff --git a/docs/reference/error-codes.md b/docs/reference/error-codes.md index b4a43701ba..8ee7c59f73 100644 --- a/docs/reference/error-codes.md +++ b/docs/reference/error-codes.md @@ -371,7 +371,7 @@ remediation block renders as: ``` Remediation: d2bd is not reachable. Start the daemon and re-run: - sudo systemctl start d2b-priv-broker.socket + sudo systemctl start d2b-broker.socket sudo systemctl start d2bd.service For the full operator runbook context, see: docs/how-to/migrate-d2b-v0-to-v1.md diff --git a/docs/reference/inet-d2b-chains.md b/docs/reference/inet-d2b-chains.md index 27fe7d1e73..d162d55e68 100644 --- a/docs/reference/inet-d2b-chains.md +++ b/docs/reference/inet-d2b-chains.md @@ -122,9 +122,9 @@ kebab-case discriminant and the typed inner detail. (`build_inet_d2b_chains`) - Detector: `d2b_host::nftables::detect_firewall_manager` - Matrix: `d2b_host::nftables::evaluate_coexistence_policy` -- Broker op: `packages/d2b-priv-broker/src/ops/nft.rs` +- Broker op: `packages/d2b-broker/src/ops/nft.rs` (`apply_nftables`) -- USBIP carve-out: `packages/d2b-priv-broker/src/ops/usbip_firewall.rs` +- USBIP carve-out: `packages/d2b-broker/src/ops/usbip_firewall.rs` (`bind_firewall_rule`) - Enforcing coverage: `d2b-host::nftables::tests` and - `d2b-priv-broker::ops::{nft,usbip_firewall}::tests` in `test-rust` + `d2b-broker::ops::{nft,usbip_firewall}::tests` in `test-rust` diff --git a/docs/reference/kernel-module-check.md b/docs/reference/kernel-module-check.md index 515cb39d08..9839ceb75b 100644 --- a/docs/reference/kernel-module-check.md +++ b/docs/reference/kernel-module-check.md @@ -30,7 +30,7 @@ state lock is acquired and after the pidfd table is restored, but The check does **not** invoke `modprobe`, `modinfo`, or any side-effecting helper. Mutating module load remains the broker's -responsibility (`d2b-priv-broker::ops::modprobe`). +responsibility (`d2b-broker::ops::modprobe`). If `/proc/modules` cannot be read, the check treats *every* module as absent - required modules then read as missing and the daemon diff --git a/docs/reference/manifest-schema.md b/docs/reference/manifest-schema.md index 3a6a38b14a..6d0bd30dbc 100644 --- a/docs/reference/manifest-schema.md +++ b/docs/reference/manifest-schema.md @@ -14,7 +14,7 @@ ships it as: /run/current-system/sw/share/d2b/vms.json ``` -The Rust CLI, `d2bd`, and `d2b-priv-broker` consume this public +The Rust CLI, `d2bd`, and `d2b-broker` consume this public inventory. Private bundle artifacts live beside it and are documented in [`manifest-bundle.md`](./manifest-bundle.md). diff --git a/docs/reference/naming-conventions.md b/docs/reference/naming-conventions.md index bbe46d8871..e6320a006e 100644 --- a/docs/reference/naming-conventions.md +++ b/docs/reference/naming-conventions.md @@ -7,7 +7,7 @@ Canonical reference for host-visible d2b naming. AGENTS.md and the design doc ke | Resource | Pattern | Notes | | --- | --- | --- | | Daemon | `d2bd.service` | v1.0 non-root daemon; the only persistent user-facing system unit d2b declares besides the broker (per ADR 0015). | -| Privileged broker | `d2b-priv-broker.{socket,service}` | v1.0 socket-activated privileged dispatcher for every audited host mutation. | +| Privileged broker | `d2b-broker.{socket,service}` | v1.0 socket-activated privileged dispatcher for every audited host mutation. | | Per-VM runner leaves | `d2b.slice//` | v1.0 broker-spawned runner cgroup leaves (replaces legacy `d2b--{gpu,video,snd,swtpm,store-sync}.service`). | | Per-env runner leaves | `d2b.slice/sys-/usbipd-{backend,proxy}` | v1.0 broker-spawned per-env USBIP runners (replaces legacy `d2b-sys--usbipd-{backend,proxy}.service`). | | Legacy per-VM lifecycle wrapper | `d2b@.service` | Retired in v1.0 (per ADR 0015); v1.0 lifecycle dispatches through `d2bd` → broker `SpawnRunner` instead. | diff --git a/docs/reference/privileges.md b/docs/reference/privileges.md index 3127b7a0d2..8789c8175e 100644 --- a/docs/reference/privileges.md +++ b/docs/reference/privileges.md @@ -1,7 +1,7 @@ # Reference: broker privileged operation matrix > Diataxis: reference. Stable catalog of every closed-enum operation -> the privileged broker (`d2b-priv-broker`) is allowed to perform +> the privileged broker (`d2b-broker`) is allowed to perform > on behalf of the unprivileged `d2bd` daemon. The wire-level > source of truth is `d2b_contracts::BrokerRequest`; this page is the > human-readable index keyed by operation name. @@ -26,6 +26,17 @@ Every row carries three policy flags: Unknown variants and unknown fields in security-sensitive artifacts are denied (`defaultForUnknown: deny`). +## Mode-bound broker instances + +Host and Guest authorities use the same `d2b-broker` artifact, selected at +process start as `d2b-broker host` or `d2b-broker guest`. The active profile is +not carried on the request wire and cannot be changed by a caller. + +Each authority instance has its own socket, caller UID/GID, state root, audit +root, and authority label. Guest mode exposes only the closed local-process +effect catalog; Host-only networking, device, storage, realm, cutover, and +allocator operations are refused before dispatch. + > **Authz-class vs system-group naming note.** The **Allowed groups** > column below uses the broker's **authz class** identifiers > `d2b-launcher` (singular) and `d2b-admin`, which are the @@ -413,7 +424,7 @@ mountPolicy.bindMounts = [ This section documents broker ops used by the daemon-side host-prep DAG and the daemon-side preflights that run beside them. The current daemon-only surface is `d2bd.service` + -`d2b-priv-broker.{service,socket}` + per-VM runners spawned via +`d2b-broker.{service,socket}` + per-VM runners spawned via broker `SpawnRunner`. `StoreSync` is live. `SshKeygenProbe` remains documented as future @@ -569,7 +580,7 @@ This section is the canonical legacy-systemd obituary index. The canonical surface is exactly: - `d2bd.service` (unprivileged daemon) -- `d2b-priv-broker.service` + `d2b-priv-broker.socket` +- `d2b-broker.service` + `d2b-broker.socket` (socket-activated privileged broker) - per-VM / per-role runners spawned via broker `SpawnRunner` (no systemd unit per runner; lifecycle is daemon-supervised via pidfd) @@ -616,7 +627,7 @@ is advisory until the fixture-contract lane is enabled and promoted. | Per-VM `desktopItems` generation (cli.nix `d2b-launch-`) | Daemon-native launcher module emitting `.desktop` wrappers calling `d2b guest start --apply`. | not emitted | The root-visible `systemd.services.*` declarations the framework owns -under `nixos-modules/` are `d2bd`, `d2b-priv-broker`, +under `nixos-modules/` are `d2bd`, `d2b-broker`, `d2b-load-store-db` (boot-time tmpfiles helper), and `d2b-load-host-keys` (boot-time helper). The observability backend declares native services inside the auto-declared `sys-obs` VM, not @@ -759,7 +770,7 @@ capabilities. `CAP_SYS_PTRACE` is explicitly excluded. ### Socket-activation contract -`d2b-priv-broker.socket` is socket-activated. systemd creates, +`d2b-broker.socket` is socket-activated. systemd creates, binds, listens on, and applies the ACL to the socket before the broker process starts. The broker adopts the fd via `SD_LISTEN_FDS`. @@ -773,7 +784,7 @@ process starts. The broker adopts the fd via `SD_LISTEN_FDS`. - The broker calls `sd_notify(READY=1)` only after the listener fd is adopted and the audit log is open - the systemd `notify` service type guarantees daemon readiness. -- `d2bd.service` carries `Wants=d2b-priv-broker.socket` (not +- `d2bd.service` carries `Wants=d2b-broker.socket` (not `Requires=`) so the daemon can serve even when the broker has idled. - The socket is the **broker private socket** at `/run/d2b/priv.sock`, owned `root:d2bd` with mode @@ -889,4 +900,4 @@ opened operation-owned directory. ## Related ADRs -- [ADR 0015: daemon-only clean break](../adr/0015-daemon-only-clean-break.md) - the architectural decision record that defines the daemon-only root surface of `d2bd` + `d2b-priv-broker`. +- [ADR 0015: daemon-only clean break](../adr/0015-daemon-only-clean-break.md) - the architectural decision record that defines the daemon-only root surface of `d2bd` + `d2b-broker`. diff --git a/docs/reference/provider-capability-matrix.md b/docs/reference/provider-capability-matrix.md index 290b01c2d9..d2cc6613ad 100644 --- a/docs/reference/provider-capability-matrix.md +++ b/docs/reference/provider-capability-matrix.md @@ -16,7 +16,7 @@ For display and virtual I/O capabilities beyond console and audio, see | Provider | Identity | Guest-control channel | | --- | --- | --- | -| Cloud Hypervisor NixOS | Local VM managed by `d2bd` + `d2b-priv-broker` | `guestd` over authenticated vsock guest-control | +| Cloud Hypervisor NixOS | Local VM managed by `d2bd` + `d2b-broker` | `guestd` over authenticated vsock guest-control | | qemu-media | Dedicated media/console workload; no `guestd` | None | | ACA sandbox | Provider-managed workload (Azure Container Apps) | guestd-compatible in-sandbox agent over ADR 0032 relay/peer transport | diff --git a/docs/reference/provider-managed-sandboxes.md b/docs/reference/provider-managed-sandboxes.md index e5349f37b5..abbe20b371 100644 --- a/docs/reference/provider-managed-sandboxes.md +++ b/docs/reference/provider-managed-sandboxes.md @@ -21,11 +21,11 @@ guest-control), see [remote full-host nodes](./remote-full-host-nodes.md). A provider-managed sandbox is a named workload target in a realm whose lifecycle (create, start, stop, destroy) is owned by an external provider -API rather than by a `d2b-priv-broker` running on a locally managed +API rather than by a `d2b-broker` running on a locally managed host. From the daemon's perspective it is a node with a bounded positive capability set derived from what that provider API or provider-side d2b agent supports. The daemon never provisions, registers, or expects a full -host `d2bd`, `d2b-priv-broker`, KVM subsystem, vsock channel, cgroup +host `d2bd`, `d2b-broker`, KVM subsystem, vsock channel, cgroup subtree, namespace hierarchy, full-host lifecycle, or device-hotplug surface on a provider-managed node. ADR 0039 defines one exception to the old exec-only model: a provider-managed sandbox may advertise persistent shell @@ -40,7 +40,7 @@ key differences: | Dimension | Provider-managed sandbox | Remote full-host node | | --- | --- | --- | -| Who owns lifecycle | Cloud provider API | Remote `d2bd` + `d2b-priv-broker` | +| Who owns lifecycle | Cloud provider API | Remote `d2bd` + `d2b-broker` | | Broker presence | None | Full broker on the remote host | | Guest-control / vsock | No vsock or raw guest-control tunnel; persistent-shell-capable sandboxes require a guestd-compatible agent over constellation peer transport. | Present | | KVM / hypervisor | Absent | Present | @@ -149,7 +149,7 @@ are never routed through this adapter. Requests for them fail closed with `UnsupportedFeature` or `CapabilityDenied`; there are no fallbacks. - **No broker operation forwarding.** The adapter never forwards raw - `d2b-priv-broker` frames to the container runtime. + `d2b-broker` frames to the container runtime. - **No raw guest-control or vsock frames.** The current executeShellCommand-only Azure Container Apps adapter has no guestd instance and no vsock channel to attach or tunnel. Future persistent-shell-capable diff --git a/docs/reference/realm-controller-config.md b/docs/reference/realm-controller-config.md index 6f58e8ecfd..72f33c9d76 100644 --- a/docs/reference/realm-controller-config.md +++ b/docs/reference/realm-controller-config.md @@ -30,7 +30,7 @@ The current invariants are: | --- | --- | | `metadataOnly` | The file is descriptive metadata, not a runtime activation command. | | `noSystemdUnitsMaterialized` | Legacy double-negative kept for the v2 schema: `true` means every controller row is metadata-only with no emitted systemd unit/socket; `false` means at least one host-local realm materializes daemon/broker units or sockets. | -| `preservesGlobalDaemonBehavior` | Existing `d2bd.service`, `d2b-priv-broker.socket`, and `d2b-priv-broker.service` behavior is unchanged. | +| `preservesGlobalDaemonBehavior` | Existing `d2bd.service`, `d2b-broker.socket`, and `d2b-broker.service` behavior is unchanged. | | `preservesDirectUnixSocketSemantics` | Future realm clients are expected to authenticate to the owning realm socket directly, not through a host byte proxy. | ## Deterministic names diff --git a/docs/reference/remote-full-host-nodes.md b/docs/reference/remote-full-host-nodes.md index d6c3616b48..d6f931d428 100644 --- a/docs/reference/remote-full-host-nodes.md +++ b/docs/reference/remote-full-host-nodes.md @@ -23,7 +23,7 @@ substrate capabilities see [host substrate providers](./host-substrate-providers The architectural rationale is in [ADR 0032](../adr/0032-d2b-v2-constellation-control-plane.md). For provider-managed sandboxes - nodes whose lifecycle is owned by a -cloud provider API rather than by a locally managed `d2b-priv-broker` - +cloud provider API rather than by a locally managed `d2b-broker` - see [provider-managed sandboxes](./provider-managed-sandboxes.md). --- @@ -31,7 +31,7 @@ see [provider-managed sandboxes](./provider-managed-sandboxes.md). ## What a remote full-host node is A remote full-host node is a host running its own `d2bd`, -`d2b-priv-broker`, and guest-control stack that a gateway guest +`d2b-broker`, and guest-control stack that a gateway guest can reach through a transport peer session. From the gateway's point of view, the remote host appears as a named `NodeId` in a realm with a declared capability set. All lifecycle, broker, and guest-control @@ -337,12 +337,12 @@ does not implement fallbacks or workarounds. | Surface | Boundary | | --- | --- | -| Raw broker operation forwarding | The gateway never forwards raw `d2b-priv-broker` frames. All broker work stays on the remote host. | +| Raw broker operation forwarding | The gateway never forwards raw `d2b-broker` frames. All broker work stays on the remote host. | | Guest-control frame tunneling | Guest-control (vsock) frames are not proxied through the gateway. Persistent shell routes as [ADR 0039](../adr/0039-constellation-persistent-shell-routing.md) semantic `Shell*` operations; the remote `d2bd` opens its own guest-control sessions near the guest. | | Pidfd / fd forwarding | File descriptors, pidfds, and socket handles are never sent across the transport session. | | Host path and endpoint exposure | Host-local paths, socket addresses, runner argv, and endpoint strings are not visible in the operation envelope or in gateway audit records. | | Provider/relay credential forwarding | Transport and realm credentials remain in the layer that owns them and are never placed in operation payloads. | -| Remote host install / host prepare | Registration assumes the remote host is already running a compatible `d2bd`/`d2b-priv-broker` stack. Host installation and host preparation are out of scope for this adapter. | +| Remote host install / host prepare | Registration assumes the remote host is already running a compatible `d2bd`/`d2b-broker` stack. Host installation and host preparation are out of scope for this adapter. | | Production WAN transports | The preview is validated with mock and loopback peer clients only. Azure Relay over a live WAN, QUIC, and SSH are not yet connected. See [transport support policy](./transport-support-policy.md). | | Network mutation | The adapter does not configure routing, firewall rules, or overlays on the remote host or on the gateway network. | | SSH fallback | There is no implicit SSH fallback when the peer session transport is unavailable. Callers receive a typed `GatewayUnavailable` / transport-layer refusal. | diff --git a/docs/reference/runtime-provider-selection.md b/docs/reference/runtime-provider-selection.md index 5ab5de4282..093cea08b5 100644 --- a/docs/reference/runtime-provider-selection.md +++ b/docs/reference/runtime-provider-selection.md @@ -4,7 +4,7 @@ Runtime providers describe the local VM runner family used for a workload. They are capability declarations and routing policy, not -escape hatches around `d2bd` or `d2b-priv-broker`. +escape hatches around `d2bd` or `d2b-broker`. ## Default provider diff --git a/docs/reference/store-sync.md b/docs/reference/store-sync.md index c5410550d1..58b1216bf7 100644 --- a/docs/reference/store-sync.md +++ b/docs/reference/store-sync.md @@ -221,7 +221,7 @@ positive-allow-list projection to a dedicated export file: written `0640`, `O_APPEND`, one JSON object per line, daily-rotated by UTC date. The projection is a dedicated `StoreSyncObservabilityRecord` struct -(`packages/d2b-priv-broker/src/ops/store_sync_export.rs`) built by +(`packages/d2b-broker/src/ops/store_sync_export.rs`) built by `from_audit_fields()`, which reads **only** the allow-listed fields - so no serializer ever receives the full audit struct and host-only fields cannot leak by construction (`#[serde(deny_unknown_fields)]` + an @@ -380,11 +380,11 @@ asserts `len() == 0`. ## Implementation file map -- `packages/d2b-priv-broker/src/ops/store_sync.rs` - pure +- `packages/d2b-broker/src/ops/store_sync.rs` - pure handler (`run_store_sync`) + typed `StoreSyncError`. Derives the `generation_id`, materialises via `build_store_view_cross_mount_safe`, and publishes (`state/current`, `meta/current`, live marker). -- `packages/d2b-priv-broker/src/ops/store_view_farm.rs` - +- `packages/d2b-broker/src/ops/store_view_farm.rs` - cross-mount-safe wrappers (`build_store_view_cross_mount_safe`) that retry the build/replace in a private mount namespace when `/nix/store` is a separate vfsmount. The broker execs the activation helper directly; no shell @@ -397,7 +397,7 @@ asserts `len() == 0`. the `private-store ` entrypoint that unshares the mount namespace, makes propagation private, lazily detaches `/nix/store`, and runs `build-store-view` / `replace-store-view-live` from stdin JSON. -- `packages/d2b-priv-broker/src/runtime.rs` - wire dispatch +- `packages/d2b-broker/src/runtime.rs` - wire dispatch arm (`RealBrokerRequest::StoreSync(req) => …`). - `packages/d2b-contracts-broker/src/broker_wire.rs` - typed request/ response structs + enum variants. The wire carries both the @@ -406,12 +406,12 @@ asserts `len() == 0`. only and is never the on-disk key. - `packages/d2b-contracts/src/types.rs` - `BundleClosureRef` opaque newtype. -- `packages/d2b-priv-broker/src/ops/audit_op.rs` - +- `packages/d2b-broker/src/ops/audit_op.rs` - `OperationFields::StoreSync` newtype over `StoreSyncAuditFields`. -- `packages/d2b-priv-broker/src/ops/store_sync_audit.rs` - +- `packages/d2b-broker/src/ops/store_sync_audit.rs` - the signed `StoreSyncAuditFields` terminal audit schema, its enums, invariant-enforcing constructors, and `validate()`. -- `packages/d2b-priv-broker/src/ops/store_verify.rs` - explicit +- `packages/d2b-broker/src/ops/store_verify.rs` - explicit StoreVerify broker op, top-level live-pool verifier, and host-only integrity record writer. diff --git a/docs/reference/tap-dag-contract.md b/docs/reference/tap-dag-contract.md index b73d7e59f6..8c5ac93ec4 100644 --- a/docs/reference/tap-dag-contract.md +++ b/docs/reference/tap-dag-contract.md @@ -82,7 +82,7 @@ apply-nftables-rules ---> bring-up-tap-interface ---> pre-open-vhost-net-fd race where packets traverse the bridge before the firewall policy is in place. 2. `ApplyNmUnmanaged` (host-wide, not per-VM) - enforced inside - the broker handler via [`TapCreateGate`](../../packages/d2b-priv-broker/src/ops/tap.rs). + the broker handler via [`TapCreateGate`](../../packages/d2b-broker/src/ops/tap.rs). The broker REFUSES `CreateTapFd` / `CreatePersistentTap` unless the prior `ApplyNmUnmanaged` op recorded either `NmUnmanagedOutcome::Applied` or @@ -273,7 +273,7 @@ ownership marker so `ApplyNmUnmanaged` can succeed), the next - **Host-prep DAG**: [host-prep-dag.md](./host-prep-dag.md) - parent contract for the per-VM DAG this step lives in. - **Bridge port flags**: [`packages/d2b-host/src/bridge_port.rs`](../../packages/d2b-host/src/bridge_port.rs). -- **Tap broker handlers**: [`packages/d2b-priv-broker/src/ops/tap.rs`](../../packages/d2b-priv-broker/src/ops/tap.rs). +- **Tap broker handlers**: [`packages/d2b-broker/src/ops/tap.rs`](../../packages/d2b-broker/src/ops/tap.rs). - **Derived ifname emitter**: [`packages/d2b-host/src/ifname.rs`](../../packages/d2b-host/src/ifname.rs). - **Host config DTO**: [`packages/d2b-core/src/host.rs`](../../packages/d2b-core/src/host.rs) - `ChNetHandoffMode`, `IfNameMapping`, `HostChConfig`. - **Failure envelope**: [`packages/d2b-host/src/host_prep_dag.rs`](../../packages/d2b-host/src/host_prep_dag.rs) - `HostPrepStepFailed`. diff --git a/docs/reference/tracing-contract.md b/docs/reference/tracing-contract.md index 78f88c4d25..0237b4ab63 100644 --- a/docs/reference/tracing-contract.md +++ b/docs/reference/tracing-contract.md @@ -6,7 +6,7 @@ > and promoted. The d2b daemon (`d2bd`) and the privileged broker -(`d2b-priv-broker`) both emit OpenTelemetry spans and structured +(`d2b-broker`) both emit OpenTelemetry spans and structured `tracing` events. Those spans flow to the native SigNoz backend through the `OtelHostBridge` broker role and the `sys-obs` collector. To keep the backend's ClickHouse cardinality budget bounded **and** to prevent @@ -96,7 +96,7 @@ than emitting the path itself. ### Broker bundle-load span (canonical reference) -Before (`packages/d2b-priv-broker/src/runtime.rs`, pre-`b6f4ac9`): +Before (`packages/d2b-broker/src/runtime.rs`, pre-`b6f4ac9`): ```rust tracing::info!( diff --git a/docs/reference/wave6-foundation-manifest.json b/docs/reference/wave6-foundation-manifest.json index 4dc079f70e..625be07a68 100644 --- a/docs/reference/wave6-foundation-manifest.json +++ b/docs/reference/wave6-foundation-manifest.json @@ -868,7 +868,7 @@ "owner": "provider.device-tpm", "status": "Landed", "implementationPath": "packages/d2b-provider-device-tpm", - "validationProof": "`tests/effect_fake.rs` proves migration precedes ensure and carries no path/uid/gid/mode/command; static proof that non-test files do not import `d2b_priv_broker`", + "validationProof": "`tests/effect_fake.rs` proves migration precedes ensure and carries no path/uid/gid/mode/command; static proof that non-test files do not import `d2b_broker`", "removalProof": "Direct broker references in controller/daemon TPM paths are superseded by the effect-port/resource-provider boundary; final deletion is ADR046-device-tpm-013", "sourceState": "Planned" }, @@ -1814,8 +1814,8 @@ "owner": "provider.device-security-key", "status": "Landed", "implementationPath": "packages/d2b-provider-device-security-key", - "validationProof": "`packages/d2b-priv-broker/tests/security_key_broker.rs` updates for bundle table lookup and zone-field round trip; `device_grant_no_path.rs` proves Provider code does not call the broker and sees no device path; audit tests prove path-free grant records.", - "removalProof": "The superseded iterative sysfs scan/fallback behavior in `packages/d2b-priv-broker/src/ops/security_key.rs` is removed once bundle-token lookup and revalidation tests pass.", + "validationProof": "`packages/d2b-broker/tests/security_key_broker.rs` updates for bundle table lookup and zone-field round trip; `device_grant_no_path.rs` proves Provider code does not call the broker and sees no device path; audit tests prove path-free grant records.", + "removalProof": "The superseded iterative sysfs scan/fallback behavior in `packages/d2b-broker/src/ops/security_key.rs` is removed once bundle-token lookup and revalidation tests pass.", "sourceState": "Planned" }, { @@ -2661,7 +2661,7 @@ "owner": "provider.volume-local", "status": "Landed", "implementationPath": "packages/d2b-provider-volume-local", - "validationProof": "All `tests/marker.rs`, `tests/state.rs`, and `integration/provision.rs` scenarios; dependency/source checks prove no `d2b-priv-broker`/`d2bd`, syscall binding, command execution, raw host-path type, audit writer, or Provider-local descriptor in the Provider crate", + "validationProof": "All `tests/marker.rs`, `tests/state.rs`, and `integration/provision.rs` scenarios; dependency/source checks prove no `d2b-broker`/`d2bd`, syscall binding, command execution, raw host-path type, audit writer, or Provider-local descriptor in the Provider crate", "removalProof": "`swtpm_dir.rs` marker implementation retired only after device-tpm Provider Volume is live and marker-check parity is confirmed", "sourceState": "Planned" }, @@ -2673,7 +2673,7 @@ "status": "Landed", "implementationPath": "packages/d2b-provider-volume-local", "validationProof": "`tests/layout_provision.rs`, `tests/layout_repair.rs`, `tests/layout_adopt.rs`, `tests/acl.rs`, `tests/view_rights.rs`, `tests/source.rs`, `integration/provision.rs`", - "removalProof": "`d2b-priv-broker/src/ops/storage_contract.rs` `reconcile_storage_scope` retired only after Volume controller parity confirmed", + "removalProof": "`d2b-broker/src/ops/storage_contract.rs` `reconcile_storage_scope` retired only after Volume controller parity confirmed", "sourceState": "Planned" }, { @@ -2684,7 +2684,7 @@ "status": "Landed", "implementationPath": "packages/d2b-provider-volume-local", "validationProof": "`tests/store_view.rs` all invariants; `integration/store_view.rs` same-filesystem boundary; private-NS sync with concurrent reader", - "removalProof": "`packages/d2b-provider-volume-local/nix/store.nix` activation and `d2b-priv-broker/src/ops/store_sync.rs` retired only after store-view Volume controller is live and passes all parity tests", + "removalProof": "`packages/d2b-provider-volume-local/nix/store.nix` activation and `d2b-broker/src/ops/store_sync.rs` retired only after store-view Volume controller is live and passes all parity tests", "sourceState": "Planned" }, { @@ -2695,7 +2695,7 @@ "status": "Landed", "implementationPath": "packages/d2b-provider-volume-local", "validationProof": "`tests/swtpm_volume.rs` all scenarios; `integration/swtpm_marker.rs` real broker-maintained marker", - "removalProof": "`d2b-priv-broker/src/ops/swtpm_dir.rs` retired only after device-tpm Provider TPM Volume is live and fail-closed tests pass", + "removalProof": "`d2b-broker/src/ops/swtpm_dir.rs` retired only after device-tpm Provider TPM Volume is live and fail-closed tests pass", "sourceState": "Planned" }, { diff --git a/examples/graphics-workstation/README.md b/examples/graphics-workstation/README.md index 508f7dd04f..a0baeb4ad9 100644 --- a/examples/graphics-workstation/README.md +++ b/examples/graphics-workstation/README.md @@ -229,7 +229,7 @@ implicitly by the host activation script) produces: bridge - workload VMs cannot reach the host's neighbours. - **Root-visible services:** `d2bd.service`, - `d2b-priv-broker.socket`, and `d2b-priv-broker.service`. + `d2b-broker.socket`, and `d2b-broker.service`. Per-VM runners (cloud-hypervisor, virtiofsd, swtpm, GPU/audio/USBIP sidecars) are broker-spawned and supervised by `d2bd`, not by per-VM systemd templates. diff --git a/flake.nix b/flake.nix index d440203732..b7b3031b0f 100644 --- a/flake.nix +++ b/flake.nix @@ -530,7 +530,7 @@ # userland - e.g. proving a static d2b binary runs on stock Ubuntu. # It deliberately does NOT boot systemd for daemon/socket activation; # that is covered natively by - # packages/d2b-priv-broker/tests/socket_activation.rs plus nix-unit. + # packages/d2b-broker/tests/socket_activation.rs plus nix-unit. # See tests/integration/containers/README.md. # # Auto-discovered from tests/integration/containers/images/*.nix: each image module is @@ -1057,7 +1057,7 @@ } // args); rustToolchainChannel = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml)).toolchain.channel; - brokerManifestToml = builtins.fromTOML (builtins.readFile ./packages/d2b-priv-broker/Cargo.toml); + brokerManifestToml = builtins.fromTOML (builtins.readFile ./packages/d2b-broker/Cargo.toml); mainManifestToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); assertRustToolchain = '' rustc --version | grep -F "${rustToolchainChannel}" diff --git a/labs/wlattach/deny.toml b/labs/wlattach/deny.toml index 34c4109d33..a4267083b9 100644 --- a/labs/wlattach/deny.toml +++ b/labs/wlattach/deny.toml @@ -1,5 +1,5 @@ # Standalone supply-chain config, wired into the per-manifest `cargo deny` loop -# in tests/test-rust.sh alongside d2b-priv-broker and d2b-guest-shell-runner. +# in tests/test-rust.sh alongside d2b-broker and d2b-guest-shell-runner. [advisories] version = 2 yanked = "deny" diff --git a/labs/wlattach/src/serve/sys.rs b/labs/wlattach/src/serve/sys.rs index be9bd9d333..a4136e1718 100644 --- a/labs/wlattach/src/serve/sys.rs +++ b/labs/wlattach/src/serve/sys.rs @@ -1,6 +1,6 @@ //! The crate's **only** `unsafe` code. //! -//! Quarantined here deliberately, following the `d2b-priv-broker` precedent of +//! Quarantined here deliberately, following the `d2b-broker` precedent of //! `unsafe_code = "deny"` plus one audited module, so every unsafe expression in //! the prototype is reviewable in a single short file. //! diff --git a/nix/prebuilt.json b/nix/prebuilt.json index 85e5a88caa..d36c17bb94 100644 --- a/nix/prebuilt.json +++ b/nix/prebuilt.json @@ -10,9 +10,10 @@ "url": "https://github.com/vicondoa/d2b/releases/download/v1.4.1/d2b-activation-helper-1.4.1-x86_64-linux.tar.gz", "hash": "sha256-krP84+rfDOi/7EghxtPqjIWMx9Klryz9JLFMPZnGV8M=" }, - "d2b-priv-broker": { + "d2b-broker": { "url": "https://github.com/vicondoa/d2b/releases/download/v1.4.1/d2b-priv-broker-1.4.1-x86_64-linux.tar.gz", - "hash": "sha256-qY5eXFpenVG6UrgwwV4nQC+QQxFr3O+mJHVdOSj256A=" + "hash": "sha256-qY5eXFpenVG6UrgwwV4nQC+QQxFr3O+mJHVdOSj256A=", + "sourceBinary": "d2b-priv-broker" }, "d2b-unsafe-local-helper": { "url": "https://github.com/vicondoa/d2b/releases/download/v1.4.1/d2b-unsafe-local-helper-1.4.1-x86_64-linux.tar.gz", diff --git a/nix/prebuilt.nix b/nix/prebuilt.nix index 1cc9fc8994..4fe1044ea2 100644 --- a/nix/prebuilt.nix +++ b/nix/prebuilt.nix @@ -3,6 +3,8 @@ # Reads nix/prebuilt.json and provides pre-built binary derivations. # Uses autoPatchelfHook to fix library paths for the consumer's nixpkgs. # Returns null when no release is available (callers fall back to source). +# A manifest entry may set `sourceBinary` when a released asset still carries +# a legacy executable name that the consumer package must rename. let manifest = builtins.fromJSON (builtins.readFile ./prebuilt.json); @@ -11,6 +13,24 @@ let && builtins.length (builtins.attrNames manifest.binaries) > 0; mkPrebuilt = name: spec: + let + sourceBinary = spec.sourceBinary or null; + installBinaries = + if sourceBinary != null then '' + candidate=./${sourceBinary} + if [ ! -f "$candidate" ] || [ ! -x "$candidate" ]; then + echo "prebuilt ${name}: expected executable ${sourceBinary}" >&2 + exit 1 + fi + install -Dm755 "$candidate" "$out/bin/${name}" + '' else '' + for candidate in ./*; do + if [ -f "$candidate" ] && [ -x "$candidate" ]; then + install -Dm755 "$candidate" "$out/bin/$(basename "$candidate")" + fi + done + ''; + in pkgs.stdenv.mkDerivation { pname = name; version = manifest.version; @@ -19,6 +39,7 @@ let }; nativeBuildInputs = [ pkgs.autoPatchelfHook ]; buildInputs = [ pkgs.stdenv.cc.cc.lib ]; + passthru = { inherit sourceBinary; }; dontConfigure = true; dontBuild = true; unpackPhase = '' @@ -29,11 +50,7 @@ let installPhase = '' runHook preInstall mkdir -p "$out/bin" - for candidate in ./*; do - if [ -f "$candidate" ] && [ -x "$candidate" ]; then - install -Dm755 "$candidate" "$out/bin/$(basename "$candidate")" - fi - done + ${installBinaries} runHook postInstall ''; meta.platforms = [ manifest.system ]; diff --git a/nixos-modules/default.nix b/nixos-modules/default.nix index 84325d72c6..558121111f 100644 --- a/nixos-modules/default.nix +++ b/nixos-modules/default.nix @@ -56,7 +56,7 @@ in # host-otel-relay-acl.nix retired per ADR 0018. # The OTel host-bridge + per-VM relay ACL contract moved into the # broker pre-spawn pipeline (`SpawnRunner{role: OtelHostBridge}` - # in `packages/d2b-priv-broker/src/runtime.rs`). The retired + # in `packages/d2b-broker/src/runtime.rs`). The retired # module file is kept as a stub for one release for diff # readability; consumers should not import it directly. A future # commit deletes the stub file outright. diff --git a/nixos-modules/guest-broker.nix b/nixos-modules/guest-broker.nix new file mode 100644 index 0000000000..ee771180c4 --- /dev/null +++ b/nixos-modules/guest-broker.nix @@ -0,0 +1,112 @@ +# d2b-broker guest profile +# +# Every workload Guest gets a separate socket-activated broker instance. The +# executable is the same host-tool derivation used by the Host profile; only +# the process-start profile and authority roots differ. +{ config, lib, pkgs, name, d2bHostTools, d2bUsePrebuiltHostTools ? false, ... }: + +let + prebuilt = + if d2bUsePrebuiltHostTools + then import ./prebuilt-packages.nix { inherit pkgs lib; } + else { }; + brokerSourcePackage = d2bHostTools.broker; + brokerPackage = + if prebuilt ? "selectPackage" + then prebuilt.selectPackage "d2b-broker" brokerSourcePackage + else brokerSourcePackage; + brokerSocket = "/run/d2b/guest-broker.sock"; + brokerState = "/var/lib/d2b/guest-broker"; + brokerAudit = "/var/lib/d2b/guest-audit"; + brokerBundle = "/etc/d2b/guest-bundle.json"; + brokerUid = config.users.users.d2bd.uid or 997; + brokerGid = config.users.groups.d2bd.gid or 997; +in +{ + users.groups.d2bd = { + gid = lib.mkDefault 997; + }; + users.users.d2bd = { + isSystemUser = true; + group = "d2bd"; + uid = lib.mkDefault 997; + }; + + environment.systemPackages = [ brokerPackage ]; + + systemd.tmpfiles.rules = [ + "d /run/d2b 0750 root d2bd -" + "d ${brokerState} 0700 root d2bd -" + "d ${brokerAudit} 0750 root d2bd -" + ]; + + systemd.sockets.d2b-broker-guest = { + description = "d2b Guest privileged broker socket"; + wantedBy = [ "sockets.target" ]; + requires = [ "systemd-tmpfiles-setup.service" ]; + after = [ "systemd-tmpfiles-setup.service" ]; + socketConfig = { + ListenSequentialPacket = brokerSocket; + SocketUser = "root"; + SocketGroup = "d2bd"; + SocketMode = "0660"; + Accept = false; + FileDescriptorName = "priv.sock"; + }; + }; + + systemd.services.d2b-broker-guest = { + description = "d2b Guest privileged broker"; + requires = [ + "d2b-broker-guest.socket" + "systemd-tmpfiles-setup.service" + ]; + after = [ + "d2b-broker-guest.socket" + "systemd-tmpfiles-setup.service" + "local-fs.target" + ]; + environment = { + RUST_LOG = lib.mkDefault "info"; + }; + serviceConfig = { + Type = "notify"; + NotifyAccess = "main"; + User = "root"; + Group = "d2bd"; + CapabilityBoundingSet = [ + "CAP_DAC_OVERRIDE" + "CAP_DAC_READ_SEARCH" + "CAP_FOWNER" + "CAP_KILL" + "CAP_SETGID" + "CAP_SETUID" + "CAP_SYS_ADMIN" + "CAP_SYS_RESOURCE" + ]; + AmbientCapabilities = [ "" ]; + NoNewPrivileges = false; + KillMode = "process"; + PrivateTmp = true; + ProtectHome = true; + ProtectClock = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ "AF_UNIX" "AF_VSOCK" ]; + SystemCallArchitectures = "native"; + UMask = "0027"; + ExecStart = + "${brokerPackage}/bin/d2b-broker guest " + + "--authority-id guest-${name} " + + "--audit-dir ${brokerAudit} " + + "--bundle-path ${brokerBundle} " + + "--state-dir ${brokerState} " + + "--d2bd-uid ${toString brokerUid} " + + "--d2bd-gid ${toString brokerGid}"; + Restart = "on-failure"; + RestartSec = "2s"; + StandardOutput = "journal"; + StandardError = "journal"; + SyslogIdentifier = "d2b-broker-guest"; + }; + }; +} diff --git a/nixos-modules/host-broker.nix b/nixos-modules/host-broker.nix index 16b24cc15c..499c9f82f7 100644 --- a/nixos-modules/host-broker.nix +++ b/nixos-modules/host-broker.nix @@ -1,4 +1,4 @@ -# d2b-priv-broker.{socket,service} +# d2b-broker.{socket,service} # # Socket-activated privileged broker per ADR 0001 (`unsafe_code = # "deny"` quarantine boundary). Daemon connects to /run/d2b/priv.sock @@ -27,7 +27,10 @@ let brokerSourcePackage = d2bHostTools.broker.overrideAttrs (_: { meta.description = "d2b privileged broker (uid 0 host-mutation surface)"; }); - brokerPackage = if prebuilt ? "d2b-priv-broker" then prebuilt."d2b-priv-broker" else brokerSourcePackage; + brokerPackage = + if prebuilt ? "selectPackage" + then prebuilt.selectPackage "d2b-broker" brokerSourcePackage + else brokerSourcePackage; cutoverRunnerSourcePackage = d2bHostTools.cutoverRunner; cutoverRunnerPackage = if prebuilt ? "d2b-cutover-runner" @@ -133,7 +136,8 @@ let SystemCallArchitectures = "native"; UMask = "0027"; ExecStart = - "${brokerPackage}/bin/d2b-priv-broker serve " + + "${brokerPackage}/bin/d2b-broker host " + + "--authority-id realm-${realm.id} " + "--audit-dir ${realm.controller.broker.auditDir} " + "--audit-retention-days ${toString auditRetentionDays} " + "--bundle-path ${bundleManifestPath} " + @@ -146,7 +150,7 @@ let RestartSec = "2s"; StandardOutput = "journal"; StandardError = "journal"; - SyslogIdentifier = "d2b-realm-priv-broker"; + SyslogIdentifier = "d2b-realm-broker"; }; }; realmBrokerSockets = lib.listToAttrs (map @@ -216,7 +220,7 @@ in # via SD_LISTEN_FDS=1 LISTEN_FDS=1 LISTEN_FDNAMES=priv.sock and # MUST NOT bind/listen itself when activated this way. systemd.sockets = { - d2b-priv-broker = { + d2b-broker = { description = "d2b privileged broker socket"; wantedBy = [ "sockets.target" ]; requires = [ "systemd-tmpfiles-setup.service" ]; @@ -233,7 +237,7 @@ in } // realmBrokerSockets; systemd.services = { - d2b-priv-broker = { + d2b-broker = { description = "d2b privileged broker (uid 0 host-mutation surface)"; documentation = [ "https://github.com/vicondoa/d2b/blob/main/docs/adr/0002-non-root-daemon-and-privileged-broker.md" @@ -242,11 +246,11 @@ in # Socket-activated; service activation comes from the socket unit. # No wantedBy here. requires = [ - "d2b-priv-broker.socket" + "d2b-broker.socket" "systemd-tmpfiles-setup.service" ]; after = [ - "d2b-priv-broker.socket" + "d2b-broker.socket" "systemd-tmpfiles-setup.service" "local-fs.target" ]; @@ -279,7 +283,7 @@ in # Type=notify so the daemon can deterministically observe # READY=1 after the broker has adopted the listen fd and # completed cgroup delegation. Pair with sd_notify(READY=1) - # in packages/d2b-priv-broker/src/runtime.rs after the + # in packages/d2b-broker/src/runtime.rs after the # SD_LISTEN_FDS adoption + cgroup delegation sequence. # Type = "notify"; @@ -378,11 +382,11 @@ in # must be optional for ExecStartPre: systemd loads EnvironmentFile # before every command in the service, and ExecStartPre is what # creates the file before ExecStart consumes it. - EnvironmentFile = "-/run/d2b/broker/priv-broker.env"; - ExecStartPre = "+${pkgs.writeShellScript "d2b-priv-broker-prep" '' + EnvironmentFile = "-/run/d2b/broker/broker.env"; + ExecStartPre = "+${pkgs.writeShellScript "d2b-broker-prep" '' set -euo pipefail - env_file=/run/d2b/broker/priv-broker.env - env_tmp=/run/d2b/broker/priv-broker.env.new + env_file=/run/d2b/broker/broker.env + env_tmp=/run/d2b/broker/broker.env.new uid=$(${pkgs.coreutils}/bin/id -u d2bd) gid=$(${pkgs.coreutils}/bin/id -g d2bd) umask 0077 @@ -399,7 +403,8 @@ in # MUST adopt the inherited fd; the --socket-path flag is the # non-activated-mode fallback only. ExecStart = - "${brokerPackage}/bin/d2b-priv-broker serve " + + "${brokerPackage}/bin/d2b-broker host " + + "--authority-id host " + "--audit-dir /var/lib/d2b/audit " + "--audit-retention-days ${toString auditRetentionDays} " + "--bundle-path ${bundleManifestPath} " + @@ -412,7 +417,7 @@ in StandardOutput = "journal"; StandardError = "journal"; - SyslogIdentifier = "d2b-priv-broker"; + SyslogIdentifier = "d2b-broker"; }; }; @@ -432,8 +437,8 @@ in # `daemonExperimental.enable = false` drops the daemon config), # since systemd merges these at the unit-file level. d2bd = { - wants = [ "d2b-priv-broker.socket" ]; - after = [ "d2b-priv-broker.socket" ]; + wants = [ "d2b-broker.socket" ]; + after = [ "d2b-broker.socket" ]; }; } // realmBrokerServices; }; diff --git a/nixos-modules/host-daemon.nix b/nixos-modules/host-daemon.nix index 61ac47ddcf..5daeb22c33 100644 --- a/nixos-modules/host-daemon.nix +++ b/nixos-modules/host-daemon.nix @@ -221,14 +221,14 @@ let wants = [ "systemd-tmpfiles-setup.service" - "d2b-priv-broker.socket" + "d2b-broker.socket" ] ++ lib.optional (brokerSocketUnit != null) brokerSocketUnit; after = [ "systemd-tmpfiles-setup.service" - "d2b-priv-broker.socket" - "d2b-priv-broker.service" + "d2b-broker.socket" + "d2b-broker.service" "network.target" "dbus.socket" "dbus.service" @@ -464,14 +464,14 @@ in description = "d2b daemon skeleton"; wantedBy = [ "multi-user.target" ]; wants = [ - "d2b-priv-broker.socket" + "d2b-broker.socket" "systemd-tmpfiles-setup.service" ]; after = [ "systemd-tmpfiles-setup.service" "network.target" - "d2b-priv-broker.socket" - "d2b-priv-broker.service" + "d2b-broker.socket" + "d2b-broker.service" "dbus.socket" "dbus.service" "d2b.slice" diff --git a/nixos-modules/host-polkit.nix b/nixos-modules/host-polkit.nix index b8ec04a683..1308fd8866 100644 --- a/nixos-modules/host-polkit.nix +++ b/nixos-modules/host-polkit.nix @@ -33,7 +33,7 @@ let # * `d2bd.service` - the public daemon. It may restart after a # `nixos-rebuild switch`; VM runners survive via KillMode=process # and daemon re-adoption. - # * `d2b-priv-broker.service` + `d2b-priv-broker.socket` - + # * `d2b-broker.service` + `d2b-broker.socket` - # the privileged broker pair. Socket-activated; operators may # bounce them to recover from a stuck handler. # @@ -51,8 +51,8 @@ let # --------------------------------------------------------------------------- launcherAllowedUnits = [ "d2bd.service" - "d2b-priv-broker.service" - "d2b-priv-broker.socket" + "d2b-broker.service" + "d2b-broker.socket" ]; launcherAllowedUnitsJs = diff --git a/nixos-modules/host.nix b/nixos-modules/host.nix index d1989c081b..779ef5ce1a 100644 --- a/nixos-modules/host.nix +++ b/nixos-modules/host.nix @@ -1,6 +1,6 @@ { inputs }: -{ config, pkgs, lib, ... }: +{ config, pkgs, lib, d2bHostTools ? null, ... }: let d2bLib = import ./lib.nix { inherit lib; }; @@ -14,7 +14,7 @@ let # lib.nix helpers (vmRunner / vmToplevel / vmDeclaredRunner) # read it. vmSubmodule = (import ./vm-submodule.nix { inherit inputs; }) - { inherit config lib pkgs; }; + { inherit config lib pkgs d2bHostTools; }; composeVm = vmSubmodule._composeVm; cfg = config.d2b; diff --git a/nixos-modules/index.nix b/nixos-modules/index.nix index 3ecdf79825..58a8747096 100644 --- a/nixos-modules/index.nix +++ b/nixos-modules/index.nix @@ -400,8 +400,8 @@ let user = "root"; group = principal; socketPath = realm.paths.brokerSocket; - socketUnitName = "${unitPrefix}-priv-broker.socket"; - serviceUnitName = "${unitPrefix}-priv-broker.service"; + socketUnitName = "${unitPrefix}-broker.socket"; + serviceUnitName = "${unitPrefix}-broker.service"; auditDir = realm.paths.auditDir; materializedSocket = brokerMaterialized; materializedService = brokerMaterialized; diff --git a/nixos-modules/minijail-profiles.nix b/nixos-modules/minijail-profiles.nix index f685fbb94e..9f7a2554a0 100644 --- a/nixos-modules/minijail-profiles.nix +++ b/nixos-modules/minijail-profiles.nix @@ -515,7 +515,7 @@ let # deviceBinds is intentionally empty: /dev/dri/renderD128 is # pre-opened by the broker parent and passed to the user-NS child # via fd inheritance (RENDER_NODE_INHERITED_FD = 10 protocol - # constant in d2b-priv-broker/src/sys.rs). No bind-mount. + # constant in d2b-broker/src/sys.rs). No bind-mount. deviceBinds = [ ]; # No real host compositor bind-mount: the GPU runner connects to # the per-VM proxy socket at /run/d2b-wlproxy//wayland-0. diff --git a/nixos-modules/options-site.nix b/nixos-modules/options-site.nix index 305c309a80..7850e26860 100644 --- a/nixos-modules/options-site.nix +++ b/nixos-modules/options-site.nix @@ -60,7 +60,7 @@ default = true; description = '' Use release prebuilt host binaries for `d2b`, `d2bd`, - `d2b-priv-broker`, and the activation helper when they are + `d2b-broker`, and the activation helper when they are available. Set to `false` on development hosts that intentionally validate the checked-out flake's Rust sources before a release artifact exists. @@ -106,12 +106,12 @@ **Reserved.** The broker accepts `--audit-retention-days ` and the runtime prune-on-rotate - loop is shipping in `packages/d2b-priv-broker/src/audit.rs`, + loop is shipping in `packages/d2b-broker/src/audit.rs`, but the NixOS module does not yet spawn the broker (`d2bd` does so at runtime in a future wiring, and this option's value will then thread through - `daemon-config.json` → `d2bd` → `d2b-priv-broker - serve --audit-retention-days `). Until that wiring + `daemon-config.json` → `d2bd` → `d2b-broker + host --audit-retention-days `). Until that wiring lands, overriding this option is a no-op at runtime - the broker defaults to 14 days regardless. diff --git a/nixos-modules/prebuilt-packages.nix b/nixos-modules/prebuilt-packages.nix index f2798e96f4..ae95ce1507 100644 --- a/nixos-modules/prebuilt-packages.nix +++ b/nixos-modules/prebuilt-packages.nix @@ -1,3 +1,16 @@ { pkgs, lib }: -import ../nix/prebuilt.nix { inherit pkgs lib; } +let + packages = import ../nix/prebuilt.nix { inherit pkgs lib; }; + selectPackage = name: fallback: + let + candidate = + if packages != null && builtins.hasAttr name packages + then builtins.getAttr name packages + else null; + in + if candidate != null && (candidate.sourceBinary or null) == null + then candidate + else fallback; +in +(if packages == null then { } else packages) // { inherit selectPackage; } diff --git a/nixos-modules/rust-host-tools.nix b/nixos-modules/rust-host-tools.nix index 3a99b99abe..98ec1a812b 100644 --- a/nixos-modules/rust-host-tools.nix +++ b/nixos-modules/rust-host-tools.nix @@ -176,7 +176,7 @@ let }); brokerCargoArtifacts = craneLib.buildDepsOnly (commonBuildArgs // { - pname = "d2b-priv-broker"; + pname = "d2b-broker"; version = "0.0.0-bootstrap"; dummySrc = dummySource; sourceRoot = "d2b-provider-rust-src"; @@ -184,8 +184,8 @@ let cargoLock = brokerCargoLock; cargoVendorDir = brokerCargoVendorDir; inherit outputHashes; - cargoCheckExtraArgs = "--package d2b-priv-broker --no-default-features"; - cargoBuildExtraArgs = "--package d2b-priv-broker --no-default-features"; + cargoCheckExtraArgs = "--package d2b-broker --no-default-features"; + cargoBuildExtraArgs = "--package d2b-broker --no-default-features"; }); installBinaries = binaries: @@ -214,7 +214,7 @@ let }); broker = craneLib.buildPackage (commonBuildArgs // { - pname = "d2b-priv-broker"; + pname = "d2b-broker"; version = "0.0.0-bootstrap"; cargoArtifacts = brokerCargoArtifacts; inherit outputHashes; @@ -224,7 +224,7 @@ let cargoLock = brokerCargoLock; cargoVendorDir = brokerCargoVendorDir; cargoBuildExtraArgs = "--no-default-features"; - installPhaseCommand = installBinaries [ "d2b-priv-broker" ]; + installPhaseCommand = installBinaries [ "d2b-broker" ]; }); in { diff --git a/nixos-modules/vm-evaluator.nix b/nixos-modules/vm-evaluator.nix index c3da0d1a97..3e4ff864e5 100644 --- a/nixos-modules/vm-evaluator.nix +++ b/nixos-modules/vm-evaluator.nix @@ -22,7 +22,7 @@ # networking, services, etc. - driven by the consumer's # `vm.config` module list). { inputs }: -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, d2bHostTools ? null, ... }: let cfg = config.d2b; @@ -49,6 +49,7 @@ let ./vm-options.nix ./vm-guest-base.nix ./guest-control.nix + ./guest-broker.nix # Inherit host nixpkgs policy so per-VM evals honor the consumer's # allowUnfree / overlays / security fixes without re-stating them in # each per-VM module. @@ -61,7 +62,11 @@ let specialArgs = { inherit inputs; } // cfg.site.extraSpecialArgs - // { d2bInputs = inputs; }; + // { + d2bInputs = inputs; + inherit d2bHostTools; + d2bUsePrebuiltHostTools = cfg.site.usePrebuiltHostTools; + }; inherit (pkgs.stdenv.hostPlatform) system; }; diff --git a/nixos-modules/vm-guest-base.nix b/nixos-modules/vm-guest-base.nix index 3fc4be1b6e..af974397e8 100644 --- a/nixos-modules/vm-guest-base.nix +++ b/nixos-modules/vm-guest-base.nix @@ -145,7 +145,7 @@ in # /dev/disk/by-id/virtio-rootfs. Mount it at the overlay path # so the upperdir/workdir live on a real filesystem (ext4 - the # broker's DiskInit op runs mkfs.ext4 when creating the image; - # see packages/d2b-priv-broker/src/ops/disk_init.rs). + # see packages/d2b-broker/src/ops/disk_init.rs). # Without this mount the overlay upper/work live on the rootfs # tmpfs and are wiped on every reboot, which defeats the # writableStoreOverlay design. diff --git a/nixos-modules/vm-submodule.nix b/nixos-modules/vm-submodule.nix index 56ef212254..d71faa0993 100644 --- a/nixos-modules/vm-submodule.nix +++ b/nixos-modules/vm-submodule.nix @@ -9,11 +9,11 @@ # # No upstream microvm.nix dependency anywhere in this graph. { inputs }: -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, d2bHostTools ? null, ... }: let evaluator = (import ./vm-evaluator.nix { inherit inputs; }) - { inherit config lib pkgs; }; + { inherit config lib pkgs d2bHostTools; }; in { _composeVm = evaluator._composeVm; diff --git a/packages/d2b-priv-broker/BUILD.bazel b/packages/d2b-broker/BUILD.bazel similarity index 64% rename from packages/d2b-priv-broker/BUILD.bazel rename to packages/d2b-broker/BUILD.bazel index 9ffee07cc7..1cc4f4ac76 100644 --- a/packages/d2b-priv-broker/BUILD.bazel +++ b/packages/d2b-broker/BUILD.bazel @@ -16,16 +16,16 @@ exports_files( visibility = ["//visibility:public"], ) -d2b_priv_broker_srcs = glob(["src/**/*.rs"], allow_empty = True) +d2b_broker_srcs = glob(["src/**/*.rs"], allow_empty = True) -d2b_priv_broker_compile_data = [ +d2b_broker_compile_data = [ "//tests/fixtures:deny-unknown/host-valid.json", # keep "//:tests/golden/manifest_v04/baseline-vms.json", # keep "Cargo.toml", "src/sys.rs", ] -d2b_priv_broker_shared_deps = [ +d2b_broker_shared_deps = [ "//packages/d2b-audit:d2b_audit", "//packages/d2b-contracts:d2b_contracts", "//packages/d2b-contracts-broker:d2b_contracts_broker", @@ -35,7 +35,7 @@ d2b_priv_broker_shared_deps = [ "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", ] -d2b_priv_broker_test_support_deps = [ +d2b_broker_test_support_deps = [ "//packages/d2b-audit:d2b_audit_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", @@ -46,94 +46,94 @@ d2b_priv_broker_test_support_deps = [ ] rust_library( - name = "d2b_priv_broker", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, visibility = ["//visibility:public"], - deps = d2b_priv_broker_shared_deps + [ + deps = d2b_broker_shared_deps + [ "//packages/d2b-core:d2b_core", "//packages/d2b-host:d2b_host", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_test_support", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, - crate_name = "d2b_priv_broker", + name = "d2b_broker_test_support", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_test_support_deps + [ + deps = d2b_broker_test_support_deps + [ "//packages/d2b-core:d2b_core_test_support", "//packages/d2b-host:d2b_host_fake_backends", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_layer1_bootstrap", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker_layer1_bootstrap", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, crate_features = ["layer1-bootstrap"], - crate_name = "d2b_priv_broker", + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_shared_deps + [ + deps = d2b_broker_shared_deps + [ "//packages/d2b-core:d2b_core", "//packages/d2b-host:d2b_host", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_layer1_bootstrap_test_support", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker_layer1_bootstrap_test_support", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, crate_features = ["layer1-bootstrap"], - crate_name = "d2b_priv_broker", + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_test_support_deps + [ + deps = d2b_broker_test_support_deps + [ "//packages/d2b-core:d2b_core_test_support", "//packages/d2b-host:d2b_host_fake_backends", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_fake_backends", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker_fake_backends", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, crate_features = ["fake-backends"], - crate_name = "d2b_priv_broker", + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_test_support_deps + [ + deps = d2b_broker_test_support_deps + [ "//packages/d2b-core:d2b_core_test_support", "//packages/d2b-host:d2b_host_fake_backends", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_library( - name = "d2b_priv_broker_fake_backends_test_support", - srcs = d2b_priv_broker_srcs, - compile_data = d2b_priv_broker_compile_data, + name = "d2b_broker_fake_backends_test_support", + srcs = d2b_broker_srcs, + compile_data = d2b_broker_compile_data, crate_features = ["fake-backends"], - crate_name = "d2b_priv_broker", + crate_name = "d2b_broker", visibility = ["//visibility:public"], - deps = d2b_priv_broker_test_support_deps + [ + deps = d2b_broker_test_support_deps + [ "//packages/d2b-core:d2b_core_test_support", "//packages/d2b-host:d2b_host_fake_backends", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_binary( - name = "d2b-priv-broker", + name = "d2b-broker", srcs = ["src/main.rs"], compile_data = ["Cargo.toml"], visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker", + ":d2b_broker", ] + all_crate_deps(normal = True, cargo_only = True), ) rust_binary( - name = "d2b-priv-broker-layer1-bootstrap", + name = "d2b-broker-layer1-bootstrap", srcs = ["src/main.rs"], compile_data = ["Cargo.toml"], crate_features = ["layer1-bootstrap"], @@ -141,7 +141,7 @@ rust_binary( deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_layer1_bootstrap", + ":d2b_broker_layer1_bootstrap", ] + all_crate_deps(normal = True, cargo_only = True), ) @@ -149,15 +149,15 @@ rust_test( name = "bridge_lifecycle", srcs = ["tests/bridge_lifecycle.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-core:d2b_core_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), @@ -172,9 +172,9 @@ rust_test( compile_data = ["Cargo.toml"], crate_features = ["layer1-bootstrap"], crate_root = "tests/broker_export_audit.rs", - data = [":d2b-priv-broker-layer1-bootstrap"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker-layer1-bootstrap)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker-layer1-bootstrap)"}, + data = [":d2b-broker-layer1-bootstrap"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker-layer1-bootstrap)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker-layer1-bootstrap)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ @@ -188,9 +188,9 @@ rust_test( name = "broker_protocol_compatibility", srcs = ["tests/broker_protocol_compatibility.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ @@ -210,9 +210,9 @@ rust_test( compile_data = ["Cargo.toml"], crate_features = ["layer1-bootstrap"], crate_root = "tests/broker_socket_acl.rs", - data = [":d2b-priv-broker-layer1-bootstrap"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker-layer1-bootstrap)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker-layer1-bootstrap)"}, + data = [":d2b-broker-layer1-bootstrap"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker-layer1-bootstrap)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker-layer1-bootstrap)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ @@ -225,15 +225,15 @@ rust_test( name = "bundle_tampered_broker", srcs = ["tests/bundle_tampered_broker.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-core:d2b_core_test_support", @@ -244,15 +244,15 @@ rust_test( name = "kernel_surface", srcs = ["tests/kernel_surface.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -260,15 +260,15 @@ rust_test( name = "persistent_tap_lifecycle", srcs = ["tests/persistent_tap_lifecycle.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-core:d2b_core_test_support", @@ -280,15 +280,15 @@ rust_test( srcs = ["tests/pidfd_handoff_scm_rights.rs"], compile_data = ["Cargo.toml"], crate_features = ["fake-backends"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_fake_backends_test_support", + ":d2b_broker_fake_backends_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -296,15 +296,15 @@ rust_test( name = "pidfd_real_spawner", srcs = ["tests/pidfd_real_spawner.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -312,15 +312,15 @@ rust_test( name = "security_key_broker", srcs = ["tests/security_key_broker.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-core:d2b_core_test_support", @@ -332,20 +332,20 @@ rust_test( srcs = ["tests/socket_activation.rs"], compile_data = ["Cargo.toml"], data = [ - ":d2b-priv-broker", + ":d2b-broker", "//:tests/tools/scrub-shell-environment", ], env = { - "CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)", + "CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)", "D2B_TEST_SCRUB_SHELL_ENVIRONMENT": "$(rootpath //:tests/tools/scrub-shell-environment)", }, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), @@ -355,15 +355,15 @@ rust_test( name = "w12_fd_passing_response", srcs = ["tests/w12_fd_passing_response.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -375,15 +375,15 @@ rust_test( "//:tests/golden/manifest_v04/baseline-vms.json", # keep "Cargo.toml", ], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-core:d2b_core_test_support", @@ -394,28 +394,28 @@ rust_test( name = "zone_store", srcs = ["tests/zone_store.rs"], compile_data = ["Cargo.toml"], - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, - rustc_env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, tags = broker_test_tags(), visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", - ":d2b_priv_broker_test_support", + ":d2b_broker_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) rust_test( - name = "d2b_priv_broker_test", + name = "d2b_broker_test", compile_data = ["Cargo.toml"], - crate = ":d2b_priv_broker_test_support", - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + crate = ":d2b_broker_test_support", + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, rustc_env = { - "CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)", + "CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)", "CARGO_MANIFEST_DIR": ".", }, tags = broker_test_tags(), @@ -426,13 +426,13 @@ rust_test( ) rust_test( - name = "d2b_priv_broker_layer1_bootstrap_test", + name = "d2b_broker_layer1_bootstrap_test", compile_data = ["Cargo.toml"], - crate = ":d2b_priv_broker_layer1_bootstrap_test_support", - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + crate = ":d2b_broker_layer1_bootstrap_test_support", + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, rustc_env = { - "CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)", + "CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)", "CARGO_MANIFEST_DIR": ".", }, tags = broker_test_tags(), @@ -440,19 +440,72 @@ rust_test( ) rust_test( - name = "d2b_priv_broker_fake_backends_test", + name = "d2b_broker_fake_backends_test", compile_data = ["Cargo.toml"], - crate = ":d2b_priv_broker_fake_backends_test_support", - data = [":d2b-priv-broker"], - env = {"CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)"}, + crate = ":d2b_broker_fake_backends_test_support", + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, rustc_env = { - "CARGO_BIN_EXE_d2b-priv-broker": "$(rootpath :d2b-priv-broker)", + "CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)", "CARGO_MANIFEST_DIR": ".", }, tags = broker_test_tags(), deps = all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) +rust_test( + name = "host_profile", + srcs = ["tests/host_profile.rs"], + compile_data = ["Cargo.toml"], + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + tags = broker_test_tags(), + visibility = ["//visibility:public"], + deps = [ + "//packages/d2b-contracts-broker:d2b_contracts_broker", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_test( + name = "guest_profile", + srcs = [ + "tests/guest_profile.rs", + "tests/common/mod.rs", + ], + compile_data = ["Cargo.toml"], + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + tags = broker_test_tags(), + visibility = ["//visibility:public"], + deps = [ + "//packages/d2b-contracts:d2b_contracts_test_support", + "//packages/d2b-contracts-broker:d2b_contracts_broker", + "//packages/d2b-contracts-resource:d2b_contracts_resource", + "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", + ":d2b_broker_test_support", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_test( + name = "profile_separation", + srcs = [ + "tests/profile_separation.rs", + "tests/common/mod.rs", + ], + compile_data = ["Cargo.toml"], + data = [":d2b-broker"], + env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + rustc_env = {"CARGO_BIN_EXE_d2b-broker": "$(rootpath :d2b-broker)"}, + tags = broker_test_tags(), + visibility = ["//visibility:public"], + deps = [ + "//packages/d2b-contracts-broker:d2b_contracts_broker", + ":d2b_broker_test_support", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + # keep filegroup( name = "cargo_workspace_sources", @@ -472,6 +525,6 @@ filegroup( ) rust_doc_test( - name = "d2b_priv_broker_doc_test", - crate = "//packages/d2b-priv-broker:d2b_priv_broker", + name = "d2b_broker_doc_test", + crate = "//packages/d2b-broker:d2b_broker", ) diff --git a/packages/d2b-priv-broker/Cargo.toml b/packages/d2b-broker/Cargo.toml similarity index 99% rename from packages/d2b-priv-broker/Cargo.toml rename to packages/d2b-broker/Cargo.toml index 7454f77b1c..1cac120a5c 100644 --- a/packages/d2b-priv-broker/Cargo.toml +++ b/packages/d2b-broker/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "d2b-priv-broker" +name = "d2b-broker" version = "0.0.0-bootstrap" edition = "2024" publish = false diff --git a/packages/d2b-priv-broker/src/audit.rs b/packages/d2b-broker/src/audit.rs similarity index 99% rename from packages/d2b-priv-broker/src/audit.rs rename to packages/d2b-broker/src/audit.rs index 0bd57364b9..227fd1e4cd 100644 --- a/packages/d2b-priv-broker/src/audit.rs +++ b/packages/d2b-broker/src/audit.rs @@ -528,7 +528,7 @@ impl AuditLog { /// Legacy short-record writer for errored outcomes that need /// admin-visible diagnostics. The full detail is also surfaced in - /// the broker journal (`journalctl -u d2b-priv-broker`) for + /// the broker journal (`journalctl -u d2b-broker`) for /// live-handler failures. pub fn write_error_entry( &self, diff --git a/packages/d2b-priv-broker/src/bootstrap.rs b/packages/d2b-broker/src/bootstrap.rs similarity index 100% rename from packages/d2b-priv-broker/src/bootstrap.rs rename to packages/d2b-broker/src/bootstrap.rs diff --git a/packages/d2b-priv-broker/src/fd_passing.rs b/packages/d2b-broker/src/fd_passing.rs similarity index 100% rename from packages/d2b-priv-broker/src/fd_passing.rs rename to packages/d2b-broker/src/fd_passing.rs diff --git a/packages/d2b-priv-broker/src/lib.rs b/packages/d2b-broker/src/lib.rs similarity index 95% rename from packages/d2b-priv-broker/src/lib.rs rename to packages/d2b-broker/src/lib.rs index bc5b7d12b0..761f7ca69c 100644 --- a/packages/d2b-priv-broker/src/lib.rs +++ b/packages/d2b-broker/src/lib.rs @@ -47,12 +47,14 @@ pub mod protocol; pub mod runtime; pub mod sys; +pub use d2b_contracts_broker::broker_wire::BrokerProfile; + #[cfg(test)] pub(crate) fn test_scratch_root() -> std::path::PathBuf { let root = std::env::var_os("TEST_TMPDIR") .or_else(|| std::env::var_os("CARGO_TARGET_TMPDIR")) .map(std::path::PathBuf::from) - .unwrap_or_else(|| std::env::temp_dir().join("d2b-priv-broker-tests")); + .unwrap_or_else(|| std::env::temp_dir().join("d2b-broker-tests")); std::fs::create_dir_all(&root).expect("create test scratch root"); root } diff --git a/packages/d2b-priv-broker/src/live_handlers.rs b/packages/d2b-broker/src/live_handlers.rs similarity index 99% rename from packages/d2b-priv-broker/src/live_handlers.rs rename to packages/d2b-broker/src/live_handlers.rs index 38aeb17e84..75dd867e16 100644 --- a/packages/d2b-priv-broker/src/live_handlers.rs +++ b/packages/d2b-broker/src/live_handlers.rs @@ -3820,7 +3820,7 @@ mod tests { file_path: root.join("00-d2b-unmanaged.conf"), contents: concat!( "# d2b-managed begin\n", - "# Generated by d2b-priv-broker; do not edit by hand.\n", + "# Generated by d2b-broker; do not edit by hand.\n", "[keyfile]\n", "unmanaged-devices=interface-name:d2b-*\n", "# marker-id=nm-unmanaged:host\n", diff --git a/packages/d2b-priv-broker/src/main.rs b/packages/d2b-broker/src/main.rs similarity index 95% rename from packages/d2b-priv-broker/src/main.rs rename to packages/d2b-broker/src/main.rs index ccc7150f86..0f25efc4ff 100644 --- a/packages/d2b-priv-broker/src/main.rs +++ b/packages/d2b-broker/src/main.rs @@ -1,6 +1,6 @@ use std::process::ExitCode; -use d2b_priv_broker::runtime::{RunError, parse_command, run}; +use d2b_broker::runtime::{RunError, parse_command, run}; fn main() -> ExitCode { // Enable RUST_LOG-driven env filter so the broker surfaces diff --git a/packages/d2b-priv-broker/src/ops/audit_op.rs b/packages/d2b-broker/src/ops/audit_op.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/audit_op.rs rename to packages/d2b-broker/src/ops/audit_op.rs index c4aa11895e..050e6f34da 100644 --- a/packages/d2b-priv-broker/src/ops/audit_op.rs +++ b/packages/d2b-broker/src/ops/audit_op.rs @@ -9,10 +9,7 @@ use std::io; use d2b_audit::{DurabilityEvidence, OperationIdentity, ZoneOperationKey}; use d2b_contracts_broker::broker_wire::{NftablesProjectionAction, RunnerAllocation}; -use d2b_contracts_resource::v3::{ - ResourceBundleGenerationId, - ResourceGeneration, -}; +use d2b_contracts_resource::v3::{ResourceBundleGenerationId, ResourceGeneration}; use serde::{Deserialize, Serialize}; use serde_json::Value; diff --git a/packages/d2b-priv-broker/src/ops/cgroup.rs b/packages/d2b-broker/src/ops/cgroup.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/cgroup.rs rename to packages/d2b-broker/src/ops/cgroup.rs diff --git a/packages/d2b-priv-broker/src/ops/cutover_artifacts.rs b/packages/d2b-broker/src/ops/cutover_artifacts.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/cutover_artifacts.rs rename to packages/d2b-broker/src/ops/cutover_artifacts.rs index 95922cc24a..ca359a6246 100644 --- a/packages/d2b-priv-broker/src/ops/cutover_artifacts.rs +++ b/packages/d2b-broker/src/ops/cutover_artifacts.rs @@ -12,8 +12,8 @@ use std::{ path::{Path, PathBuf}, }; +use d2b_contracts::types::BundleOpId; use d2b_contracts_broker::broker_wire::CanonicalAuditDigest; -use d2b_contracts::{types::BundleOpId}; use d2b_contracts_resource::v3::ArtifactId; use nix::unistd::geteuid; use rustix::fs::{ diff --git a/packages/d2b-priv-broker/src/ops/device.rs b/packages/d2b-broker/src/ops/device.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/device.rs rename to packages/d2b-broker/src/ops/device.rs diff --git a/packages/d2b-priv-broker/src/ops/disk_init.rs b/packages/d2b-broker/src/ops/disk_init.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/disk_init.rs rename to packages/d2b-broker/src/ops/disk_init.rs diff --git a/packages/d2b-priv-broker/src/ops/exec_reconcile.rs b/packages/d2b-broker/src/ops/exec_reconcile.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/exec_reconcile.rs rename to packages/d2b-broker/src/ops/exec_reconcile.rs diff --git a/packages/d2b-priv-broker/src/ops/gpu.rs b/packages/d2b-broker/src/ops/gpu.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/gpu.rs rename to packages/d2b-broker/src/ops/gpu.rs diff --git a/packages/d2b-priv-broker/src/ops/host_generation_handoff.rs b/packages/d2b-broker/src/ops/host_generation_handoff.rs similarity index 98% rename from packages/d2b-priv-broker/src/ops/host_generation_handoff.rs rename to packages/d2b-broker/src/ops/host_generation_handoff.rs index e064f420b7..b0b7a6fa1d 100644 --- a/packages/d2b-priv-broker/src/ops/host_generation_handoff.rs +++ b/packages/d2b-broker/src/ops/host_generation_handoff.rs @@ -82,9 +82,7 @@ impl HandoffEffect for SuccessfulHandoffEffect { request: &ApplyHostGenerationHandoff, ) -> Result { Ok( - if request.intent.activation_mode - == d2b_contracts_resource::v3::ActivationMode::Adopt - { + if request.intent.activation_mode == d2b_contracts_resource::v3::ActivationMode::Adopt { ActivationHelperOutcome::Adopted } else { ActivationHelperOutcome::Succeeded @@ -401,11 +399,7 @@ mod tests { use d2b_contracts_broker::host_generation::{ HandoffCallerRole, HostGenerationHandoffIntent, SourceGenerationCompatibilityFloorV1, }; - use d2b_contracts_resource::v3::{ - ActivationMode, - ArtifactId, - ResourceRef, -}; + use d2b_contracts_resource::v3::{ActivationMode, ArtifactId, ResourceRef}; fn request() -> ApplyHostGenerationHandoff { let target = ResourceRef::parse("Host/host-system").unwrap(); diff --git a/packages/d2b-priv-broker/src/ops/hosts.rs b/packages/d2b-broker/src/ops/hosts.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/hosts.rs rename to packages/d2b-broker/src/ops/hosts.rs diff --git a/packages/d2b-priv-broker/src/ops/media.rs b/packages/d2b-broker/src/ops/media.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/media.rs rename to packages/d2b-broker/src/ops/media.rs diff --git a/packages/d2b-priv-broker/src/ops/mod.rs b/packages/d2b-broker/src/ops/mod.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/mod.rs rename to packages/d2b-broker/src/ops/mod.rs diff --git a/packages/d2b-priv-broker/src/ops/modprobe.rs b/packages/d2b-broker/src/ops/modprobe.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/modprobe.rs rename to packages/d2b-broker/src/ops/modprobe.rs diff --git a/packages/d2b-priv-broker/src/ops/network.rs b/packages/d2b-broker/src/ops/network.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/network.rs rename to packages/d2b-broker/src/ops/network.rs index 3825d5cea7..fee23f62f7 100644 --- a/packages/d2b-priv-broker/src/ops/network.rs +++ b/packages/d2b-broker/src/ops/network.rs @@ -641,10 +641,7 @@ fn digest_parts(parts: &[&[u8]]) -> String { mod tests { use super::*; use d2b_contracts_resource::v3::IfName; - use d2b_contracts_resource::v3::{ - ResourceGeneration, - ResourceUid, -}; + use d2b_contracts_resource::v3::{ResourceGeneration, ResourceUid}; use std::cell::Cell; struct FakeBridge { diff --git a/packages/d2b-priv-broker/src/ops/nft.rs b/packages/d2b-broker/src/ops/nft.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/nft.rs rename to packages/d2b-broker/src/ops/nft.rs index b147b91ce6..334465fb48 100644 --- a/packages/d2b-priv-broker/src/ops/nft.rs +++ b/packages/d2b-broker/src/ops/nft.rs @@ -1164,11 +1164,10 @@ mod tests { ), scope_id: d2b_contracts::types::ScopeId::new("scope:opaque"), action: d2b_contracts_broker::broker_wire::NftablesProjectionAction::Apply, - expected_generation_id: - d2b_contracts_resource::v3::ResourceBundleGenerationId::parse( - "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - ) - .unwrap(), + expected_generation_id: d2b_contracts_resource::v3::ResourceBundleGenerationId::parse( + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ) + .unwrap(), desired_hash: None, tracing_span_id: None, }; diff --git a/packages/d2b-priv-broker/src/ops/nm.rs b/packages/d2b-broker/src/ops/nm.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/nm.rs rename to packages/d2b-broker/src/ops/nm.rs index 44afd393d3..c49d3de7b2 100644 --- a/packages/d2b-priv-broker/src/ops/nm.rs +++ b/packages/d2b-broker/src/ops/nm.rs @@ -92,7 +92,7 @@ impl From for ApplyNmError { pub fn render_nm_conf(entries: &[NmUnmanagedEntry]) -> String { let mut out = String::new(); out.push_str("# d2b-managed begin\n"); - out.push_str("# Generated by d2b-priv-broker; do not edit by hand.\n"); + out.push_str("# Generated by d2b-broker; do not edit by hand.\n"); out.push_str("[keyfile]\n"); let unmanaged_devices: Vec = entries .iter() diff --git a/packages/d2b-priv-broker/src/ops/pidfd.rs b/packages/d2b-broker/src/ops/pidfd.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/pidfd.rs rename to packages/d2b-broker/src/ops/pidfd.rs diff --git a/packages/d2b-priv-broker/src/ops/route.rs b/packages/d2b-broker/src/ops/route.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/route.rs rename to packages/d2b-broker/src/ops/route.rs diff --git a/packages/d2b-priv-broker/src/ops/security_key.rs b/packages/d2b-broker/src/ops/security_key.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/security_key.rs rename to packages/d2b-broker/src/ops/security_key.rs diff --git a/packages/d2b-priv-broker/src/ops/spawn_runner.rs b/packages/d2b-broker/src/ops/spawn_runner.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/spawn_runner.rs rename to packages/d2b-broker/src/ops/spawn_runner.rs diff --git a/packages/d2b-priv-broker/src/ops/state_dir.rs b/packages/d2b-broker/src/ops/state_dir.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/state_dir.rs rename to packages/d2b-broker/src/ops/state_dir.rs diff --git a/packages/d2b-priv-broker/src/ops/storage_contract.rs b/packages/d2b-broker/src/ops/storage_contract.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/storage_contract.rs rename to packages/d2b-broker/src/ops/storage_contract.rs index f2bc39be37..0d2a34d67f 100644 --- a/packages/d2b-priv-broker/src/ops/storage_contract.rs +++ b/packages/d2b-broker/src/ops/storage_contract.rs @@ -685,8 +685,8 @@ mod tests { mode: "0750".to_owned(), access_acl: Vec::new(), default_acl: Vec::new(), - creator: actor(ActorKind::Broker, "d2b-priv-broker"), - writers: vec![actor(ActorKind::Broker, "d2b-priv-broker")], + creator: actor(ActorKind::Broker, "d2b-broker"), + writers: vec![actor(ActorKind::Broker, "d2b-broker")], readers: vec![actor(ActorKind::Daemon, "d2bd")], cleanup_policy: CleanupPolicy::Boot, repair_policy: RepairPolicy::BrokerReconcile, diff --git a/packages/d2b-priv-broker/src/ops/store_sync.rs b/packages/d2b-broker/src/ops/store_sync.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_sync.rs rename to packages/d2b-broker/src/ops/store_sync.rs diff --git a/packages/d2b-priv-broker/src/ops/store_sync_audit.rs b/packages/d2b-broker/src/ops/store_sync_audit.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_sync_audit.rs rename to packages/d2b-broker/src/ops/store_sync_audit.rs diff --git a/packages/d2b-priv-broker/src/ops/store_sync_export.rs b/packages/d2b-broker/src/ops/store_sync_export.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_sync_export.rs rename to packages/d2b-broker/src/ops/store_sync_export.rs diff --git a/packages/d2b-priv-broker/src/ops/store_verify.rs b/packages/d2b-broker/src/ops/store_verify.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_verify.rs rename to packages/d2b-broker/src/ops/store_verify.rs diff --git a/packages/d2b-priv-broker/src/ops/store_view_farm.rs b/packages/d2b-broker/src/ops/store_view_farm.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_view_farm.rs rename to packages/d2b-broker/src/ops/store_view_farm.rs diff --git a/packages/d2b-priv-broker/src/ops/store_view_posture.rs b/packages/d2b-broker/src/ops/store_view_posture.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/store_view_posture.rs rename to packages/d2b-broker/src/ops/store_view_posture.rs diff --git a/packages/d2b-priv-broker/src/ops/swtpm_dir.rs b/packages/d2b-broker/src/ops/swtpm_dir.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/swtpm_dir.rs rename to packages/d2b-broker/src/ops/swtpm_dir.rs diff --git a/packages/d2b-priv-broker/src/ops/swtpm_migration.rs b/packages/d2b-broker/src/ops/swtpm_migration.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/swtpm_migration.rs rename to packages/d2b-broker/src/ops/swtpm_migration.rs diff --git a/packages/d2b-priv-broker/src/ops/sysctl.rs b/packages/d2b-broker/src/ops/sysctl.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/sysctl.rs rename to packages/d2b-broker/src/ops/sysctl.rs diff --git a/packages/d2b-priv-broker/src/ops/systemd.rs b/packages/d2b-broker/src/ops/systemd.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/systemd.rs rename to packages/d2b-broker/src/ops/systemd.rs index 63ef9ef200..0ea823d746 100644 --- a/packages/d2b-priv-broker/src/ops/systemd.rs +++ b/packages/d2b-broker/src/ops/systemd.rs @@ -113,9 +113,8 @@ fn validate_request( { return Err(SystemdError::BundleIntent); } - let expected_execution = - d2b_contracts_resource::v3::ResourceRef::parse(&intent.execution_ref) - .map_err(|_| SystemdError::IdentityMismatch)?; + let expected_execution = d2b_contracts_resource::v3::ResourceRef::parse(&intent.execution_ref) + .map_err(|_| SystemdError::IdentityMismatch)?; if request .execution_ref .as_ref() @@ -594,10 +593,7 @@ mod tests { use super::*; use d2b_contracts::types::{BundleOpId, RoleId, VmId}; use d2b_contracts_broker::broker_wire::{RunnerRole, SystemdUnitRequest}; - use d2b_contracts_resource::v3::{ - ResourceRef, - ResourceUid, -}; + use d2b_contracts_resource::v3::{ResourceRef, ResourceUid}; fn request() -> SystemdUnitRequest { SystemdUnitRequest { diff --git a/packages/d2b-priv-broker/src/ops/tap.rs b/packages/d2b-broker/src/ops/tap.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/tap.rs rename to packages/d2b-broker/src/ops/tap.rs index 5c2cfb2651..d2d7847728 100644 --- a/packages/d2b-priv-broker/src/ops/tap.rs +++ b/packages/d2b-broker/src/ops/tap.rs @@ -969,9 +969,7 @@ pub fn fake_backend() -> FakeBackend { #[cfg(test)] mod tests { use super::*; - use d2b_contracts_resource::v3::{ - IfName as BundleIfName, -}; + use d2b_contracts_resource::v3::IfName as BundleIfName; use d2b_core::bundle::{Bundle, BundleGeneration}; use d2b_core::host::{ BridgePortFlags as HostBridgePortFlags, HostJson as BundleHostJson, IfNameMapping, diff --git a/packages/d2b-priv-broker/src/ops/usbip_firewall.rs b/packages/d2b-broker/src/ops/usbip_firewall.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/usbip_firewall.rs rename to packages/d2b-broker/src/ops/usbip_firewall.rs diff --git a/packages/d2b-priv-broker/src/ops/usbip_host.rs b/packages/d2b-broker/src/ops/usbip_host.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/usbip_host.rs rename to packages/d2b-broker/src/ops/usbip_host.rs diff --git a/packages/d2b-priv-broker/src/ops/usbip_lock.rs b/packages/d2b-broker/src/ops/usbip_lock.rs similarity index 100% rename from packages/d2b-priv-broker/src/ops/usbip_lock.rs rename to packages/d2b-broker/src/ops/usbip_lock.rs diff --git a/packages/d2b-priv-broker/src/ops/zone_store.rs b/packages/d2b-broker/src/ops/zone_store.rs similarity index 99% rename from packages/d2b-priv-broker/src/ops/zone_store.rs rename to packages/d2b-broker/src/ops/zone_store.rs index cd1a0b239a..f05e14d436 100644 --- a/packages/d2b-priv-broker/src/ops/zone_store.rs +++ b/packages/d2b-broker/src/ops/zone_store.rs @@ -16,13 +16,11 @@ use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; use d2b_contracts_broker::broker_wire::{OpenZoneStoreResponse, ZoneStoreDisposition}; -use d2b_contracts_resource::v3::{ - storage::{ +use d2b_contracts_resource::v3::storage::{ ZoneStoreAuxiliaryDirectory, ZoneStoreDescriptorPublicationRequirement, ZoneStoreDirectoryRepairOwner, ZoneStoreFilesystemRequirement, ZoneStoreFsyncRequirement, ZoneStoreId, ZoneStoreLockingRequirement, ZoneStorePrincipal, ZoneStoreReplacementDetection, ZoneStoreReplacementPublicationRequirement, ZoneStoreStorageRow, -}, }; use d2b_core::bundle_resolver::BundleResolver; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; diff --git a/packages/d2b-priv-broker/src/protocol.rs b/packages/d2b-broker/src/protocol.rs similarity index 100% rename from packages/d2b-priv-broker/src/protocol.rs rename to packages/d2b-broker/src/protocol.rs diff --git a/packages/d2b-priv-broker/src/runtime.rs b/packages/d2b-broker/src/runtime.rs similarity index 97% rename from packages/d2b-priv-broker/src/runtime.rs rename to packages/d2b-broker/src/runtime.rs index 9fa20018b2..55e2902a7e 100644 --- a/packages/d2b-priv-broker/src/runtime.rs +++ b/packages/d2b-broker/src/runtime.rs @@ -56,9 +56,11 @@ use crate::protocol::{ use crate::bootstrap::manifest as manifest_api; #[cfg(feature = "layer1-bootstrap")] use crate::bootstrap::wire::{BrokerRequest, BrokerResponse, CallerRole, RequestEnvelope}; +#[cfg(feature = "layer1-bootstrap")] +use d2b_contracts_broker::broker_wire::BrokerProfile; #[cfg(not(feature = "layer1-bootstrap"))] use d2b_contracts_broker::broker_wire::{ - AuditJoinContext, BrokerCallerRole as CallerRole, BrokerRequest, + AuditJoinContext, BrokerCallerRole as CallerRole, BrokerProfile, BrokerRequest, BrokerRequestEnvelope as RequestEnvelope, BrokerResponse, CanonicalAuditDigest, }; #[cfg(feature = "layer1-bootstrap")] @@ -75,10 +77,12 @@ use d2b_realm_core::{RealmIdentityConfigJson, RealmIdentityConfigSummary}; /// `D2B_BROKER_SOCKET_PATH`) to override the path used in non-activated /// (test / legacy) mode. const DEFAULT_SOCKET_PATH: &str = "/run/d2b/priv.sock"; +const DEFAULT_GUEST_SOCKET_PATH: &str = "/run/d2b/guest-broker.sock"; /// Audit records land under /// `/var/lib/d2b/audit/broker-.jsonl` (no more legacy /// single `broker-audit.log` file). Override via `--audit-dir`. const DEFAULT_AUDIT_DIR: &str = "/var/lib/d2b/audit"; +const DEFAULT_GUEST_AUDIT_DIR: &str = "/var/lib/d2b/guest-audit"; /// Default audit retention. Matches the docs claim in /// `docs/reference/daemon-api.md` "Audit" and `AGENTS.md` "Control /// plane". Override via `--audit-retention-days` (broker flag) or the @@ -86,9 +90,13 @@ const DEFAULT_AUDIT_DIR: &str = "/var/lib/d2b/audit"; /// to disable pruning. const DEFAULT_AUDIT_RETENTION_DAYS: u32 = 30; const DEFAULT_BUNDLE_PATH: &str = "/var/lib/d2b/current-bundle/manifest.json"; +const DEFAULT_GUEST_BUNDLE_PATH: &str = "/etc/d2b/guest-bundle.json"; const DEFAULT_REALM_CONTROLLERS_PATH: &str = "/etc/d2b/realm-controllers.json"; +const DEFAULT_GUEST_REALM_CONTROLLERS_PATH: &str = "/etc/d2b/guest-realm-controllers.json"; const DEFAULT_REALM_IDENTITY_PATH: &str = "/etc/d2b/realm-identity.json"; +const DEFAULT_GUEST_REALM_IDENTITY_PATH: &str = "/etc/d2b/guest-realm-identity.json"; const DEFAULT_STATE_DIR: &str = "/var/lib/d2b"; +const DEFAULT_GUEST_STATE_DIR: &str = "/var/lib/d2b/guest-broker"; const DEFAULT_ACTIVATION_HELPER_PATH: &str = "/run/current-system/sw/bin/d2b-activation-helper"; const DEFAULT_CUTOVER_RUNNER_PATH: &str = "/run/current-system/sw/bin/d2b-cutover-runner"; const DEFAULT_CUTOVER_SOCKET_ROOT: &str = "/run/d2b"; @@ -355,7 +363,8 @@ fn cutover_relaunch_allowed( config: &ServerConfig, request: &d2b_contracts_broker::broker_wire::BrokerRequest, ) -> Result { - let d2b_contracts_broker::broker_wire::BrokerRequest::LaunchCutoverRunner(request) = request else { + let d2b_contracts_broker::broker_wire::BrokerRequest::LaunchCutoverRunner(request) = request + else { return Ok(false); }; let capabilities = load_cutover_capabilities(config)?; @@ -389,6 +398,11 @@ const MAX_MODULE_NAME_LEN: usize = 64; #[derive(Debug, Clone)] pub struct ServerConfig { + /// Fixed process-start authority profile. Requests cannot change it. + pub profile: BrokerProfile, + /// Stable authority label binding this process instance's state, socket, + /// audit root, and caller identity. + pub authority_id: String, pub socket_path: PathBuf, pub audit_dir: PathBuf, pub audit_retention_days: u32, @@ -428,12 +442,12 @@ pub struct ServerConfig { #[cfg(all(test, not(feature = "layer1-bootstrap")))] mod cutover_audit_tests { use super::*; + use d2b_contracts::types::BundleOpId; use d2b_contracts_broker::broker_wire::{ BrokerCallerRole, BrokerRequest, CanonicalAuditDigest, CutoverAuditRequest, CutoverAuditTransition, CutoverEffectAuthority, CutoverEffectKind, CutoverEffectRequest, CutoverReplayClass, }; - use d2b_contracts::types::BundleOpId; use nix::unistd::Gid; fn request() -> BrokerRequest { @@ -449,6 +463,8 @@ mod cutover_audit_tests { fn config(root: &Path) -> ServerConfig { ServerConfig { + profile: BrokerProfile::Host, + authority_id: "test-host".to_owned(), socket_path: root.join("broker.sock"), audit_dir: root.join("audit"), audit_retention_days: 30, @@ -836,7 +852,8 @@ mod cutover_audit_tests { #[derive(Debug, Clone)] pub enum BrokerMode { - Serve(ServerConfig), + Host(ServerConfig), + Guest(ServerConfig), #[cfg(feature = "layer1-bootstrap")] ProbeHello { socket_path: PathBuf, @@ -1007,6 +1024,10 @@ enum BrokerError { PeerCredentialRefused { operation: &'static str, }, + ProfileOperationRefused { + profile: BrokerProfile, + operation: &'static str, + }, #[cfg_attr(feature = "layer1-bootstrap", allow(dead_code))] GuestControlSignRefused { reason: &'static str, @@ -1040,193 +1061,243 @@ where I: IntoIterator, { let mut args = args.into_iter(); - let subcommand = args.next().unwrap_or_else(|| "serve".to_owned()); + let subcommand = args + .next() + .ok_or_else(|| RunError::Usage("usage: d2b-broker [host|guest] [options]".to_owned()))?; + #[cfg(feature = "layer1-bootstrap")] match subcommand.as_str() { - "serve" => { - // --socket-path is optional. Resolution order: - // 1. --socket-path flag (explicit override) - // 2. D2B_BROKER_SOCKET_PATH env var - // 3. DEFAULT_SOCKET_PATH constant ("/run/d2b/priv.sock") - // Under SD_LISTEN_FDS=1 (socket activation) the resolved path is - // informational only; the broker adopts fd 3 from systemd and - // MUST NOT bind, fchmod, or fchown the socket path. - let mut socket_path_override: Option = None; - let mut audit_dir = PathBuf::from(DEFAULT_AUDIT_DIR); - let mut audit_retention_days = DEFAULT_AUDIT_RETENTION_DAYS; - let mut bundle_path = PathBuf::from(DEFAULT_BUNDLE_PATH); - let mut realm_controllers_path = PathBuf::from(DEFAULT_REALM_CONTROLLERS_PATH); - let mut realm_identity_path = PathBuf::from(DEFAULT_REALM_IDENTITY_PATH); - let mut state_dir = PathBuf::from(DEFAULT_STATE_DIR); - let mut activation_helper_path = PathBuf::from(DEFAULT_ACTIVATION_HELPER_PATH); - let mut store_sync_export_dir = - PathBuf::from(crate::ops::store_sync_export::DEFAULT_STORE_SYNC_EXPORT_DIR); - let mut d2bd_uid = None; - let mut d2bd_gid = None; - let mut test_mode = false; - let rest: Vec = args.collect(); - let mut index = 0; - while index < rest.len() { - match rest[index].as_str() { - "--socket-path" => { - index += 1; - socket_path_override = - Some(PathBuf::from(expect_arg(&rest, index, "--socket-path")?)); - } - "--audit-dir" => { - index += 1; - audit_dir = PathBuf::from(expect_arg(&rest, index, "--audit-dir")?); - } - "--audit-retention-days" => { - index += 1; - audit_retention_days = expect_arg(&rest, index, "--audit-retention-days")? - .parse() - .map_err(|_| { - RunError::Usage( - "invalid --audit-retention-days (expected a non-negative integer; 0 disables pruning)" - .to_owned(), - ) - })?; - } - "--bundle-path" => { - // Broker reads the bundle manifest from this - // server-configured path so the daemon never - // names a bundle path on the wire. - index += 1; - bundle_path = PathBuf::from(expect_arg(&rest, index, "--bundle-path")?); - } - "--realm-controllers-path" => { - index += 1; - realm_controllers_path = - PathBuf::from(expect_arg(&rest, index, "--realm-controllers-path")?); - } - "--realm-identity-path" => { - index += 1; - realm_identity_path = - PathBuf::from(expect_arg(&rest, index, "--realm-identity-path")?); - } - "--state-dir" => { - index += 1; - state_dir = PathBuf::from(expect_arg(&rest, index, "--state-dir")?); - } - "--activation-helper-path" => { - index += 1; - activation_helper_path = - PathBuf::from(expect_arg(&rest, index, "--activation-helper-path")?); - } - "--store-sync-export-dir" => { - index += 1; - store_sync_export_dir = - PathBuf::from(expect_arg(&rest, index, "--store-sync-export-dir")?); - } - "--d2bd-uid" => { - index += 1; - d2bd_uid = Some( - expect_arg(&rest, index, "--d2bd-uid")? - .parse() - .map_err(|_| RunError::Usage("invalid --d2bd-uid".to_owned()))?, - ); - } - "--d2bd-gid" => { - index += 1; - d2bd_gid = Some( - expect_arg(&rest, index, "--d2bd-gid")? - .parse() - .map_err(|_| RunError::Usage("invalid --d2bd-gid".to_owned()))?, - ); - } - "--test-mode" => test_mode = true, - other => { - return Err(RunError::Usage(format!("unknown serve flag: {other}"))); - } - } - index += 1; - } - - // Resolve socket path: flag > env var > built-in default. - let socket_path = socket_path_override - .or_else(|| env::var("D2B_BROKER_SOCKET_PATH").ok().map(PathBuf::from)) - .unwrap_or_else(|| PathBuf::from(DEFAULT_SOCKET_PATH)); - - let fallback_uid = if test_mode { - nix::unistd::Uid::current().as_raw() - } else { - env::var("D2BD_UID") - .ok() - .and_then(|value| value.parse().ok()) - .ok_or_else(|| { - RunError::Usage( - "missing d2bd uid: pass --d2bd-uid or set D2BD_UID".to_owned(), - ) - })? - }; - let fallback_gid = if test_mode { - nix::unistd::Gid::current().as_raw() - } else { - env::var("D2BD_GID") - .ok() - .and_then(|value| value.parse().ok()) - .ok_or_else(|| { - RunError::Usage( - "missing d2bd gid: pass --d2bd-gid or set D2BD_GID".to_owned(), - ) - })? - }; - - Ok(BrokerMode::Serve(ServerConfig { - socket_path, - audit_dir, - audit_retention_days, - bundle_path, - realm_controllers_path, - realm_identity_path, - state_dir, - activation_helper_path, - cutover_runner_path: env::var_os("D2B_CUTOVER_RUNNER_PATH") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(DEFAULT_CUTOVER_RUNNER_PATH)), - d2bd_uid: d2bd_uid.unwrap_or(fallback_uid), - d2bd_gid: d2bd_gid.unwrap_or(fallback_gid), - store_sync_export_dir, - test_mode, - })) - } - #[cfg(feature = "layer1-bootstrap")] "probe-hello" => { let (socket_path, test_uid) = parse_probe_flags(args.collect())?; - Ok(BrokerMode::ProbeHello { + return Ok(BrokerMode::ProbeHello { socket_path, test_uid, - }) + }); } - #[cfg(feature = "layer1-bootstrap")] "probe-stub" => { let rest: Vec = args.collect(); let (socket_path, test_uid, operation) = parse_stub_flags(&rest)?; - Ok(BrokerMode::ProbeStub { + return Ok(BrokerMode::ProbeStub { socket_path, test_uid, operation, - }) + }); } - #[cfg(feature = "layer1-bootstrap")] "probe-export-audit" => { let rest: Vec = args.collect(); let (socket_path, test_uid, caller_role) = parse_export_flags(&rest)?; - Ok(BrokerMode::ProbeExportAudit { + return Ok(BrokerMode::ProbeExportAudit { socket_path, test_uid, caller_role, - }) + }); + } + _ => {} + } + let profile = match subcommand.as_str() { + "host" => BrokerProfile::Host, + "guest" => BrokerProfile::Guest, + other => { + return Err(RunError::Usage(format!( + "usage: d2b-broker [host|guest] [options] (unknown profile: {other})" + ))); + } + }; + // --socket-path is optional. Resolution order: + // 1. --socket-path flag (explicit override) + // 2. D2B_BROKER_SOCKET_PATH env var + // 3. DEFAULT_SOCKET_PATH constant ("/run/d2b/priv.sock") + // Under SD_LISTEN_FDS=1 (socket activation) the resolved path is + // informational only; the broker adopts fd 3 from systemd and + // MUST NOT bind, fchmod, or fchown the socket path. + let mut socket_path_override: Option = None; + let mut audit_dir = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_AUDIT_DIR, + BrokerProfile::Guest => DEFAULT_GUEST_AUDIT_DIR, + }); + let mut audit_retention_days = DEFAULT_AUDIT_RETENTION_DAYS; + let mut bundle_path = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_BUNDLE_PATH, + BrokerProfile::Guest => DEFAULT_GUEST_BUNDLE_PATH, + }); + let mut realm_controllers_path = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_REALM_CONTROLLERS_PATH, + BrokerProfile::Guest => DEFAULT_GUEST_REALM_CONTROLLERS_PATH, + }); + let mut realm_identity_path = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_REALM_IDENTITY_PATH, + BrokerProfile::Guest => DEFAULT_GUEST_REALM_IDENTITY_PATH, + }); + let mut state_dir = PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_STATE_DIR, + BrokerProfile::Guest => DEFAULT_GUEST_STATE_DIR, + }); + let mut activation_helper_path = PathBuf::from(DEFAULT_ACTIVATION_HELPER_PATH); + let mut store_sync_export_dir = match profile { + BrokerProfile::Host => { + PathBuf::from(crate::ops::store_sync_export::DEFAULT_STORE_SYNC_EXPORT_DIR) + } + BrokerProfile::Guest => PathBuf::from("/var/lib/d2b/guest-audit/store-sync"), + }; + let mut authority_id = profile.as_str().to_owned(); + let mut d2bd_uid = None; + let mut d2bd_gid = None; + let mut test_mode = false; + let rest: Vec = args.collect(); + let mut index = 0; + while index < rest.len() { + match rest[index].as_str() { + "--socket-path" => { + index += 1; + socket_path_override = + Some(PathBuf::from(expect_arg(&rest, index, "--socket-path")?)); + } + "--audit-dir" => { + index += 1; + audit_dir = PathBuf::from(expect_arg(&rest, index, "--audit-dir")?); + } + "--audit-retention-days" => { + index += 1; + audit_retention_days = expect_arg(&rest, index, "--audit-retention-days")? + .parse() + .map_err(|_| { + RunError::Usage( + "invalid --audit-retention-days (expected a non-negative integer; 0 disables pruning)" + .to_owned(), + ) + })?; + } + "--bundle-path" => { + // Broker reads the bundle manifest from this + // server-configured path so the daemon never + // names a bundle path on the wire. + index += 1; + bundle_path = PathBuf::from(expect_arg(&rest, index, "--bundle-path")?); + } + "--realm-controllers-path" => { + index += 1; + realm_controllers_path = + PathBuf::from(expect_arg(&rest, index, "--realm-controllers-path")?); + } + "--realm-identity-path" => { + index += 1; + realm_identity_path = + PathBuf::from(expect_arg(&rest, index, "--realm-identity-path")?); + } + "--state-dir" => { + index += 1; + state_dir = PathBuf::from(expect_arg(&rest, index, "--state-dir")?); + } + "--activation-helper-path" => { + index += 1; + activation_helper_path = + PathBuf::from(expect_arg(&rest, index, "--activation-helper-path")?); + } + "--store-sync-export-dir" => { + index += 1; + store_sync_export_dir = + PathBuf::from(expect_arg(&rest, index, "--store-sync-export-dir")?); + } + "--authority-id" => { + index += 1; + authority_id = expect_arg(&rest, index, "--authority-id")?.to_owned(); + if authority_id.is_empty() { + return Err(RunError::Usage( + "--authority-id must not be empty".to_owned(), + )); + } + } + "--d2bd-uid" => { + index += 1; + d2bd_uid = Some( + expect_arg(&rest, index, "--d2bd-uid")? + .parse() + .map_err(|_| RunError::Usage("invalid --d2bd-uid".to_owned()))?, + ); + } + "--d2bd-gid" => { + index += 1; + d2bd_gid = Some( + expect_arg(&rest, index, "--d2bd-gid")? + .parse() + .map_err(|_| RunError::Usage("invalid --d2bd-gid".to_owned()))?, + ); + } + "--test-mode" => test_mode = true, + other => { + return Err(RunError::Usage(format!( + "unknown {} flag: {other}", + profile.as_str() + ))); + } } - _ => Err(RunError::Usage( - "usage: d2b-priv-broker [serve|probe-hello|probe-stub|probe-export-audit]".to_owned(), - )), + index += 1; } + + // Resolve socket path: flag > env var > built-in default. + let socket_path = socket_path_override + .or_else(|| { + let variable = match profile { + BrokerProfile::Host => "D2B_BROKER_SOCKET_PATH", + BrokerProfile::Guest => "D2B_GUEST_BROKER_SOCKET_PATH", + }; + env::var(variable).ok().map(PathBuf::from) + }) + .unwrap_or_else(|| { + PathBuf::from(match profile { + BrokerProfile::Host => DEFAULT_SOCKET_PATH, + BrokerProfile::Guest => DEFAULT_GUEST_SOCKET_PATH, + }) + }); + + let fallback_uid = if test_mode { + nix::unistd::Uid::current().as_raw() + } else { + env::var("D2BD_UID") + .ok() + .and_then(|value| value.parse().ok()) + .ok_or_else(|| { + RunError::Usage("missing d2bd uid: pass --d2bd-uid or set D2BD_UID".to_owned()) + })? + }; + let fallback_gid = if test_mode { + nix::unistd::Gid::current().as_raw() + } else { + env::var("D2BD_GID") + .ok() + .and_then(|value| value.parse().ok()) + .ok_or_else(|| { + RunError::Usage("missing d2bd gid: pass --d2bd-gid or set D2BD_GID".to_owned()) + })? + }; + + let config = ServerConfig { + profile, + authority_id, + socket_path, + audit_dir, + audit_retention_days, + bundle_path, + realm_controllers_path, + realm_identity_path, + state_dir, + activation_helper_path, + cutover_runner_path: env::var_os("D2B_CUTOVER_RUNNER_PATH") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(DEFAULT_CUTOVER_RUNNER_PATH)), + d2bd_uid: d2bd_uid.unwrap_or(fallback_uid), + d2bd_gid: d2bd_gid.unwrap_or(fallback_gid), + store_sync_export_dir, + test_mode, + }; + Ok(match profile { + BrokerProfile::Host => BrokerMode::Host(config), + BrokerProfile::Guest => BrokerMode::Guest(config), + }) } pub fn run(command: BrokerMode) -> Result<(), RunError> { match command { - BrokerMode::Serve(config) => run_server(config), + BrokerMode::Host(config) | BrokerMode::Guest(config) => run_server(config), #[cfg(feature = "layer1-bootstrap")] BrokerMode::ProbeHello { socket_path, @@ -1623,6 +1694,19 @@ fn try_load_resolver_with_policy( } } +/// Bind a kernel-authenticated peer to this broker instance before any wire +/// bytes are decoded. Test mode keeps the existing simulated envelope UID +/// support, but still requires the actual local test process credentials. +fn peer_matches_instance(config: &ServerConfig, peer_uid: u32, peer_gid: u32) -> bool { + if config.test_mode { + return (peer_uid == nix::unistd::Uid::current().as_raw() + && peer_gid == nix::unistd::Gid::current().as_raw()) + || peer_uid == 0; + } + (peer_uid == config.d2bd_uid && peer_gid == config.d2bd_gid) + || (config.profile == BrokerProfile::Host && peer_uid == 0) +} + fn handle_connection( fd: OwnedFd, config: &ServerConfig, @@ -1633,6 +1717,23 @@ fn handle_connection( ipc_rate_limiter: &Arc>, ) -> io::Result<()> { let (peer_uid, peer_gid, peer_pid) = peer_credentials(fd.as_raw_fd())?; + if !peer_matches_instance(config, peer_uid, peer_gid) { + let _ = write_refusal_audit_bounded( + audit_log, + AuditWriteClass::Unprivileged, + "PeerAuthentication", + peer_uid, + peer_gid, + "peer-refused-before-decode", + "broker-instance", + "closed", + ); + // Do not decode or synchronously drain an untrusted frame. Closing + // the accepted socket is the fail-closed response and avoids letting + // an unauthenticated peer hold a broker worker while it trickles + // request bytes. + return Ok(()); + } #[cfg(not(feature = "layer1-bootstrap"))] let (envelope, request_fds) = match recv_json_frame_with_fds::(fd.as_raw_fd())? { @@ -1659,6 +1760,25 @@ fn handle_connection( let operation = request.op_name(); let opaque_target_id = request.opaque_target_id(); #[cfg(not(feature = "layer1-bootstrap"))] + if !config.profile.allows_request(&request) { + let error = BrokerError::ProfileOperationRefused { + profile: config.profile, + operation, + }; + let _ = write_refusal_audit_bounded( + audit_log, + AuditWriteClass::Privileged, + operation, + peer_uid, + peer_gid, + "profile-operation-denied", + opaque_target_id, + "closed", + ); + send_json_frame(fd.as_raw_fd(), &error.into_response())?; + return Ok(()); + } + #[cfg(not(feature = "layer1-bootstrap"))] let cutover_window = match cutover_window_active(config) { Ok(active) => active, Err(error) => { @@ -2448,7 +2568,7 @@ fn dispatch_request( "ok", ) .map_err(|err| BrokerError::Protocol(err.to_string()))?; - Ok(hello_ok_response()) + Ok(hello_ok_response(_config.profile)) } BrokerRequest::ValidateBundle { path } => { handle_validate_bundle(&path, caller_uid, caller_gid, audit_log) @@ -2648,7 +2768,9 @@ fn cutover_effect_operation_name( d2b_contracts_broker::broker_wire::CutoverEffectKind::GuestActivation => { "CutoverEffect.GuestActivation" } - d2b_contracts_broker::broker_wire::CutoverEffectKind::Verification => "CutoverEffect.Verification", + d2b_contracts_broker::broker_wire::CutoverEffectKind::Verification => { + "CutoverEffect.Verification" + } d2b_contracts_broker::broker_wire::CutoverEffectKind::CutoverFinalization => { "CutoverEffect.CutoverFinalization" } @@ -2674,12 +2796,16 @@ fn cutover_effect_operation_name( "CutoverEffect.CutoverBroker" } d2b_contracts_broker::broker_wire::CutoverEffectKind::HostDrain - | d2b_contracts_broker::broker_wire::CutoverEffectKind::ClosureActivation => "CutoverEffect", + | d2b_contracts_broker::broker_wire::CutoverEffectKind::ClosureActivation => { + "CutoverEffect" + } } } #[cfg(not(feature = "layer1-bootstrap"))] -fn cutover_effect_owner(effect: d2b_contracts_broker::broker_wire::CutoverEffectKind) -> &'static str { +fn cutover_effect_owner( + effect: d2b_contracts_broker::broker_wire::CutoverEffectKind, +) -> &'static str { use d2b_contracts_broker::broker_wire::CutoverEffectKind; match effect { CutoverEffectKind::ApplyAdmission => "ADR046-reset-001", @@ -2708,8 +2834,8 @@ fn execute_cutover_verification( request: &d2b_contracts_broker::broker_wire::CutoverVerificationRequest, resolver: &BundleResolver, ) -> d2b_contracts_broker::broker_wire::CutoverVerificationResponse { - use d2b_contracts_broker::broker_wire::{CutoverVerificationResponse, CutoverZoneVerification}; use d2b_contracts::types::BundleOpId; + use d2b_contracts_broker::broker_wire::{CutoverVerificationResponse, CutoverZoneVerification}; let configured_zone_ids = resolver .bundle .artifact_hashes @@ -3229,7 +3355,7 @@ fn dispatch_request_with_backend_and_request_fds( client_version: req.client_version, }, )?; - Ok(DispatchResult::no_fds(hello_ok_response())) + Ok(DispatchResult::no_fds(hello_ok_response(config.profile))) } RealBrokerRequest::GuestControlSign(req) => { let response = handle_guest_control_sign(req.clone(), config, resolver)?; @@ -6168,7 +6294,11 @@ fn dispatch_request_with_backend_and_request_fds( d2b_contracts_broker::broker_wire::CutoverEffectKind::Verification if matches!( &req.payload, - Some(d2b_contracts_broker::broker_wire::CutoverEffectPayload::Verification(_)) + Some( + d2b_contracts_broker::broker_wire::CutoverEffectPayload::Verification( + _ + ) + ) ) => { let d2b_contracts_broker::broker_wire::CutoverEffectPayload::Verification( @@ -10555,8 +10685,7 @@ fn validate_sandbox_launch_plan( resolved: "unsupported".to_owned(), }); } - if plan.environment_class != d2b_contracts_resource::v3::process::EnvironmentClass::Minimal - { + if plan.environment_class != d2b_contracts_resource::v3::process::EnvironmentClass::Minimal { return Err(BrokerError::SpawnRunnerIntentMismatch { field: "sandbox_plan.environment_class", requested: format!("{:?}", plan.environment_class), @@ -10659,9 +10788,7 @@ fn capability_matches( capability: &str, ) -> bool { let expected = match class { - d2b_contracts_resource::v3::process::CapabilityClass::NetworkBind => { - "CAP_NET_BIND_SERVICE" - } + d2b_contracts_resource::v3::process::CapabilityClass::NetworkBind => "CAP_NET_BIND_SERVICE", d2b_contracts_resource::v3::process::CapabilityClass::NetworkRaw => "CAP_NET_RAW", d2b_contracts_resource::v3::process::CapabilityClass::NetworkAdmin => "CAP_NET_ADMIN", d2b_contracts_resource::v3::process::CapabilityClass::SysTime => "CAP_SYS_TIME", @@ -11778,12 +11905,14 @@ fn launch_cutover_runner( // Close the registration race for a runner that exits before the // background SIGCHLD loop observes the pidfd. targeted_reap_runner(&runner_id, outcome.pidfd.as_fd()); - Ok(d2b_contracts_broker::broker_wire::LaunchCutoverRunnerResponse { - operation_id: request.operation_id.clone(), - pid, - start_time_ticks, - pidfd_index: None, - }) + Ok( + d2b_contracts_broker::broker_wire::LaunchCutoverRunnerResponse { + operation_id: request.operation_id.clone(), + pid, + start_time_ticks, + pidfd_index: None, + }, + ) } impl BrokerError { @@ -11832,6 +11961,7 @@ impl BrokerError { | Self::StoreSyncFailed { .. } | Self::SwtpmDirHardening { .. } | Self::RequestValidation { .. } + | Self::ProfileOperationRefused { .. } ); let has_request_payload = !audit_context .request_fields @@ -12124,7 +12254,7 @@ impl BrokerError { // stderr) in the broker journal, not just the // `Broker.LiveHandlerFailed` wrapper kind. The same // detail is recorded in the audit log; an operator - // reading `journalctl -u d2b-priv-broker` should not + // reading `journalctl -u d2b-broker` should not // have to cross-reference the audit jsonl to learn why a // runner spawn failed. tracing::warn!( @@ -12154,6 +12284,37 @@ impl BrokerError { message, )?; } + Self::ProfileOperationRefused { profile, operation } => { + audit_log.write_entry_with_caller_ids( + operation, + caller_uid, + caller_gid, + "profile-operation-denied", + opaque_target_id, + "denied", + )?; + audit_log.record_with_join( + operation, + opaque_target_id, + caller_uid, + caller_gid, + audit_context.peer_pid, + audit_context.peer_role.as_str(), + authz_result, + "", + opaque_target_id, + audit_context.verb.as_str(), + audit_context.request_fields.clone(), + "denied-refused", + Some("profile-operation-denied"), + None, + bundle_metadata.bundle_version, + bundle_metadata.bundle_hash, + audit_context.duration_us(), + Some(serde_json::json!({ "profile": profile.as_str() })), + supplied_join, + )?; + } Self::CoexistenceRefused { manager, rationale } => { audit_log.write_entry_with_caller_ids( operation, @@ -12543,7 +12704,17 @@ impl BrokerError { operation, None, "broker peer credential check refused the private request", - "Ensure only d2bd connects to d2b-priv-broker.socket; restart d2bd after host credential changes.", + "Ensure only d2bd connects to d2b-broker.socket; restart d2bd after host credential changes.", + ), + Self::ProfileOperationRefused { profile, operation } => error_response( + "Broker.ProfileOperationDenied", + operation, + None, + &format!( + "broker profile `{}` does not admit this effect class", + profile.as_str() + ), + "Use the fixed effect adapter for the authority that owns this operation; the active broker profile cannot be changed by a request.", ), Self::GuestControlSignRefused { reason } => error_response( "Broker.GuestControlSignRefused", @@ -12640,13 +12811,24 @@ fn public_protocol_message(message: &str) -> String { } } -fn hello_ok_response() -> BrokerResponse { +fn profile_capabilities(profile: BrokerProfile) -> Vec { + match profile { + BrokerProfile::Host => CAPABILITIES.iter().map(|item| (*item).to_owned()).collect(), + BrokerProfile::Guest => profile + .operations() + .iter() + .map(|item| (*item).to_owned()) + .collect(), + } +} + +fn hello_ok_response(profile: BrokerProfile) -> BrokerResponse { #[cfg(feature = "layer1-bootstrap")] { BrokerResponse::HelloOk { server_version: "0.0.0-w2-bootstrap".to_owned(), selected_version: "0.0.0-test".to_owned(), - capabilities: CAPABILITIES.iter().map(|item| (*item).to_owned()).collect(), + capabilities: profile_capabilities(profile), } } #[cfg(not(feature = "layer1-bootstrap"))] @@ -12654,7 +12836,7 @@ fn hello_ok_response() -> BrokerResponse { BrokerResponse::Hello(d2b_contracts_broker::broker_wire::HelloResponse { server_version: "0.0.0-w2".to_owned(), selected_version: "0.0.0-w2".to_owned(), - capabilities: CAPABILITIES.iter().map(|item| (*item).to_owned()).collect(), + capabilities: profile_capabilities(profile), }) } } @@ -12762,10 +12944,7 @@ fn redact_public_detail(detail: &str) -> String { { return detail.to_owned(); } - d2b_contracts_resource::v3::canonical_digest( - "d2b:broker-public-error:v1", - detail.as_bytes(), - ) + d2b_contracts_resource::v3::canonical_digest("d2b:broker-public-error:v1", detail.as_bytes()) } /// Start a background tokio runtime that listens for SIGCHLD and reaps @@ -13471,17 +13650,17 @@ mod tests { #[allow(clippy::infallible_destructuring_match)] fn parse_command_accepts_realm_controllers_path_without_changing_socket_defaults() { let mode = parse_command([ - "serve".to_owned(), + "host".to_owned(), "--realm-controllers-path".to_owned(), "/etc/d2b/custom-realm-controllers.json".to_owned(), "--realm-identity-path".to_owned(), "/etc/d2b/custom-realm-identity.json".to_owned(), "--test-mode".to_owned(), ]) - .expect("serve command parses"); + .expect("host command parses"); let config = match mode { - BrokerMode::Serve(config) => config, + BrokerMode::Host(config) | BrokerMode::Guest(config) => config, #[cfg(feature = "layer1-bootstrap")] other => panic!("expected serve mode, got {other:?}"), }; @@ -13502,11 +13681,11 @@ mod tests { // Rewriting it as a `let` would stop compiling with the feature enabled. #[allow(clippy::infallible_destructuring_match)] fn parse_command_defaults_realm_metadata_paths() { - let mode = parse_command(["serve".to_owned(), "--test-mode".to_owned()]) - .expect("serve command parses with defaults"); + let mode = parse_command(["host".to_owned(), "--test-mode".to_owned()]) + .expect("host command parses with defaults"); let config = match mode { - BrokerMode::Serve(config) => config, + BrokerMode::Host(config) | BrokerMode::Guest(config) => config, #[cfg(feature = "layer1-bootstrap")] other => panic!("expected serve mode, got {other:?}"), }; @@ -14170,6 +14349,8 @@ mod tests { #[cfg(not(feature = "layer1-bootstrap"))] fn test_server_config(root: &Path, manifest_path: &Path) -> ServerConfig { ServerConfig { + profile: BrokerProfile::Host, + authority_id: "test-host".to_owned(), socket_path: root.join("broker.sock"), audit_dir: root.join("audit"), audit_retention_days: 14, @@ -17147,26 +17328,24 @@ mod tests { .expect("send broker request"); handle_connection(server, &config, &log, None, None, &limiter) .expect("handle refused peer"); - let response = crate::protocol::recv_json_frame::(client.as_raw_fd()) - .expect("receive refusal response") - .expect("broker sends typed refusal"); - let BrokerResponse::Error(error) = response else { - panic!("expected peer credential refusal for {operation}"); - }; - assert_eq!(error.kind, "Broker.PeerCredentialRefused"); - assert_eq!(error.operation, operation); - let rendered = format!("{} {}", error.message, error.action); - assert!(!rendered.contains(&actual_uid.to_string()), "{rendered}"); - assert!(!rendered.contains("1-2.3"), "{rendered}"); - assert!(!rendered.contains("/"), "{rendered}"); + let response = crate::protocol::recv_json_frame::(client.as_raw_fd()); + assert!( + matches!( + response, + Err(ref error) if error.kind() == io::ErrorKind::ConnectionReset + ) || matches!(response, Ok(None)), + "unauthenticated peers must be closed before request decoding: {response:?}" + ); } let audit = fs::read_to_string(log.current_daily_path()).expect("read audit log"); - for operation in operations { - assert!(audit.contains(&format!(r#""op":"{operation}""#)), "{audit}"); + for _operation in operations { + assert!(audit.contains(r#""op":"PeerAuthentication""#), "{audit}"); } assert_eq!( - audit.matches(r#""disposition":"peer-refused""#).count(), + audit + .matches(r#""disposition":"peer-refused-before-decode""#) + .count(), 4, "{audit}" ); diff --git a/packages/d2b-priv-broker/src/seccomp_compile_tests.rs b/packages/d2b-broker/src/seccomp_compile_tests.rs similarity index 100% rename from packages/d2b-priv-broker/src/seccomp_compile_tests.rs rename to packages/d2b-broker/src/seccomp_compile_tests.rs diff --git a/packages/d2b-priv-broker/src/sys.rs b/packages/d2b-broker/src/sys.rs similarity index 100% rename from packages/d2b-priv-broker/src/sys.rs rename to packages/d2b-broker/src/sys.rs diff --git a/packages/d2b-priv-broker/tests/bridge_lifecycle.rs b/packages/d2b-broker/tests/bridge_lifecycle.rs similarity index 99% rename from packages/d2b-priv-broker/tests/bridge_lifecycle.rs rename to packages/d2b-broker/tests/bridge_lifecycle.rs index 18d37ddc9c..abe67c8f1a 100644 --- a/packages/d2b-priv-broker/tests/bridge_lifecycle.rs +++ b/packages/d2b-broker/tests/bridge_lifecycle.rs @@ -2,7 +2,7 @@ use std::cell::{Cell, RefCell}; use d2b_contracts_resource::v3::IfName; use d2b_core::bundle_resolver::ResolvedBridgeIntent; -use d2b_priv_broker::ops::network::{ +use d2b_broker::ops::network::{ BridgeBackend, BridgeReadback, NetworkOpError, bridge_intent_digest, create_bridge, delete_bridge, }; diff --git a/packages/d2b-priv-broker/tests/broker_export_audit.rs b/packages/d2b-broker/tests/broker_export_audit.rs similarity index 100% rename from packages/d2b-priv-broker/tests/broker_export_audit.rs rename to packages/d2b-broker/tests/broker_export_audit.rs diff --git a/packages/d2b-priv-broker/tests/broker_protocol_compatibility.rs b/packages/d2b-broker/tests/broker_protocol_compatibility.rs similarity index 100% rename from packages/d2b-priv-broker/tests/broker_protocol_compatibility.rs rename to packages/d2b-broker/tests/broker_protocol_compatibility.rs diff --git a/packages/d2b-priv-broker/tests/broker_socket_acl.rs b/packages/d2b-broker/tests/broker_socket_acl.rs similarity index 100% rename from packages/d2b-priv-broker/tests/broker_socket_acl.rs rename to packages/d2b-broker/tests/broker_socket_acl.rs diff --git a/packages/d2b-priv-broker/tests/bundle_tampered_broker.rs b/packages/d2b-broker/tests/bundle_tampered_broker.rs similarity index 98% rename from packages/d2b-priv-broker/tests/bundle_tampered_broker.rs rename to packages/d2b-broker/tests/bundle_tampered_broker.rs index 1c13e26ad2..95e40adc23 100644 --- a/packages/d2b-priv-broker/tests/bundle_tampered_broker.rs +++ b/packages/d2b-broker/tests/bundle_tampered_broker.rs @@ -6,7 +6,7 @@ //! `"Broker.BundleResolverUnavailable"` or any other kind). //! //! The test uses the public `probe_bundle_load_response` helper exported -//! from `d2b_priv_broker::runtime`, which exercises the same +//! from `d2b_broker::runtime`, which exercises the same //! `try_load_resolver` → `BundleSlot::Tampered` → `BrokerError::BundleTampered` //! → `into_response()` pipeline as the live `serve` loop. @@ -14,7 +14,7 @@ mod broker_tampered { use d2b_contracts_broker::broker_wire::BrokerResponse; use d2b_core::bundle_resolver::BundleVerifyPolicy; - use d2b_priv_broker::runtime::{ + use d2b_broker::runtime::{ probe_bundle_load_response, probe_bundle_load_response_with_policy, }; use std::fs; diff --git a/packages/d2b-priv-broker/tests/common/mod.rs b/packages/d2b-broker/tests/common/mod.rs similarity index 92% rename from packages/d2b-priv-broker/tests/common/mod.rs rename to packages/d2b-broker/tests/common/mod.rs index 37cc82883c..c9ff88a1bb 100644 --- a/packages/d2b-priv-broker/tests/common/mod.rs +++ b/packages/d2b-broker/tests/common/mod.rs @@ -10,7 +10,7 @@ use std::time::Duration; use tempfile::{Builder, TempDir}; -const BROKER_BIN: &str = env!("CARGO_BIN_EXE_d2b-priv-broker"); +const BROKER_BIN: &str = env!("CARGO_BIN_EXE_d2b-broker"); const O_APPEND: u32 = 0o2000; pub const D2BD_UID: u32 = 4242; @@ -52,11 +52,17 @@ pub struct TestBroker { impl TestBroker { pub fn spawn(prefix: &str) -> Self { + Self::spawn_profile(prefix, "host-test", "host", D2BD_UID) + } + + pub fn spawn_profile(prefix: &str, authority_id: &str, profile: &str, d2bd_uid: u32) -> Self { let scratch = Scratch::new(prefix); let run_dir = scratch.path().join("run/d2b"); let audit_dir = scratch.path().join("var/lib/d2b/audit"); + let state_dir = scratch.path().join("var/lib/d2b/state"); fs::create_dir_all(&run_dir).expect("create broker run dir"); fs::create_dir_all(&audit_dir).expect("create broker audit dir"); + fs::create_dir_all(&state_dir).expect("create broker state dir"); let socket_path = run_dir.join("priv.sock"); let server_log_path = scratch.path().join("server.log"); @@ -65,21 +71,25 @@ impl TestBroker { let current_gid = nix::unistd::Gid::current().as_raw(); let child = Command::new(BROKER_BIN) - .arg("serve") + .arg(profile) + .arg("--authority-id") + .arg(authority_id) .arg("--socket-path") .arg(&socket_path) .arg("--audit-dir") .arg(&audit_dir) .arg("--d2bd-uid") - .arg(D2BD_UID.to_string()) + .arg(d2bd_uid.to_string()) .arg("--d2bd-gid") .arg(current_gid.to_string()) + .arg("--state-dir") + .arg(&state_dir) .arg("--test-mode") .env("RUST_LOG", "off") .stdout(Stdio::from(server_log)) .stderr(Stdio::from(server_log_err)) .spawn() - .expect("spawn d2b-priv-broker serve"); + .expect("spawn d2b-broker profile"); let broker = Self { scratch, @@ -87,7 +97,7 @@ impl TestBroker { socket_path, audit_dir, server_log_path, - d2bd_uid: D2BD_UID, + d2bd_uid, }; broker.wait_for_socket(); broker @@ -113,6 +123,10 @@ impl TestBroker { self.d2bd_uid } + pub fn socket_path(&self) -> &Path { + &self.socket_path + } + pub fn caller_gid(&self) -> u32 { nix::unistd::Gid::current().as_raw() } diff --git a/packages/d2b-broker/tests/guest_profile.rs b/packages/d2b-broker/tests/guest_profile.rs new file mode 100644 index 0000000000..e5b7abdefd --- /dev/null +++ b/packages/d2b-broker/tests/guest_profile.rs @@ -0,0 +1,146 @@ +use d2b_contracts_broker::broker_wire::BrokerProfile; + +#[path = "common/mod.rs"] +#[cfg(not(feature = "layer1-bootstrap"))] +mod common; + +#[cfg(not(feature = "layer1-bootstrap"))] +use std::os::fd::AsRawFd; + +#[cfg(not(feature = "layer1-bootstrap"))] +use common::{D2BD_UID, TestBroker}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_broker::protocol::{connect_seqpacket, recv_json_frame, send_json_frame}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_contracts::types::{BundleOpId, RoleId, ScopeId, VmId}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_contracts_broker::broker_wire::{ + ApplyNftablesRequest, BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, + RunnerRole, SpawnRunnerRequest, +}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_contracts_resource::v3::ResourceRef; + +#[test] +fn guest_profile_admits_only_local_process_effects() { + for operation in [ + "Hello", + "ValidateBundle", + "ExportBrokerAudit", + "SpawnRunner", + "OpenPidfd", + "ObserveRunner", + "SignalRunner", + "DeregisterRunnerPidfd", + "StartSystemdUnit", + "StopSystemdUnit", + ] { + assert!( + BrokerProfile::Guest.allows_operation(operation), + "guest profile should admit declared local effect {operation}" + ); + } +} + +#[test] +fn guest_profile_rejects_every_host_only_effect_class() { + for operation in [ + "ApplyNftables", + "ApplyRoute", + "ApplySysctl", + "CreateBridge", + "OpenKvm", + "OpenHidrawSecurityKey", + "OpenZoneStore", + "StoreSync", + "RunHostInstall", + "RunMigrate", + "CutoverEffect", + "UsbipBind", + "SecurityKeyOpenDevice", + ] { + assert!( + !BrokerProfile::Guest.allows_operation(operation), + "guest profile must reject host-only operation {operation}" + ); + } +} + +#[test] +#[cfg(not(feature = "layer1-bootstrap"))] +fn guest_binary_rejects_host_effects_before_bundle_mutation() { + let broker = TestBroker::spawn_profile("guest-profile-", "guest-test", "guest", D2BD_UID); + let client = connect_seqpacket(broker.socket_path()).expect("connect guest broker"); + let envelope = BrokerRequestEnvelope { + request: BrokerRequest::ApplyNftables(ApplyNftablesRequest { + bundle_nft_intent_ref: BundleOpId::new("nft:host-only"), + scope_id: ScopeId::new("env:work"), + desired_hash: None, + destroy: false, + tracing_span_id: None, + }), + caller_role: BrokerCallerRole::AdminUid { uid: D2BD_UID }, + test_peer_uid: Some(D2BD_UID), + audit_join: None, + }; + send_json_frame(client.as_raw_fd(), &envelope).expect("send host-only request"); + let response: BrokerResponse = recv_json_frame(client.as_raw_fd()) + .expect("receive guest profile response") + .expect("guest broker response"); + + let BrokerResponse::Error(error) = response else { + panic!("guest profile must return a typed denial"); + }; + assert_eq!(error.kind, "Broker.ProfileOperationDenied"); + assert_eq!(error.operation, "ApplyNftables"); + assert!(broker.audit_contents().contains("profile-operation-denied")); +} + +#[test] +#[cfg(not(feature = "layer1-bootstrap"))] +fn guest_binary_rejects_spawn_runner_before_bundle_mutation() { + let broker = + TestBroker::spawn_profile("guest-runner-profile-", "guest-test", "guest", D2BD_UID); + + for execution_ref in ["Guest/guest-vm", "Host/host"] { + let client = connect_seqpacket(broker.socket_path()).expect("connect guest broker"); + let envelope = BrokerRequestEnvelope { + request: BrokerRequest::SpawnRunner(SpawnRunnerRequest { + vm_id: VmId::new("guest-vm"), + role_id: RoleId::new("cloud-hypervisor"), + resource_ref: None, + resource_uid: None, + bundle_content_identity: None, + provider_identity: None, + template_identity: None, + generation: None, + sandbox_plan: None, + role: RunnerRole::CloudHypervisor, + bundle_runner_intent_ref: BundleOpId::new("runner:test"), + execution_ref: Some( + ResourceRef::parse(execution_ref).expect("valid execution ref"), + ), + execution_domain: None, + user_ref: None, + runtime_allocations: Vec::new(), + tracing_span_id: None, + workload_identity: None, + }), + caller_role: BrokerCallerRole::AdminUid { uid: D2BD_UID }, + test_peer_uid: Some(D2BD_UID), + audit_join: None, + }; + send_json_frame(client.as_raw_fd(), &envelope).expect("send SpawnRunner request"); + let response: BrokerResponse = recv_json_frame(client.as_raw_fd()) + .expect("receive guest profile response") + .expect("guest broker response"); + + let BrokerResponse::Error(error) = response else { + panic!("guest broker must refuse SpawnRunner before bundle dispatch"); + }; + assert_eq!(error.kind, "Broker.ProfileOperationDenied"); + assert_eq!(error.operation, "SpawnRunner"); + } + + assert!(broker.audit_contents().contains("profile-operation-denied")); +} diff --git a/packages/d2b-broker/tests/host_profile.rs b/packages/d2b-broker/tests/host_profile.rs new file mode 100644 index 0000000000..eca0757be5 --- /dev/null +++ b/packages/d2b-broker/tests/host_profile.rs @@ -0,0 +1,31 @@ +use d2b_contracts_broker::broker_wire::BrokerProfile; + +#[test] +fn host_profile_keeps_the_complete_closed_operation_catalog() { + let operations = BrokerProfile::Host.operations(); + + for operation in [ + "ApplyNftables", + "OpenZoneStore", + "SpawnRunner", + "ApplyHostGenerationHandoff", + "LaunchCutoverRunner", + "ExportBrokerAudit", + "ValidateBundle", + ] { + assert!( + operations.contains(&operation), + "host profile lost the existing operation {operation}" + ); + assert!( + BrokerProfile::Host.allows_operation(operation), + "host profile must admit {operation}" + ); + } +} + +#[test] +fn host_profile_is_not_an_open_ended_default() { + assert!(!BrokerProfile::Host.allows_operation("SelectProfile")); + assert!(!BrokerProfile::Host.allows_operation("UnknownFutureOperation")); +} diff --git a/packages/d2b-priv-broker/tests/kernel_surface.rs b/packages/d2b-broker/tests/kernel_surface.rs similarity index 99% rename from packages/d2b-priv-broker/tests/kernel_surface.rs rename to packages/d2b-broker/tests/kernel_surface.rs index 2c25969d67..1ffd16e7d7 100644 --- a/packages/d2b-priv-broker/tests/kernel_surface.rs +++ b/packages/d2b-broker/tests/kernel_surface.rs @@ -3,7 +3,7 @@ use std::io; use std::os::unix::fs::PermissionsExt; use std::process::Command; -use d2b_priv_broker::sys::path_safe; +use d2b_broker::sys::path_safe; use tempfile::TempDir; const OPEN_DIR_XDEV_HELPER_ENV: &str = "D2B_OPEN_DIR_XDEV_HELPER"; diff --git a/packages/d2b-priv-broker/tests/persistent_tap_lifecycle.rs b/packages/d2b-broker/tests/persistent_tap_lifecycle.rs similarity index 99% rename from packages/d2b-priv-broker/tests/persistent_tap_lifecycle.rs rename to packages/d2b-broker/tests/persistent_tap_lifecycle.rs index 6229dd84a1..f55fd932b7 100644 --- a/packages/d2b-priv-broker/tests/persistent_tap_lifecycle.rs +++ b/packages/d2b-broker/tests/persistent_tap_lifecycle.rs @@ -6,7 +6,7 @@ use d2b_contracts_resource::v3::{ ResourceGeneration, ResourceUid, }; -use d2b_priv_broker::ops::{ +use d2b_broker::ops::{ audit_op::OperationFields, network::{ NetworkOpError, PersistentTapBackend, PersistentTapRealization, attachment_digest, diff --git a/packages/d2b-priv-broker/tests/pidfd_handoff_scm_rights.rs b/packages/d2b-broker/tests/pidfd_handoff_scm_rights.rs similarity index 89% rename from packages/d2b-priv-broker/tests/pidfd_handoff_scm_rights.rs rename to packages/d2b-broker/tests/pidfd_handoff_scm_rights.rs index 69db4c81f6..acde7bd27f 100644 --- a/packages/d2b-priv-broker/tests/pidfd_handoff_scm_rights.rs +++ b/packages/d2b-broker/tests/pidfd_handoff_scm_rights.rs @@ -1,9 +1,9 @@ //! Integration: pidfd handoff over `SCM_RIGHTS`. //! -//! Drives the broker's existing [`d2b_priv_broker::fd_passing`] +//! Drives the broker's existing [`d2b_broker::fd_passing`] //! helpers end-to-end against a real `socketpair(SOCK_SEQPACKET)`, //! using a pidfd-shaped stand-in fd from -//! [`d2b_priv_broker::ops::pidfd::test_harness`]. +//! [`d2b_broker::ops::pidfd::test_harness`]. //! //! Assertions: //! @@ -20,9 +20,9 @@ use std::os::fd::AsRawFd; -use d2b_priv_broker::fd_passing::{recv_fds, send_fds}; -use d2b_priv_broker::ops::pidfd::test_harness::FakePidfdSpawner; -use d2b_priv_broker::ops::pidfd::{PidfdPayload, PidfdSpawner, assert_cloexec}; +use d2b_broker::fd_passing::{recv_fds, send_fds}; +use d2b_broker::ops::pidfd::test_harness::FakePidfdSpawner; +use d2b_broker::ops::pidfd::{PidfdPayload, PidfdSpawner, assert_cloexec}; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; @@ -82,7 +82,7 @@ fn scm_rights_transports_pidfd_with_cloexec_preserved() { #[test] fn reconciliation_refuses_start_time_drift() { - use d2b_priv_broker::ops::pidfd::StartTime; + use d2b_broker::ops::pidfd::StartTime; let s = FakePidfdSpawner::default(); let err = s .reconcile_drift(4242, StartTime(1_000_000), StartTime(2_000_000)) diff --git a/packages/d2b-priv-broker/tests/pidfd_real_spawner.rs b/packages/d2b-broker/tests/pidfd_real_spawner.rs similarity index 98% rename from packages/d2b-priv-broker/tests/pidfd_real_spawner.rs rename to packages/d2b-broker/tests/pidfd_real_spawner.rs index ff64f4d55d..a4428f7eb2 100644 --- a/packages/d2b-priv-broker/tests/pidfd_real_spawner.rs +++ b/packages/d2b-broker/tests/pidfd_real_spawner.rs @@ -14,7 +14,7 @@ use std::os::fd::AsRawFd; use std::path::PathBuf; -use d2b_priv_broker::ops::pidfd::{ +use d2b_broker::ops::pidfd::{ PidfdMethod, PidfdOpError, PidfdPayload, PidfdSpawner, RealPidfdSpawner, StartTime, }; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; diff --git a/packages/d2b-broker/tests/profile_separation.rs b/packages/d2b-broker/tests/profile_separation.rs new file mode 100644 index 0000000000..d116c59065 --- /dev/null +++ b/packages/d2b-broker/tests/profile_separation.rs @@ -0,0 +1,144 @@ +use d2b_broker::runtime::{BrokerMode, parse_command}; + +#[path = "common/mod.rs"] +#[cfg(not(feature = "layer1-bootstrap"))] +mod common; + +#[cfg(not(feature = "layer1-bootstrap"))] +use std::os::fd::AsRawFd; + +#[cfg(not(feature = "layer1-bootstrap"))] +use common::{D2BD_UID, TestBroker}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_broker::protocol::{connect_seqpacket, recv_json_frame, send_json_frame}; +#[cfg(not(feature = "layer1-bootstrap"))] +use d2b_contracts_broker::broker_wire::{ + BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, HelloRequest, +}; + +fn parse(args: &[&str]) -> BrokerMode { + parse_command(args.iter().map(|arg| (*arg).to_owned())).expect("profile command should parse") +} + +#[test] +fn profile_is_selected_at_process_start() { + let host = parse(&["host", "--test-mode"]); + let guest = parse(&["guest", "--test-mode"]); + + let (host_config, guest_config) = match (host, guest) { + (BrokerMode::Host(host), BrokerMode::Guest(guest)) => (host, guest), + other => panic!("unexpected broker modes: {other:?}"), + }; + + assert_eq!(host_config.profile.as_str(), "host"); + assert_eq!(guest_config.profile.as_str(), "guest"); + assert_ne!(host_config.profile, guest_config.profile); +} + +#[test] +fn each_profile_requires_its_own_instance_bindings() { + let host = parse(&[ + "host", + "--test-mode", + "--authority-id", + "host-authority", + "--socket-path", + "/run/d2b/host-broker.sock", + "--state-dir", + "/var/lib/d2b/host-broker", + "--audit-dir", + "/var/lib/d2b/host-audit", + ]); + let guest = parse(&[ + "guest", + "--test-mode", + "--authority-id", + "guest-authority", + "--d2bd-uid", + "1001", + "--socket-path", + "/run/d2b/guest-broker.sock", + "--state-dir", + "/var/lib/d2b/guest-broker", + "--audit-dir", + "/var/lib/d2b/guest-audit", + ]); + + let (host, guest) = match (host, guest) { + (BrokerMode::Host(host), BrokerMode::Guest(guest)) => (host, guest), + other => panic!("unexpected broker modes: {other:?}"), + }; + + assert_ne!(host.authority_id, guest.authority_id); + assert_ne!(host.socket_path, guest.socket_path); + assert_ne!(host.state_dir, guest.state_dir); + assert_ne!(host.audit_dir, guest.audit_dir); + assert_ne!(host.d2bd_uid, guest.d2bd_uid); +} + +#[test] +fn requests_cannot_select_a_profile() { + let error = parse_command([ + "host".to_owned(), + "guest".to_owned(), + "--test-mode".to_owned(), + ]) + .expect_err("profile must be a process-start argument, not a request-like trailing token"); + assert!(format!("{error:?}").contains("unknown host flag")); +} + +#[test] +#[cfg(not(feature = "layer1-bootstrap"))] +fn host_and_guest_instances_keep_separate_runtime_bindings() { + let host = TestBroker::spawn_profile("host-instance-", "host-instance", "host", D2BD_UID); + let guest = TestBroker::spawn_profile("guest-instance-", "guest-instance", "guest", D2BD_UID); + + assert_ne!(host.pid(), guest.pid()); + assert_ne!(host.socket_path(), guest.socket_path()); + + let host_client = connect_seqpacket(host.socket_path()).expect("connect host broker"); + let guest_client = connect_seqpacket(guest.socket_path()).expect("connect guest broker"); + let envelope = |request| BrokerRequestEnvelope { + request, + caller_role: BrokerCallerRole::AdminUid { uid: D2BD_UID }, + test_peer_uid: Some(D2BD_UID), + audit_join: None, + }; + send_json_frame( + host_client.as_raw_fd(), + &envelope(BrokerRequest::Hello(HelloRequest { + client_version: "test-0".to_owned(), + supported_features: vec![], + })), + ) + .expect("send host hello"); + send_json_frame( + guest_client.as_raw_fd(), + &envelope(BrokerRequest::Hello(HelloRequest { + client_version: "test-0".to_owned(), + supported_features: vec![], + })), + ) + .expect("send guest hello"); + + let host_response: BrokerResponse = recv_json_frame(host_client.as_raw_fd()) + .expect("receive host hello") + .expect("host hello response"); + let guest_response: BrokerResponse = recv_json_frame(guest_client.as_raw_fd()) + .expect("receive guest hello") + .expect("guest hello response"); + let BrokerResponse::Hello(host_hello) = host_response else { + panic!("host should return Hello"); + }; + let BrokerResponse::Hello(guest_hello) = guest_response else { + panic!("guest should return Hello"); + }; + assert!(host_hello.capabilities.contains(&"Hello".to_owned())); + assert!(guest_hello.capabilities.contains(&"SpawnRunner".to_owned())); + assert!( + !guest_hello + .capabilities + .contains(&"ApplyNftables".to_owned()) + ); + assert_ne!(host.audit_path(), guest.audit_path()); +} diff --git a/packages/d2b-priv-broker/tests/security_key_broker.rs b/packages/d2b-broker/tests/security_key_broker.rs similarity index 97% rename from packages/d2b-priv-broker/tests/security_key_broker.rs rename to packages/d2b-broker/tests/security_key_broker.rs index 824d62623d..1a70d82bda 100644 --- a/packages/d2b-priv-broker/tests/security_key_broker.rs +++ b/packages/d2b-broker/tests/security_key_broker.rs @@ -11,8 +11,8 @@ use d2b_contracts_broker::broker_wire::{ BrokerRequest, BrokerResponse, OpenHidrawSecurityKeyRequest, OpenHidrawSecurityKeyResponse, }; use d2b_contracts_resource::v3::ResourceRef; -use d2b_priv_broker::ops::audit_op::OperationFields; -use d2b_priv_broker::{fd_passing::recv_fds, protocol::send_json_frame_with_fds}; +use d2b_broker::ops::audit_op::OperationFields; +use d2b_broker::{fd_passing::recv_fds, protocol::send_json_frame_with_fds}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; use nix::unistd::close; use std::os::fd::{AsRawFd, OwnedFd}; diff --git a/packages/d2b-priv-broker/tests/socket_activation.rs b/packages/d2b-broker/tests/socket_activation.rs similarity index 96% rename from packages/d2b-priv-broker/tests/socket_activation.rs rename to packages/d2b-broker/tests/socket_activation.rs index 055f182272..d061fce837 100644 --- a/packages/d2b-priv-broker/tests/socket_activation.rs +++ b/packages/d2b-broker/tests/socket_activation.rs @@ -33,7 +33,7 @@ use std::time::Duration; use d2b_contracts_broker::broker_wire::{ BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, HelloRequest, }; -use d2b_priv_broker::protocol::{connect_seqpacket, recv_json_frame, send_json_frame}; +use d2b_broker::protocol::{connect_seqpacket, recv_json_frame, send_json_frame}; use nix::sys::socket::{ AddressFamily, Backlog, SockFlag, SockType, UnixAddr, bind, listen, socket, }; @@ -41,7 +41,7 @@ use tempfile::TempDir; /// Path to the broker binary under test; set by cargo when building /// integration tests for this package. -const BROKER_BIN: &str = env!("CARGO_BIN_EXE_d2b-priv-broker"); +const BROKER_BIN: &str = env!("CARGO_BIN_EXE_d2b-broker"); fn scratch_dir() -> TempDir { tempfile::tempdir().expect("tempdir") @@ -116,7 +116,7 @@ fn broker_adopts_socket_activated_fd_and_serves_hello() { let broker_bin = PathBuf::from(BROKER_BIN); assert!( broker_bin.exists(), - "broker binary not found at {}: set CARGO_BIN_EXE_d2b-priv-broker", + "broker binary not found at {}: set CARGO_BIN_EXE_d2b-broker", broker_bin.display() ); @@ -135,7 +135,7 @@ fn broker_adopts_socket_activated_fd_and_serves_hello() { let shell_cmd = format!( "LISTEN_PID=$$ LISTEN_FDS=1 LISTEN_FDNAMES=priv.sock \ exec 3>&{listen_fd_raw} \ - \"$BROKER\" serve \ + \"$BROKER\" host \ --test-mode \ --d2bd-uid \"$UID\" \ --d2bd-gid \"$GID\" \ diff --git a/packages/d2b-priv-broker/tests/w12_fd_passing_response.rs b/packages/d2b-broker/tests/w12_fd_passing_response.rs similarity index 96% rename from packages/d2b-priv-broker/tests/w12_fd_passing_response.rs rename to packages/d2b-broker/tests/w12_fd_passing_response.rs index eb67d050a0..42c59459ce 100644 --- a/packages/d2b-priv-broker/tests/w12_fd_passing_response.rs +++ b/packages/d2b-broker/tests/w12_fd_passing_response.rs @@ -17,8 +17,8 @@ use std::os::fd::AsRawFd; -use d2b_priv_broker::fd_passing::recv_fds; -use d2b_priv_broker::protocol::{recv_json_frame, send_json_frame, send_json_frame_with_fds}; +use d2b_broker::fd_passing::recv_fds; +use d2b_broker::protocol::{recv_json_frame, send_json_frame, send_json_frame_with_fds}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; use nix::unistd::{close, pipe, read, write}; use serde::{Deserialize, Serialize}; diff --git a/packages/d2b-priv-broker/tests/w15_install_migrate.rs b/packages/d2b-broker/tests/w15_install_migrate.rs similarity index 98% rename from packages/d2b-priv-broker/tests/w15_install_migrate.rs rename to packages/d2b-broker/tests/w15_install_migrate.rs index 5e3d6a78ce..fb83bb9366 100644 --- a/packages/d2b-priv-broker/tests/w15_install_migrate.rs +++ b/packages/d2b-broker/tests/w15_install_migrate.rs @@ -17,9 +17,9 @@ use d2b_core::{ manifest_v04::ManifestV04, processes::ProcessesJson, }; -use d2b_priv_broker::protocol::{recv_json_frame, send_json_frame}; +use d2b_broker::protocol::{recv_json_frame, send_json_frame}; #[cfg(not(feature = "layer1-bootstrap"))] -use d2b_priv_broker::{ +use d2b_broker::{ ops::exec_reconcile::{IpRouteVerb, ReconcileExecError, ReconcileExecutor, UsbipSubcommand}, runtime::{dispatch_run_host_install_response, dispatch_run_host_install_response_for_intent}, }; @@ -227,7 +227,7 @@ impl ReconcileExecutor for FailingWriteExecutor { &self, _key_path: &Path, _comment: &str, - ) -> Result { + ) -> Result { unreachable!("RunHostInstall should not invoke ssh-keygen") } } diff --git a/packages/d2b-priv-broker/tests/zone_store.rs b/packages/d2b-broker/tests/zone_store.rs similarity index 95% rename from packages/d2b-priv-broker/tests/zone_store.rs rename to packages/d2b-broker/tests/zone_store.rs index 96530452b1..1ad6ec2963 100644 --- a/packages/d2b-priv-broker/tests/zone_store.rs +++ b/packages/d2b-broker/tests/zone_store.rs @@ -7,9 +7,9 @@ use d2b_contracts_broker::broker_wire::{ BrokerResponse, OpenZoneStoreResponse, ZoneStoreDisposition, }; use d2b_contracts_resource::v3::storage::ZoneStoreId; -use d2b_priv_broker::fd_passing::{FdPassingError, recv_one_fd, send_fds}; -use d2b_priv_broker::ops::audit_op::OperationFields; -use d2b_priv_broker::protocol::send_json_frame_with_fds; +use d2b_broker::fd_passing::{FdPassingError, recv_one_fd, send_fds}; +use d2b_broker::ops::audit_op::OperationFields; +use d2b_broker::protocol::send_json_frame_with_fds; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; use nix::unistd::{close, pipe}; diff --git a/packages/d2b-contract-tests/policy-doc-links/docs/README.md b/packages/d2b-contract-tests/policy-doc-links/docs/README.md new file mode 120000 index 0000000000..b66e53300a --- /dev/null +++ b/packages/d2b-contract-tests/policy-doc-links/docs/README.md @@ -0,0 +1 @@ +/home/paydro/projects/d2b/.worktrees/v3-guest-u11/packages/d2b-contract-tests/policy-doc-outside.md \ No newline at end of file diff --git a/packages/d2b-contract-tests/policy-doc-links/escape.md b/packages/d2b-contract-tests/policy-doc-links/escape.md new file mode 120000 index 0000000000..b66e53300a --- /dev/null +++ b/packages/d2b-contract-tests/policy-doc-links/escape.md @@ -0,0 +1 @@ +/home/paydro/projects/d2b/.worktrees/v3-guest-u11/packages/d2b-contract-tests/policy-doc-outside.md \ No newline at end of file diff --git a/packages/d2b-contract-tests/policy-doc-outside.md b/packages/d2b-contract-tests/policy-doc-outside.md new file mode 100644 index 0000000000..06d10a57a7 --- /dev/null +++ b/packages/d2b-contract-tests/policy-doc-outside.md @@ -0,0 +1 @@ +outside diff --git a/packages/d2b-contract-tests/tests/host_json_drift.rs b/packages/d2b-contract-tests/tests/host_json_drift.rs index e36352eca6..472639f5a0 100644 --- a/packages/d2b-contract-tests/tests/host_json_drift.rs +++ b/packages/d2b-contract-tests/tests/host_json_drift.rs @@ -177,7 +177,7 @@ fn live_shape_fixture_uses_broker_canonical_ownership() { observed, Some(*expected), "host-valid.json: {key} is {observed:?}, expected {expected:?} \ - (broker-canonical, see packages/d2b-priv-broker/src/ops/nm.rs::DEFAULT_NM_CONF_PATH \ + (broker-canonical, see packages/d2b-broker/src/ops/nm.rs::DEFAULT_NM_CONF_PATH \ and packages/d2b-host/src/routes.rs::HOSTS_MANAGED_BEGIN/END)" ); } diff --git a/packages/d2b-contract-tests/tests/minijail_roles.rs b/packages/d2b-contract-tests/tests/minijail_roles.rs index d0df331ac6..3ad3ed5beb 100644 --- a/packages/d2b-contract-tests/tests/minijail_roles.rs +++ b/packages/d2b-contract-tests/tests/minijail_roles.rs @@ -517,7 +517,7 @@ fn qemu_media_activation_acl_source_splits_kvm_from_vhost_net() { #[test] fn broker_spawn_sets_no_new_privs_before_seccomp() { - let src = read_repo_file("packages/d2b-priv-broker/src/sys.rs"); + let src = read_repo_file("packages/d2b-broker/src/sys.rs"); let no_new_privs = src .find("libc::PR_SET_NO_NEW_PRIVS") .expect("broker spawn child must call PR_SET_NO_NEW_PRIVS"); diff --git a/packages/d2b-contract-tests/tests/policy_bazel_layout.rs b/packages/d2b-contract-tests/tests/policy_bazel_layout.rs index 7acb1b3f52..b1b7915c47 100644 --- a/packages/d2b-contract-tests/tests/policy_bazel_layout.rs +++ b/packages/d2b-contract-tests/tests/policy_bazel_layout.rs @@ -243,7 +243,7 @@ fn production_bazel_layout_has_one_locked_root_authority() { let cargo = read_repo_file("Cargo.toml"); for required in [ - "packages/d2b-priv-broker", + "packages/d2b-broker", "packages/d2b-guest-shell-runner", ] { assert!( diff --git a/packages/d2b-contract-tests/tests/policy_broker_dispositions.rs b/packages/d2b-contract-tests/tests/policy_broker_dispositions.rs index 4f87cfb3ba..c261f84c6c 100644 --- a/packages/d2b-contract-tests/tests/policy_broker_dispositions.rs +++ b/packages/d2b-contract-tests/tests/policy_broker_dispositions.rs @@ -4,7 +4,7 @@ //! The legacy shell gate cross-referenced //! `docs/reference/broker-w2-dispositions.md`, //! `docs/reference/schemas/v2/privileges.json`, and the broker dispatcher in -//! `packages/d2b-priv-broker/src/runtime.rs`. This Rust port keeps the +//! `packages/d2b-broker/src/runtime.rs`. This Rust port keeps the //! same closed-world assertions while evaluating the production real-wire //! dispatcher (`RealBrokerRequest::...`) instead of the retired bootstrap //! dispatcher. @@ -19,8 +19,8 @@ use serde_json::Value; const DOC_REL: &str = "docs/reference/broker-w2-dispositions.md"; const SCHEMA_REL: &str = "docs/reference/schemas/v2/privileges.json"; -const BROKER_SRC_REL: &str = "packages/d2b-priv-broker/src"; -const RUNTIME_REL: &str = "packages/d2b-priv-broker/src/runtime.rs"; +const BROKER_SRC_REL: &str = "packages/d2b-broker/src"; +const RUNTIME_REL: &str = "packages/d2b-broker/src/runtime.rs"; fn collect_rs_files(dir: &Path, out: &mut Vec) { let entries = fs::read_dir(dir).unwrap_or_else(|err| { diff --git a/packages/d2b-contract-tests/tests/policy_broker_schema.rs b/packages/d2b-contract-tests/tests/policy_broker_schema.rs index e796f011d7..3306cf87f5 100644 --- a/packages/d2b-contract-tests/tests/policy_broker_schema.rs +++ b/packages/d2b-contract-tests/tests/policy_broker_schema.rs @@ -107,7 +107,7 @@ fn read_lossy(path: &Path) -> String { // "The broker delegates bundle validation to d2b-core only." // // The committed gate runs four `grep -R` checks over -// `packages/d2b-priv-broker/src`: +// `packages/d2b-broker/src`: // (1) the source dir exists; // (2) `d2b_core::manifest` is imported (delegation target present); // (3) `validate_bundle` is called (the single validation entry point); @@ -120,7 +120,7 @@ fn read_lossy(path: &Path) -> String { // --------------------------------------------------------------------------- #[test] fn broker_delegates_bundle_validation_to_core() { - let src_rel = "packages/d2b-priv-broker/src"; + let src_rel = "packages/d2b-broker/src"; let src_dir = repo_root().join(src_rel); assert!( src_dir.is_dir(), diff --git a/packages/d2b-contract-tests/tests/policy_contracts.rs b/packages/d2b-contract-tests/tests/policy_contracts.rs index 5bde72b173..23c254b1a2 100644 --- a/packages/d2b-contract-tests/tests/policy_contracts.rs +++ b/packages/d2b-contract-tests/tests/policy_contracts.rs @@ -61,7 +61,7 @@ fn assert_files_have_no_line(files: &[&str], pattern: &str, ctx: &str) { // // Doc/code drift gate. Asserts docs/reference/tap-dag-contract.md matches the // implementation it documents: the derived-ifname scheme in -// d2b_host::ifname, the tap broker ops in d2b-priv-broker, the +// d2b_host::ifname, the tap broker ops in d2b-broker, the // host-prep DAG variant + ordering edges in d2b_host::host_prep_dag, and // the ChNetHandoffMode enum in d2b-core. Pure grep over committed sources; // no nixpkgs eval, no rust build. @@ -70,7 +70,7 @@ fn assert_files_have_no_line(files: &[&str], pattern: &str, ctx: &str) { fn tap_dag_contract_doc_matches_implementation() { const DOC: &str = "docs/reference/tap-dag-contract.md"; const IFNAME: &str = "packages/d2b-host/src/ifname.rs"; - const TAP_OPS: &str = "packages/d2b-priv-broker/src/ops/tap.rs"; + const TAP_OPS: &str = "packages/d2b-broker/src/ops/tap.rs"; const DAG: &str = "packages/d2b-host/src/host_prep_dag.rs"; const HOST_DTO: &str = "packages/d2b-core/src/host.rs"; diff --git a/packages/d2b-contract-tests/tests/policy_daemon.rs b/packages/d2b-contract-tests/tests/policy_daemon.rs index 9891a142b7..fd05d46f25 100644 --- a/packages/d2b-contract-tests/tests/policy_daemon.rs +++ b/packages/d2b-contract-tests/tests/policy_daemon.rs @@ -25,7 +25,7 @@ fn any_line_matches(content: &str, pattern: &str) -> bool { // --------------------------------------------------------------------------- // Migrated from tests/broker-systemd-unit-eval.sh. // -// Asserts d2b-priv-broker.service + d2b-priv-broker.socket are +// Asserts d2b-broker.service + d2b-broker.socket are // unconditionally configured in `nixos-modules/host-broker.nix` (NOT gated // behind `cfg.daemonExperimental.enable`), and that the canonical // socket/service shape is preserved: ListenSequentialPacket = diff --git a/packages/d2b-contract-tests/tests/policy_docs.rs b/packages/d2b-contract-tests/tests/policy_docs.rs index 4ec6d180d5..35998020b0 100644 --- a/packages/d2b-contract-tests/tests/policy_docs.rs +++ b/packages/d2b-contract-tests/tests/policy_docs.rs @@ -43,7 +43,7 @@ fn any_line_matches(content: &str, pattern: &str) -> bool { // Two halves, ported verbatim from the bash gate: // * Positive invariants - the rewrite must surface the daemon-only end-state // section explicitly, cross-reference ADR 0015, and mention d2bd / -// d2b-priv-broker.socket / SpawnRunner. +// d2b-broker.socket / SpawnRunner. // * Negative invariants - a per-line scan: any line matching a forbidden // legacy-as-live pattern is a violation UNLESS the same line also carries an // explicit historical / retired marker (matched case-insensitively). @@ -71,8 +71,8 @@ fn agents_md_reflects_daemon_only_end_state() { "AGENTS.md does not mention d2bd" ); assert!( - any_line_matches(&agents, r"d2b-priv-broker\.socket"), - "AGENTS.md does not mention d2b-priv-broker.socket (socket-activation contract)" + any_line_matches(&agents, r"d2b-broker\.socket"), + "AGENTS.md does not mention d2b-broker.socket (socket-activation contract)" ); assert!( any_line_matches(&agents, r"SpawnRunner"), @@ -631,7 +631,7 @@ fn strategy_is_concise_product_direction_without_operational_policy() { "Declarative contract", "Current direction", "d2bd", - "d2b-priv-broker", + "d2b-broker", "microVM", ] { assert!( diff --git a/packages/d2b-contract-tests/tests/policy_guest.rs b/packages/d2b-contract-tests/tests/policy_guest.rs index 3e0a76fc6a..e2b54ff72c 100644 --- a/packages/d2b-contract-tests/tests/policy_guest.rs +++ b/packages/d2b-contract-tests/tests/policy_guest.rs @@ -113,11 +113,11 @@ fn otel_relay_acl_module_not_publicly_imported_and_broker_handler_present() { nixos-modules/default.nix: {offending:?}" ); - let broker_runtime = read_repo_file("packages/d2b-priv-broker/src/runtime.rs"); + let broker_runtime = read_repo_file("packages/d2b-broker/src/runtime.rs"); assert!( broker_runtime.contains("RunnerRole::OtelHostBridge"), "otel-acl-migration-eval: broker runtime missing OtelHostBridge handler in \ - packages/d2b-priv-broker/src/runtime.rs" + packages/d2b-broker/src/runtime.rs" ); } @@ -231,7 +231,7 @@ fn guest_activation_stays_guest_systemd_only_and_restart_safe() { "activation must not route through a shell wrapper" ); assert!( - !service.contains("d2bd.service") && !service.contains("d2b-priv-broker"), + !service.contains("d2bd.service") && !service.contains("d2b-broker"), "guest activation support must not orchestrate host daemon/broker services" ); } diff --git a/packages/d2b-contract-tests/tests/policy_host_realm_relay.rs b/packages/d2b-contract-tests/tests/policy_host_realm_relay.rs index 10359746f5..4abc8d3b0b 100644 --- a/packages/d2b-contract-tests/tests/policy_host_realm_relay.rs +++ b/packages/d2b-contract-tests/tests/policy_host_realm_relay.rs @@ -78,7 +78,7 @@ fn host_cli_and_host_sources_do_not_construct_realm_relay() { fn host_daemon_broker_and_activation_do_not_store_realm_credentials() { let checked = git_listed_files(&[ "packages/d2bd/src", - "packages/d2b-priv-broker/src", + "packages/d2b-broker/src", "nixos-modules", ]); let allowlisted = BTreeSet::from([ diff --git a/packages/d2b-contract-tests/tests/policy_misc.rs b/packages/d2b-contract-tests/tests/policy_misc.rs index aba8879577..b4e28f6bb3 100644 --- a/packages/d2b-contract-tests/tests/policy_misc.rs +++ b/packages/d2b-contract-tests/tests/policy_misc.rs @@ -28,7 +28,7 @@ fn any_line_matches(content: &str, pattern: &str) -> bool { // // Asserts nixos-modules/host-polkit.nix names ONLY the daemon-only singleton // units in its launcher-group allowlist (d2bd.service, -// d2b-priv-broker.service, d2b-priv-broker.socket) and contains NO +// d2b-broker.service, d2b-broker.socket) and contains NO // references to the retired per-VM / per-env unit shapes in its executable // region (the leading comment block is allowed to name the retired shapes; // the executable code MUST NOT). Also asserts the structural invariants on the @@ -50,8 +50,8 @@ fn polkit_allowlist_daemon_only_singletons() { // search strings include the surrounding double-quotes). for unit in [ r#""d2bd.service""#, - r#""d2b-priv-broker.service""#, - r#""d2b-priv-broker.socket""#, + r#""d2b-broker.service""#, + r#""d2b-broker.socket""#, ] { assert!( polkit.contains(unit), diff --git a/packages/d2b-contract-tests/tests/policy_modules.rs b/packages/d2b-contract-tests/tests/policy_modules.rs index ac7fc0d41f..a3c7a6ac89 100644 --- a/packages/d2b-contract-tests/tests/policy_modules.rs +++ b/packages/d2b-contract-tests/tests/policy_modules.rs @@ -117,7 +117,7 @@ fn legacy_group_allowlist() -> Regex { r"nixos-modules/host-daemon\.nix:[0-9]+:[[:space:]]*users\.groups\.d2b-launchers = \{ \};[[:space:]]*", r"packages/d2b-core/src/privileges\.rs:[0-9]+:.*d2b-launcher.*", r"packages/d2b-contracts-broker/src/broker_wire\.rs:[0-9]+:.*d2b-launcher.*", - r"packages/d2b-priv-broker/src/bootstrap\.rs:[0-9]+:.*d2b-launcher.*", + r"packages/d2b-broker/src/bootstrap\.rs:[0-9]+:.*d2b-launcher.*", r"nixos-modules/privileges-json\.nix:[0-9]+:.*d2b-launcher.*", r"tests/legacy-group-name-denylist(-self-test)?\.sh:[0-9]+:.*", r"tests/group-rename-semantic-eval\.sh:[0-9]+:.*", @@ -186,7 +186,7 @@ fn vm_submodule_cutover() { // The Rust workspace dependency graph flows one way: contracts are leaves; // realm/core depend on contracts; host depends on core+contracts; the binaries // (d2b, d2bd) and the -// privileged broker (d2b-priv-broker, a sibling workspace) sit above. The +// privileged broker (d2b-broker, a sibling workspace) sit above. The // broker must NOT depend on d2bd/d2b; the CLI/daemon must NOT depend on // the broker. // This is a pure static parse of the `Cargo.toml` files. It also asserts the @@ -210,12 +210,12 @@ fn static_rust_dependency_direction() { &["d2b-core", "d2b-realm-core", "d2b-host", "d2b-contracts"], ), ( - "d2b-priv-broker", + "d2b-broker", &["d2b-core", "d2b-realm-core", "d2b-host", "d2b-contracts"], ), ]; let internal_crate = Regex::new( - r"^(d2b-core|d2b-realm-core|d2b-host|d2b-contracts|d2b-priv-broker|d2b|d2bd|xtask)$", + r"^(d2b-core|d2b-realm-core|d2b-host|d2b-contracts|d2b-broker|d2b|d2bd|xtask)$", ) .expect("valid internal-crate regex"); @@ -350,7 +350,7 @@ impl<'ast> syn::visit::Visit<'ast> for HostEffectVisitor { "std::os::unix::net", "tokio::net", "systemd", - "d2b_priv_broker", + "d2b_broker", ]; if (forbidden .iter() @@ -436,7 +436,7 @@ fn record_forbidden_use(path: Vec, forbidden: &mut Vec) { "std::os::unix::net", "tokio::net", "systemd", - "d2b_priv_broker", + "d2b_broker", ] .iter() .any(|prefix| joined == *prefix || joined.starts_with(&format!("{prefix}::"))) diff --git a/packages/d2b-contract-tests/tests/policy_observability.rs b/packages/d2b-contract-tests/tests/policy_observability.rs index 8ecd773205..268fd31b5c 100644 --- a/packages/d2b-contract-tests/tests/policy_observability.rs +++ b/packages/d2b-contract-tests/tests/policy_observability.rs @@ -37,7 +37,7 @@ const OBS_JOURNALD: &str = "packages/d2b-provider-observability-otel/src/nix/jou #[test] fn startup_tracing_avoids_host_path_fields() { let d2bd = read_repo_file("packages/d2bd/src/composition.rs"); - let broker = read_repo_file("packages/d2b-priv-broker/src/runtime.rs"); + let broker = read_repo_file("packages/d2b-broker/src/runtime.rs"); for (rel, content, forbidden) in [ ( @@ -51,7 +51,7 @@ fn startup_tracing_avoids_host_path_fields() { .as_slice(), ), ( - "packages/d2b-priv-broker/src/runtime.rs", + "packages/d2b-broker/src/runtime.rs", broker.as_str(), ["path = %config.realm_controllers_path.display()"].as_slice(), ), @@ -66,7 +66,7 @@ fn startup_tracing_avoids_host_path_fields() { for (rel, content) in [ ("packages/d2bd/src/composition.rs", d2bd.as_str()), - ("packages/d2b-priv-broker/src/runtime.rs", broker.as_str()), + ("packages/d2b-broker/src/runtime.rs", broker.as_str()), ] { assert!( content.contains(r#"config_source = "realm-controllers""#) diff --git a/packages/d2b-contract-tests/tests/policy_production_closure.rs b/packages/d2b-contract-tests/tests/policy_production_closure.rs index e9c4c23a0b..d02caf1559 100644 --- a/packages/d2b-contract-tests/tests/policy_production_closure.rs +++ b/packages/d2b-contract-tests/tests/policy_production_closure.rs @@ -23,7 +23,7 @@ const PROTECTED_CODEOWNERS_RULES: &[&str] = &[ "/Cargo.toml @vicondoa", "/Cargo.lock @vicondoa", "/packages/Cargo.guest.lock @vicondoa", - "/packages/d2b-priv-broker/Cargo.toml @vicondoa", + "/packages/d2b-broker/Cargo.toml @vicondoa", "/packages/d2b-guest-shell-runner/Cargo.toml @vicondoa", "/packages/policy-inputs/** @vicondoa", "/packages/policy-inputs/advisory-policy.json @vicondoa", diff --git a/packages/d2b-contract-tests/tests/policy_provider_crates.rs b/packages/d2b-contract-tests/tests/policy_provider_crates.rs index b86ee0ce45..70d8c60b22 100644 --- a/packages/d2b-contract-tests/tests/policy_provider_crates.rs +++ b/packages/d2b-contract-tests/tests/policy_provider_crates.rs @@ -79,7 +79,7 @@ const ALLOWED_WORKSPACE_DEPS: &[&str] = &[ /// allowlist rejected the name. const NAMED_INVERSIONS: &[(&str, &str)] = &[ ("d2bd", "the daemon"), - ("d2b-priv-broker", "the privileged broker"), + ("d2b-broker", "the privileged broker"), ("d2b-resource-store", "the Zone store"), ("d2b-resource-store-redb", "the Zone store backend"), ("d2b-resource-api", "the Zone store API"), @@ -719,7 +719,7 @@ fn the_naming_convention_reads_base_before_implementation() { /// outside the naming rule. #[test] fn non_provider_crates_are_exempt() { - for name in ["d2b-core", "d2b-contracts", "d2bd", "d2b-priv-broker"] { + for name in ["d2b-core", "d2b-contracts", "d2bd", "d2b-broker"] { assert!(!is_in_scope(name), "{name} must be out of scope"); } for name in NON_PROVIDER_PREFIXED { @@ -1201,3 +1201,61 @@ fn the_dossier_spec_id_parse_reads_a_real_dossier() { vec!["ADR-046-provider-volume-local".to_owned()] ); } + +/// Provider controllers stay on neutral EffectPorts. The fixed supervisor is +/// the only package allowed to translate a typed effect into broker wire or a +/// broker socket. +#[test] +fn provider_controllers_cannot_import_broker_clients() { + let packages = repo_root().join("packages"); + let mut violations = Vec::new(); + for entry in fs::read_dir(&packages).expect("packages directory") { + let entry = entry.expect("provider package entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if !name.starts_with("d2b-provider-") || name == "d2b-provider-supervisor" { + continue; + } + let dir = entry.path(); + if !dir.is_dir() { + continue; + } + let mut stack = vec![dir]; + while let Some(path) = stack.pop() { + for child in fs::read_dir(&path).expect("provider source directory") { + let child = child.expect("provider source entry").path(); + if child.is_dir() { + stack.push(child); + continue; + } + if !matches!( + child.extension().and_then(|ext| ext.to_str()), + Some("rs" | "toml" | "bazel") + ) { + continue; + } + let Ok(text) = fs::read_to_string(&child) else { + continue; + }; + let production = text + .lines() + .filter(|line| !line.trim_start().starts_with("//")) + .collect::>() + .join("\n"); + if production.contains("d2b_broker") + || production.contains("d2b-broker") + || production.contains("BrokerRequest") + || production.contains("BrokerResponse") + || production.contains("D2B_BROKER_SOCKET") + || production.contains("connect_seqpacket") + { + violations.push(child.display().to_string()); + } + } + } + } + assert!( + violations.is_empty(), + "Provider controllers must use neutral EffectPorts; broker client references found in {:?}", + violations + ); +} diff --git a/packages/d2b-contract-tests/tests/policy_source.rs b/packages/d2b-contract-tests/tests/policy_source.rs index 08478d27dd..894c99c1bf 100644 --- a/packages/d2b-contract-tests/tests/policy_source.rs +++ b/packages/d2b-contract-tests/tests/policy_source.rs @@ -265,9 +265,9 @@ fn no_bash_exec_fixture_coverage() { #[test] fn broker_never_shells_out_to_vm_switch_to_configuration() { let files = [ - "packages/d2b-priv-broker/src/live_handlers.rs", - "packages/d2b-priv-broker/src/runtime.rs", - "packages/d2b-priv-broker/src/ops/exec_reconcile.rs", + "packages/d2b-broker/src/live_handlers.rs", + "packages/d2b-broker/src/runtime.rs", + "packages/d2b-broker/src/ops/exec_reconcile.rs", ]; for rel in files { let content = read_repo_file(rel); @@ -429,7 +429,7 @@ fn host_prep_dag_d2b_host_reexport() { #[test] fn host_prep_dag_broker_wire_scaffolds() { let wire_rel = "packages/d2b-contracts-broker/src/broker_wire.rs"; - let runtime_rel = "packages/d2b-priv-broker/src/runtime.rs"; + let runtime_rel = "packages/d2b-broker/src/runtime.rs"; assert!( repo_path_exists(wire_rel), "host-prep-dag-eval: missing {wire_rel}" @@ -699,3 +699,54 @@ fn retired_state_policy_rejects_a_new_reader_marker() { vec!["packages/d2b/src/provider.rs: LegacyContext::from_env".to_owned()] ); } + +#[test] +fn broker_profiles_bind_before_dispatch_and_share_one_wire() { + let wire = read_repo_file("packages/d2b-contracts-broker/src/broker_wire.rs"); + let runtime = read_repo_file("packages/d2b-broker/src/runtime.rs"); + let guest = read_repo_file("nixos-modules/guest-broker.nix"); + let host = read_repo_file("nixos-modules/host-broker.nix"); + + for marker in [ + "pub enum BrokerProfile", + "HOST_OPERATION_CATALOG", + "GUEST_OPERATION_CATALOG", + "allowed_by_profile", + ] { + assert!( + wire.contains(marker), + "broker wire missing profile marker {marker}" + ); + } + for marker in [ + "peer_matches_instance", + "ProfileOperationRefused", + "--authority-id", + ] { + assert!( + runtime.contains(marker), + "broker runtime missing profile boundary marker {marker}" + ); + } + let peer_auth = runtime + .find("let (peer_uid, peer_gid, peer_pid) = peer_credentials") + .expect("peer credentials lookup"); + let decode = runtime + .find("let (envelope, request_fds) = match recv_json_frame_with_fds") + .expect("request decode"); + assert!( + peer_auth < decode, + "broker must authenticate the kernel peer before decoding the request" + ); + assert!( + guest.contains("brokerSourcePackage = d2bHostTools.broker;") + && host.contains("brokerSourcePackage = d2bHostTools.broker.overrideAttrs"), + "Host and Guest source packages must originate from d2bHostTools.broker" + ); + assert!( + guest.contains("/run/d2b/guest-broker.sock") + && guest.contains("/var/lib/d2b/guest-broker") + && guest.contains("/var/lib/d2b/guest-audit"), + "Guest broker must have separate socket, state, and audit roots" + ); +} diff --git a/packages/d2b-contract-tests/tests/policy_units.rs b/packages/d2b-contract-tests/tests/policy_units.rs index 645ad16761..5731d47436 100644 --- a/packages/d2b-contract-tests/tests/policy_units.rs +++ b/packages/d2b-contract-tests/tests/policy_units.rs @@ -506,3 +506,28 @@ fn legacy_unit_denylist_swtpm_forms() { "manifest.nix tpmService bundle-metadata string must be SKIP (file allowlist)" ); } + +#[test] +fn broker_units_are_profile_bound_and_not_legacy_serve_units() { + let host = read_repo_file_opt("nixos-modules/host-broker.nix").expect("host broker module"); + let guest = read_repo_file_opt("nixos-modules/guest-broker.nix").expect("guest broker module"); + let daemon = read_repo_file_opt("nixos-modules/host-daemon.nix").expect("host daemon module"); + + assert!(host.contains("systemd.sockets =")); + assert!(host.contains("d2b-broker = {")); + assert!(host.contains("/bin/d2b-broker host")); + assert!(host.contains("--authority-id host")); + assert!(!host.contains("/bin/d2b-broker serve")); + + assert!(guest.contains("systemd.sockets.d2b-broker-guest")); + assert!(guest.contains("systemd.services.d2b-broker-guest")); + assert!(guest.contains("/bin/d2b-broker guest")); + assert!(guest.contains("--authority-id guest-")); + assert!(guest.contains("/run/d2b/guest-broker.sock")); + assert!(guest.contains("/var/lib/d2b/guest-broker")); + assert!(guest.contains("/var/lib/d2b/guest-audit")); + assert!(!guest.contains("/bin/d2b-broker host")); + + assert!(daemon.contains("d2b-broker.socket")); + assert!(!daemon.contains("d2b-priv-broker")); +} diff --git a/packages/d2b-contracts-broker/src/broker_wire.rs b/packages/d2b-contracts-broker/src/broker_wire.rs index 14580a49fe..eb705cab71 100644 --- a/packages/d2b-contracts-broker/src/broker_wire.rs +++ b/packages/d2b-contracts-broker/src/broker_wire.rs @@ -744,6 +744,15 @@ impl BrokerRequest { } } + /// Return whether this request is admitted by a fixed broker profile. + /// + /// The profile is selected by the broker process at startup. It is not + /// carried on the wire, so a request cannot switch or widen the active + /// authority domain. + pub fn allowed_by_profile(&self, profile: BrokerProfile) -> bool { + profile.allows_request(self) + } + /// Stable category label for the audit's "opaque_target_id" /// column. Mirrors `BootstrapCall::opaque_target_id` semantics: /// classify the kind of target without leaking caller-supplied @@ -1241,10 +1250,7 @@ impl BrokerRequest { | Self::ResumeBroker => return None, }; Some(( - d2b_contracts_resource::v3::canonical_digest( - "d2b:broker-zone:v2", - scope.as_bytes(), - ), + d2b_contracts_resource::v3::canonical_digest("d2b:broker-zone:v2", scope.as_bytes()), d2b_contracts_resource::v3::canonical_digest( "d2b:broker-operation:v2", operation.as_bytes(), @@ -1271,6 +1277,194 @@ impl BrokerRequest { } } +/// Fixed privileged-broker authority profiles. +/// +/// Host and Guest use the same wire and executable, but each process starts +/// with one closed operation catalog. The catalog is deliberately kept next +/// to the wire operation names so adding a request requires an explicit +/// profile decision. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BrokerProfile { + /// Host and realm authorities may use the complete host catalog. + Host, + /// Guest authorities may use only local process effects and read-only + /// broker lifecycle operations. + Guest, +} + +impl BrokerProfile { + /// Stable process-start profile label. + pub const fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::Guest => "guest", + } + } + + /// Closed Host operation catalog. + pub const fn host_operations() -> &'static [&'static str] { + HOST_OPERATION_CATALOG + } + + /// Closed Guest operation catalog. + pub const fn guest_operations() -> &'static [&'static str] { + GUEST_OPERATION_CATALOG + } + + /// Return the operation catalog for this profile. + pub const fn operations(self) -> &'static [&'static str] { + match self { + Self::Host => Self::host_operations(), + Self::Guest => Self::guest_operations(), + } + } + + /// Check the stable operation name against the profile catalog. + pub fn allows_operation(self, operation: &str) -> bool { + self.operations().contains(&operation) + } + + /// Check both the closed catalog and profile-specific target constraints. + pub fn allows_request(self, request: &BrokerRequest) -> bool { + if !self.allows_operation(request.op_name()) { + return false; + } + match self { + Self::Host => true, + Self::Guest => match request { + BrokerRequest::SpawnRunner(request) => { + request + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + && GUEST_LOCAL_RUNNER_ROLES.contains(&request.role) + } + _ => true, + }, + } + } +} + +/// Every request currently defined by the broker wire. Host mode is closed +/// over this list rather than using an open-ended default. +pub const HOST_OPERATION_CATALOG: &[&str] = &[ + "ApplyHostGenerationHandoff", + "LaunchCutoverRunner", + "CutoverAudit", + "CutoverEffect", + "ApplyNftables", + "ApplyNftablesProjection", + "ApplyNmUnmanaged", + "ApplyRoute", + "ApplySysctl", + "BindUnixSocket", + "CreateOrReconcileUsersGroups", + "CreateBridge", + "DeleteBridge", + "CreatePersistentTap", + "DeletePersistentTap", + "CreateTapFd", + "DelegateCgroupV2", + "ExportBrokerAudit", + "Hello", + "GuestControlSign", + "InjectSecretById", + "LaunchMinijailChild", + "ModprobeIfAllowed", + "OpenCgroupDir", + "OpenDevice", + "OpenFuse", + "OpenHidrawSecurityKey", + "OpenKvm", + "QemuMediaEnroll", + "QemuMediaRefreshRegistry", + "QemuMediaBoot", + "QemuMediaSystemPowerdown", + "QemuMediaQueryStatus", + "QemuMediaQuit", + "QemuMediaAttach", + "QemuMediaDetach", + "OpenPidfd", + "OpenPeerPidfdFromAcceptedSocket", + "ObserveRunner", + "PipeWireAudio", + "StartSystemdUnit", + "CheckSystemdUserManager", + "ObserveSystemdUnit", + "OpenSystemdUnitPidfd", + "StopSystemdUnit", + "OpenZoneStore", + "OpenVhostNet", + "PauseBroker", + "PollChildReaped", + "PrepareRuntimeDir", + "PrepareStateDir", + "MigrateLegacySwtpmState", + "ReconcileStorageScope", + "ValidateLockSpec", + "PrepareStoreView", + "StoreSync", + "StoreVerify", + "ReadSecretById", + "ResumeBroker", + "RotateSecretById", + "RunHostInstall", + "RunMigrate", + "RunActivation", + "RunGc", + "RunKeysRotate", + "RunHostKeyTrust", + "RunRotateKnownHost", + "SetBridgePortFlags", + "SetSocketAcl", + "SetupMountNamespace", + "CgroupKill", + "SignalRunner", + "DeregisterRunnerPidfd", + "SpawnRunner", + "UpdateHostsFile", + "UsbipBind", + "UsbipBindFirewallRule", + "UsbipProxyReconcile", + "UsbipUnbind", + "UsbipExplicitBind", + "UsbipExplicitFirewallRule", + "ResourceActivationAudit", + "ValidateBundle", + "SeedDnsmasqLease", + "BindMountFromHardlinkFarm", + "OwnershipMatrixCheck", + "SshHostKeyPreflight", + "DiskInit", + "SecurityKeyOpenDevice", + "SecurityKeyApplyUdevRules", +]; + +/// Guest-local process and broker lifecycle effects. Host networking, +/// devices, storage, realm, cutover, and allocator operations are intentionally +/// absent from this catalog. +pub const GUEST_OPERATION_CATALOG: &[&str] = &[ + "Hello", + "ExportBrokerAudit", + "ValidateBundle", + "OpenPidfd", + "OpenPeerPidfdFromAcceptedSocket", + "ObserveRunner", + "StartSystemdUnit", + "CheckSystemdUserManager", + "ObserveSystemdUnit", + "OpenSystemdUnitPidfd", + "StopSystemdUnit", + "PollChildReaped", + "PrepareRuntimeDir", + "PrepareStateDir", + "SetupMountNamespace", + "CgroupKill", + "SignalRunner", + "DeregisterRunnerPidfd", + "SpawnRunner", +]; + /// Broker-side installer driver. The broker resolves the bundle's /// `installer:host` intent row (synthesised by /// `d2b_core::bundle_resolver` from the `host.json` + Nix-emitted @@ -3115,6 +3309,14 @@ impl RunnerRole { } } +/// Closed set of runner roles that a Guest broker may spawn locally. +/// +/// The current runner vocabulary contains only host-side VM, device, relay, +/// observability, and compositor helpers. Guest Process resources use the +/// dedicated local effect classes instead, so this set is intentionally empty +/// until a runner with an explicitly Guest-local contract is added. +pub const GUEST_LOCAL_RUNNER_ROLES: &[RunnerRole] = &[]; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SpawnRunnerRequest { @@ -4380,6 +4582,78 @@ mod tests { } } + fn spawn_runner_for_profile(role: RunnerRole, execution_ref: &str) -> BrokerRequest { + BrokerRequest::SpawnRunner(SpawnRunnerRequest { + vm_id: VmId::new("guest-vm"), + role_id: RoleId::new(role.as_str()), + resource_ref: None, + resource_uid: None, + bundle_content_identity: None, + provider_identity: None, + template_identity: None, + generation: None, + sandbox_plan: None, + role, + bundle_runner_intent_ref: BundleOpId::new("runner:test"), + execution_ref: Some(ResourceRef::parse(execution_ref).expect("valid execution ref")), + execution_domain: None, + user_ref: None, + runtime_allocations: Vec::new(), + tracing_span_id: None, + workload_identity: None, + }) + } + + #[test] + fn profile_spawn_runner_role_matrix_is_closed() { + // No current RunnerRole is Guest-local. Guest Process resources use + // the dedicated local effect classes instead; a future runner role + // must be added to this allowlist deliberately. + let guest_local_roles: &[RunnerRole] = &[]; + let all_roles = [ + RunnerRole::CloudHypervisor, + RunnerRole::QemuMedia, + RunnerRole::Virtiofsd, + RunnerRole::Swtpm, + RunnerRole::SwtpmFlush, + RunnerRole::Gpu, + RunnerRole::Audio, + RunnerRole::Video, + RunnerRole::VsockRelay, + RunnerRole::Usbip, + RunnerRole::OtelHostBridge, + RunnerRole::WaylandProxy, + ]; + + for role in all_roles { + let expected_guest = guest_local_roles.contains(&role); + assert_eq!( + spawn_runner_for_profile(role, "Guest/guest-vm") + .allowed_by_profile(BrokerProfile::Guest), + expected_guest, + "Guest profile role {} must match the closed Guest-local allowlist", + role.as_str() + ); + assert!( + !spawn_runner_for_profile(role, "Host/host") + .allowed_by_profile(BrokerProfile::Guest), + "Guest profile must reject Host execution ref for {}", + role.as_str() + ); + assert!( + spawn_runner_for_profile(role, "Guest/guest-vm") + .allowed_by_profile(BrokerProfile::Host), + "Host profile behavior changed for Guest execution ref {}", + role.as_str() + ); + assert!( + spawn_runner_for_profile(role, "Host/host").allowed_by_profile(BrokerProfile::Host), + "Host profile behavior changed for Host execution ref {}", + role.as_str() + ); + } + } + #[test] fn spawn_runner_rejects_each_legacy_authority_field() { // argv, env, uid, gid, caps, seccomp_profile, diff --git a/packages/d2b-core/src/bundle_resolver.rs b/packages/d2b-core/src/bundle_resolver.rs index b79444a41c..1ba86cb5d1 100644 --- a/packages/d2b-core/src/bundle_resolver.rs +++ b/packages/d2b-core/src/bundle_resolver.rs @@ -2591,7 +2591,7 @@ fn render_hosts_managed_block(host: &HostJson) -> String { fn build_nm_unmanaged_intents(host: &HostJson) -> BTreeMap { let mut out = BTreeMap::new(); let mut contents = String::from("# d2b-managed begin\n"); - contents.push_str("# Generated by d2b-priv-broker; do not edit by hand.\n"); + contents.push_str("# Generated by d2b-broker; do not edit by hand.\n"); contents.push_str("[keyfile]\n"); contents.push_str("unmanaged-devices="); contents.push_str(&host.network_manager.match_criteria.join(";")); diff --git a/packages/d2b-host/Cargo.toml b/packages/d2b-host/Cargo.toml index 5f100806d9..a76d04e0c7 100644 --- a/packages/d2b-host/Cargo.toml +++ b/packages/d2b-host/Cargo.toml @@ -9,12 +9,12 @@ license.workspace = true workspace = true # d2b-host is the W3 disjoint host-prepare API surface consumed by -# d2bd and d2b-priv-broker. Stub modules land in the integrator +# d2bd and d2b-broker. Stub modules land in the integrator # prep commit; W3 scope agents s1-s4 fill them in. # # Dependency rule (per W3 file-ownership map): d2b-host depends only # on d2b-core + d2b-contracts + low-level OS crates declared in the -# workspace. It must NEVER depend on d2bd or d2b-priv-broker +# workspace. It must NEVER depend on d2bd or d2b-broker # (both are downstream consumers). [features] diff --git a/packages/d2b-host/src/devices.rs b/packages/d2b-host/src/devices.rs index ba758bc42f..dd3542471f 100644 --- a/packages/d2b-host/src/devices.rs +++ b/packages/d2b-host/src/devices.rs @@ -3,7 +3,7 @@ //! Host-check does **read-only preflight** on `/dev/kvm`, `/dev/net/tun`, //! `/dev/vhost-net`, `/dev/fuse`, plus the optional accelerator/USBIP/ //! TPM/vfio matrix. Mutation (ACL fixup, fd handoff) lives in the -//! broker (`d2b_priv_broker::ops::device`); this module exposes +//! broker (`d2b_broker::ops::device`); this module exposes //! only the typed matrix + readback + per-row validators so the L1c //! `tests/device-node-matrix.sh` canary can drive it deterministically. diff --git a/packages/d2b-host/src/ioctl_policy.rs b/packages/d2b-host/src/ioctl_policy.rs index b228a31f88..97be037e1a 100644 --- a/packages/d2b-host/src/ioctl_policy.rs +++ b/packages/d2b-host/src/ioctl_policy.rs @@ -151,7 +151,7 @@ fn class_ioctls(class: DeviceClass) -> &'static [constants::Number] { // are NOT in this declarative allowlist because the broker is // the only legitimate caller and it issues those ioctls via // raw libc bypassing the per-role policy - // (packages/d2b-priv-broker/src/ops/tap.rs: + // (packages/d2b-broker/src/ops/tap.rs: // live_create_persistent_tap). Long-lived runners like // cloud-hypervisor open /dev/net/tun + TUNSETIFF to attach to // a PRE-EXISTING persistent TAP; they MUST NOT be granted the diff --git a/packages/d2b-host/src/lib.rs b/packages/d2b-host/src/lib.rs index f610a038b7..f485b122d4 100644 --- a/packages/d2b-host/src/lib.rs +++ b/packages/d2b-host/src/lib.rs @@ -12,9 +12,9 @@ //! Crate-level invariants: //! //! - `#![forbid(unsafe_code)]`: any required `unsafe` (e.g. raw netlink -//! FFI, SCM_RIGHTS fd handling) lives in `d2b-priv-broker`'s +//! FFI, SCM_RIGHTS fd handling) lives in `d2b-broker`'s //! quarantined `sys.rs`, never here. -//! - No dependency on `d2bd` or `d2b-priv-broker`. This crate +//! - No dependency on `d2bd` or `d2b-broker`. This crate //! is consumed by both; the dependency direction is one-way. #![forbid(unsafe_code)] @@ -27,7 +27,7 @@ pub mod ioctl_policy; pub mod modules; // BPF seccomp compilation from the ioctl_policy matrix. // Lives here (not d2b-core) so DeviceClass is available without -// a dep-graph cycle; d2b-priv-broker converts CompiledSeccompProgram +// a dep-graph cycle; d2b-broker converts CompiledSeccompProgram // to libc::sock_filter in its quarantined sys.rs. pub mod netlink; pub mod nftables; diff --git a/packages/d2b-host/src/modules.rs b/packages/d2b-host/src/modules.rs index 2ec55d44a0..3a1b0bb7fe 100644 --- a/packages/d2b-host/src/modules.rs +++ b/packages/d2b-host/src/modules.rs @@ -16,7 +16,7 @@ //! recommendation surfaced in the probe result. //! //! Mutation (`modprobe`) lives in the broker - see -//! `d2b_priv_broker::ops::modprobe`. This module is pure read-only +//! `d2b_broker::ops::modprobe`. This module is pure read-only //! preflight and exposes deterministic parsers so the canary matrix can //! drive it with fixtures. diff --git a/packages/d2b-host/src/routes.rs b/packages/d2b-host/src/routes.rs index 7a19570ae3..51a31ddb72 100644 --- a/packages/d2b-host/src/routes.rs +++ b/packages/d2b-host/src/routes.rs @@ -391,7 +391,7 @@ pub fn run_route_preflight(input: &RoutePreflightInput<'_>) -> Result<(), RouteP /// trusted bundle so a foreign actor that mutates routes / nft / /// hosts after `host prepare --apply` cannot let the VM come up /// against unintended state. Surfaced to the broker `Up` runtime -/// and to `d2b-priv-broker::ops::route::apply` so the same code +/// and to `d2b-broker::ops::route::apply` so the same code /// drives both call sites. pub fn run_route_preflight_for_vm( vm_id: &str, diff --git a/packages/d2b-host/src/seccomp.rs b/packages/d2b-host/src/seccomp.rs index 96c4adfcbc..750883f09c 100644 --- a/packages/d2b-host/src/seccomp.rs +++ b/packages/d2b-host/src/seccomp.rs @@ -5,7 +5,7 @@ //! compact BPF seccomp program ready for installation by the broker via //! `SECCOMP_SET_MODE_FILTER`. This module lives in `d2b-host` (not //! `d2b-core`) so that [`DeviceClass`] is available without creating -//! a dep-graph cycle; `d2b-priv-broker` depends on `d2b-host` and +//! a dep-graph cycle; `d2b-broker` depends on `d2b-host` and //! converts the [`CompiledSeccompProgram`] to `libc::sock_filter` slices //! in its quarantined `sys.rs`. //! diff --git a/packages/d2b-provider-display-wayland/src/wayland_proxy_argv.rs b/packages/d2b-provider-display-wayland/src/wayland_proxy_argv.rs index 33a8b93393..a6dd8b6907 100644 --- a/packages/d2b-provider-display-wayland/src/wayland_proxy_argv.rs +++ b/packages/d2b-provider-display-wayland/src/wayland_proxy_argv.rs @@ -1,7 +1,7 @@ //! `d2b-wayland-proxy` host-side Wayland proxy argv generator. //! //! Pure Rust function that emits the argv for the per-VM -//! `d2b--wlproxy` role spawned by `d2b-priv-broker` +//! `d2b--wlproxy` role spawned by `d2b-broker` //! via `SpawnRunner { role: WaylandProxy }`. //! //! The proxy runs as a dedicated `d2b--wlproxy` UID with: diff --git a/packages/d2b-provider-supervisor/BUILD.bazel b/packages/d2b-provider-supervisor/BUILD.bazel index 50e8db519d..12893a9008 100644 --- a/packages/d2b-provider-supervisor/BUILD.bazel +++ b/packages/d2b-provider-supervisor/BUILD.bazel @@ -11,8 +11,8 @@ rust_library( name = "d2b_provider_supervisor", srcs = glob(["src/**/*.rs"], allow_empty = True), compile_data = [ - "//packages/d2b-priv-broker:src/live_handlers.rs", # keep - "//packages/d2b-priv-broker:src/runtime.rs", # keep + "//packages/d2b-broker:src/live_handlers.rs", # keep + "//packages/d2b-broker:src/runtime.rs", # keep "Cargo.toml", ], visibility = ["//visibility:public"], @@ -32,8 +32,8 @@ rust_library( name = "d2b_provider_supervisor_test_support", srcs = glob(["src/**/*.rs"], allow_empty = True), compile_data = [ - "//packages/d2b-priv-broker:src/live_handlers.rs", # keep - "//packages/d2b-priv-broker:src/runtime.rs", # keep + "//packages/d2b-broker:src/live_handlers.rs", # keep + "//packages/d2b-broker:src/runtime.rs", # keep "Cargo.toml", ], crate_name = "d2b_provider_supervisor", diff --git a/packages/d2b-provider-supervisor/src/broker.rs b/packages/d2b-provider-supervisor/src/broker.rs index a3bae30bdd..e89ae1abe4 100644 --- a/packages/d2b-provider-supervisor/src/broker.rs +++ b/packages/d2b-provider-supervisor/src/broker.rs @@ -10,9 +10,10 @@ use std::time::Duration; use d2b_contracts::types::{BundleOpId, RoleId, VmId}; use d2b_contracts_broker::broker_wire::{ - AuditJoinContext, BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, - CanonicalAuditDigest, DeregisterRunnerPidfdRequest, ObserveRunnerRequest, OpenPidfdRequest, - RunnerRole, RunnerSignal, SandboxLaunchPlan, SignalRunnerRequest, SpawnRunnerRequest, + AuditJoinContext, BrokerCallerRole, BrokerProfile, BrokerRequest, BrokerRequestEnvelope, + BrokerResponse, CanonicalAuditDigest, DeregisterRunnerPidfdRequest, ObserveRunnerRequest, + OpenPidfdRequest, RunnerRole, RunnerSignal, SandboxLaunchPlan, SignalRunnerRequest, + SpawnRunnerRequest, }; use d2b_contracts_resource::v3::ResourceRef; use d2b_contracts_resource::v3::execution_policy::ExecutionDomain; @@ -434,6 +435,7 @@ pub struct BrokerProcessBackend { resolver: R, socket_path: PathBuf, io_timeout: Duration, + profile: BrokerProfile, caller_role: BrokerCallerRole, observations: Mutex>, } @@ -441,41 +443,64 @@ pub struct BrokerProcessBackend { impl BrokerProcessBackend { /// Build a backend using the production broker socket path. pub fn new(resolver: R) -> Self { - Self::with_socket( + Self::with_socket_profile_and_role( resolver, d2b_contracts::BROKER_SOCKET_PATH, Duration::from_secs(10), + BrokerProfile::Host, + BrokerCallerRole::NotAuthorized, ) } /// Build a backend with an explicit socket path and I/O timeout. pub fn with_socket(resolver: R, socket_path: impl Into, io_timeout: Duration) -> Self { - Self::with_socket_and_role( + Self::with_socket_profile_and_role( resolver, socket_path, io_timeout, + BrokerProfile::Host, BrokerCallerRole::NotAuthorized, ) } - /// Build a backend with an authenticated broker caller role. - pub fn with_socket_and_role( + /// Build a backend bound to one fixed broker profile and caller identity. + pub fn with_socket_profile_and_role( resolver: R, socket_path: impl Into, io_timeout: Duration, + profile: BrokerProfile, caller_role: BrokerCallerRole, ) -> Self { Self { resolver, socket_path: socket_path.into(), io_timeout, + profile, caller_role, observations: Mutex::new(BTreeMap::new()), } } + /// Build a backend with an authenticated broker caller role. + pub fn with_socket_and_role( + resolver: R, + socket_path: impl Into, + io_timeout: Duration, + caller_role: BrokerCallerRole, + ) -> Self { + Self::with_socket_profile_and_role( + resolver, + socket_path, + io_timeout, + BrokerProfile::Host, + caller_role, + ) + } + fn request(&self, request: BrokerRequest) -> Result { - if matches!(self.caller_role, BrokerCallerRole::NotAuthorized) { + if matches!(self.caller_role, BrokerCallerRole::NotAuthorized) + || !request.allowed_by_profile(self.profile) + { return Err(ProcessEffectError::LaunchFailed); } broker_round_trip( @@ -843,7 +868,7 @@ mod tests { } fn producer_live_handler_error_kind() -> &'static str { - const SOURCE: &str = include_str!("../../d2b-priv-broker/src/runtime.rs"); + const SOURCE: &str = include_str!("../../d2b-broker/src/runtime.rs"); const ARM: &str = "Self::LiveHandler(message) => error_response("; let arm = SOURCE .split_once(ARM) @@ -887,8 +912,7 @@ mod tests { #[test] fn open_pidfd_live_handler_failure_is_ambiguous_only_after_identity_drift() { - const LIVE_HANDLER_SOURCE: &str = - include_str!("../../d2b-priv-broker/src/live_handlers.rs"); + const LIVE_HANDLER_SOURCE: &str = include_str!("../../d2b-broker/src/live_handlers.rs"); for producer_error in ["PidfdRace", "PidfdOpenFailed", "ProcStatReadFailed"] { assert!(LIVE_HANDLER_SOURCE.contains(producer_error)); } diff --git a/packages/d2b-provider-supervisor/src/systemd.rs b/packages/d2b-provider-supervisor/src/systemd.rs index b219d407d2..631140eb31 100644 --- a/packages/d2b-provider-supervisor/src/systemd.rs +++ b/packages/d2b-provider-supervisor/src/systemd.rs @@ -6,7 +6,7 @@ use std::os::fd::OwnedFd; use std::sync::Mutex; use d2b_contracts_broker::broker_wire::{ - BrokerCallerRole, BrokerRequest, BrokerResponse, OpenSystemdUnitPidfdRequest, + BrokerCallerRole, BrokerProfile, BrokerRequest, BrokerResponse, OpenSystemdUnitPidfdRequest, StopSystemdUnitRequest, SystemdStopClass, SystemdUnitDomain, SystemdUnitIdentity, SystemdUnitRequest, }; @@ -462,6 +462,7 @@ pub struct BrokerSystemdEffectOwner { resolver: BundleBackedLaunchResolver, socket_path: std::path::PathBuf, io_timeout: std::time::Duration, + profile: BrokerProfile, caller_role: BrokerCallerRole, requests: Mutex>, } @@ -483,18 +484,38 @@ impl BrokerSystemdEffectOwner { socket_path: impl Into, io_timeout: std::time::Duration, caller_role: BrokerCallerRole, + ) -> Self { + Self::with_socket_profile_and_role( + resolver, + socket_path, + io_timeout, + BrokerProfile::Host, + caller_role, + ) + } + + /// Build an owner bound to one fixed broker profile and caller identity. + pub fn with_socket_profile_and_role( + resolver: BundleBackedLaunchResolver, + socket_path: impl Into, + io_timeout: std::time::Duration, + profile: BrokerProfile, + caller_role: BrokerCallerRole, ) -> Self { Self { resolver, socket_path: socket_path.into(), io_timeout, + profile, caller_role, requests: Mutex::new(BTreeMap::new()), } } fn request(&self, request: BrokerRequest) -> Result { - if matches!(self.caller_role, BrokerCallerRole::NotAuthorized) { + if matches!(self.caller_role, BrokerCallerRole::NotAuthorized) + || !request.allowed_by_profile(self.profile) + { return Err(ProcessEffectError::LaunchFailed); } broker_round_trip( diff --git a/packages/d2b-provider-system-minijail/src/lib.rs b/packages/d2b-provider-system-minijail/src/lib.rs index 295c3189b5..143b185a74 100644 --- a/packages/d2b-provider-system-minijail/src/lib.rs +++ b/packages/d2b-provider-system-minijail/src/lib.rs @@ -10,7 +10,7 @@ //! kill or a reuse. //! //! Adapted from the existing broker `SpawnRunner` plan and pidfd path -//! (`packages/d2b-priv-broker/src/ops/spawn_runner.rs` and its +//! (`packages/d2b-broker/src/ops/spawn_runner.rs` and its //! `clone3_pidfd_or_fork_fallback`) and from the daemon supervisor's pidfd //! table (`packages/d2bd/src/supervisor/pidfd_table.rs`), whose //! `(pid, start_time_ticks)` recheck is exactly the pid-reuse guard modelled diff --git a/packages/d2b-provider-volume-local/nix/storage-json.nix b/packages/d2b-provider-volume-local/nix/storage-json.nix index 8dba976028..f1d24e5447 100644 --- a/packages/d2b-provider-volume-local/nix/storage-json.nix +++ b/packages/d2b-provider-volume-local/nix/storage-json.nix @@ -37,7 +37,7 @@ let owner ? principal "user" "d2bd", group ? principal "group" "d2bd", mode ? modeForKind kind, - creator ? actor "broker" "d2b-priv-broker", + creator ? actor "broker" "d2b-broker", writers ? [ creator ], readers ? [ (actor "daemon" "d2bd") ], cleanupPolicy ? "never", @@ -208,7 +208,7 @@ let group = principal "group" "d2bd"; mode = "0750"; creator = actor "nix-module" "tmpfiles"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + writers = [ (actor "broker" "d2b-broker") ]; cleanupPolicy = "boot"; repairPolicy = "nix-activation"; leaseClass = "file-record"; @@ -258,11 +258,11 @@ let owner = principal "user" "root"; group = principal "group" "d2bd"; mode = "0750"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "broker-reconcile"; @@ -311,9 +311,9 @@ let owner = principal "user" "root"; group = principal "group" "root"; mode = "0700"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; - readers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; + readers = [ (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "broker-fail-closed"; invariants = [ "no-symlink" "root-owned-parent" "broker-opaque-id-only" "scope-authorization-required" ]; @@ -328,11 +328,11 @@ let owner = principal "user" "root"; group = principal "group" "d2bd"; mode = "0640"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "operator" "host-doctor") ]; cleanupPolicy = "boot"; @@ -348,9 +348,9 @@ let owner = principal "user" "root"; group = principal "group" "root"; mode = "0700"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; - readers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; + readers = [ (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "broker-fail-closed"; sensitivity = "secret-adjacent"; @@ -389,11 +389,11 @@ let creator = actor "nix-module" "tmpfiles"; writers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "nix-activation"; @@ -411,7 +411,7 @@ let creator = actor "nix-module" "tmpfiles"; writers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; cleanupPolicy = "boot"; repairPolicy = "nix-activation"; @@ -430,7 +430,7 @@ let creator = actor "nix-module" "tmpfiles"; writers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; cleanupPolicy = "boot"; repairPolicy = "nix-activation"; @@ -492,10 +492,10 @@ let group = principal "group" "users"; mode = "0755"; creator = actor "nix-module" "tmpfiles"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "role" "role:${name}:virtiofsd") ]; cleanupPolicy = "never"; @@ -510,10 +510,10 @@ let group = principal "group" "users"; mode = "0755"; creator = actor "nix-module" "tmpfiles"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "role" "role:${name}:virtiofsd") ]; cleanupPolicy = "cutover-only"; @@ -528,8 +528,8 @@ let owner = principal "user" "d2bd"; group = principal "group" "users"; mode = "0444"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") (actor "role" "role:${name}:virtiofsd") @@ -546,10 +546,10 @@ let group = principal "group" "users"; mode = "0755"; creator = actor "nix-module" "tmpfiles"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "role" "role:${name}:virtiofsd") ]; cleanupPolicy = "never"; @@ -563,8 +563,8 @@ let owner = principal "user" "d2bd"; group = principal "group" "users"; mode = "0755"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") (actor "role" "role:${name}:virtiofsd") @@ -580,8 +580,8 @@ let owner = principal "user" "d2bd"; group = principal "group" "users"; mode = "0755"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") ]; cleanupPolicy = "cutover-only"; repairPolicy = "broker-reconcile"; @@ -595,8 +595,8 @@ let owner = principal "user" "d2bd"; group = principal "group" "users"; mode = "0777"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") (actor "role" "role:${name}:virtiofsd") @@ -614,11 +614,11 @@ let owner = principal "user" "d2bd"; group = principal "group" "users"; mode = "0640"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "broker-reconcile"; @@ -637,12 +637,12 @@ let owner = uidPrincipal (d2bLib.stablePrincipalId ("d2b-" + name + "-swtpm")); group = gidPrincipal (d2bLib.stablePrincipalId ("d2b-" + name + "-swtpm")); mode = "0700"; - creator = actor "broker" "d2b-priv-broker"; + creator = actor "broker" "d2b-broker"; writers = [ - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "role" "role:${name}:swtpm") ]; - readers = [ (actor "broker" "d2b-priv-broker") ]; + readers = [ (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "broker-fail-closed"; sensitivity = "secret-adjacent"; @@ -656,9 +656,9 @@ let owner = principal "user" "root"; group = principal "group" "root"; mode = "0600"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; - readers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; + readers = [ (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "broker-fail-closed"; sensitivity = "secret-adjacent"; @@ -678,11 +678,11 @@ let mode = "0750"; creator = actor "nix-module" "tmpfiles"; writers = [ - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "role" "role:${name}:qemu-media") ]; readers = [ - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "role" "role:${name}:qemu-media") ]; cleanupPolicy = "never"; @@ -700,7 +700,7 @@ let mode = "0750"; creator = actor "nix-module" "tmpfiles"; writers = [ - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "role" "role:${name}:qemu-media") ]; cleanupPolicy = "boot"; @@ -725,9 +725,9 @@ let group = principal "group" "d2b-${name}-qemu-media"; mode = "0660"; creator = actor "role" "role:${name}:qemu-media"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; cleanupPolicy = "process-exit-with-proof"; repairPolicy = "broker-fail-closed"; @@ -741,9 +741,9 @@ let owner = principal "user" "root"; group = principal "group" "root"; mode = "0700"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; - readers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; + readers = [ (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "broker-fail-closed"; sensitivity = "secret-adjacent"; @@ -757,9 +757,9 @@ let owner = principal "user" "root"; group = principal "group" "root"; mode = "0600"; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; - readers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; + readers = [ (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "broker-fail-closed"; sensitivity = "secret-adjacent"; @@ -787,11 +787,11 @@ let creator = actor "nix-module" "tmpfiles"; writers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; cleanupPolicy = "never"; repairPolicy = "nix-activation"; @@ -814,11 +814,11 @@ let creator = actor "nix-module" "tmpfiles"; writers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "operator" "audio-cli") ]; cleanupPolicy = "never"; @@ -844,11 +844,11 @@ let creator = actor "nix-module" "tmpfiles"; writers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; # Advisory OFD lock file: never unlink (preserve lock semantics # across daemon restarts). cleanupPolicy "boot" means on next boot @@ -876,7 +876,7 @@ let if lib.hasPrefix "/run/" writable.path then "boot-scoped" else "persistent"; owner = uidPrincipal node.profile.uid; group = gidPrincipal node.profile.gid; - creator = actor "broker" "d2b-priv-broker"; + creator = actor "broker" "d2b-broker"; writers = [ (actor "role" "role:${dag.vm}:${node.id}") ]; readers = [ (actor "daemon" "d2bd") @@ -910,7 +910,7 @@ let writers = [ (actor "role" "role:${dag.vm}:${node.id}") ]; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; cleanupPolicy = "process-exit-with-proof"; leaseClass = "process-pidfd"; @@ -936,8 +936,8 @@ let owner = uidPrincipal op.ownerUid; group = gidPrincipal op.ownerGid; mode = modeString op.mode; - creator = actor "broker" "d2b-priv-broker"; - writers = [ (actor "broker" "d2b-priv-broker") ]; + creator = actor "broker" "d2b-broker"; + writers = [ (actor "broker" "d2b-broker") ]; readers = [ (actor "role" "role:${dag.vm}:cloud-hypervisor") ]; cleanupPolicy = "never"; repairPolicy = "broker-reconcile"; diff --git a/packages/d2b-provider-volume-local/nix/store.nix b/packages/d2b-provider-volume-local/nix/store.nix index 0f3919559f..58182a8ed1 100644 --- a/packages/d2b-provider-volume-local/nix/store.nix +++ b/packages/d2b-provider-volume-local/nix/store.nix @@ -816,11 +816,11 @@ in # the broker's Virtiofsd SpawnRunner role owns virtiofsd # supervision end-to-end, including the marker-file gate # (now enforced via `BundleResolver::ResolvedRunnerIntent`'s - # pre-spawn validation in `d2b-priv-broker::runtime`), + # pre-spawn validation in `d2b-broker::runtime`), # the hardened exec wrapper (Rust generator output in # `d2b-provider-volume-virtiofs/src/virtiofsd_argv.rs`), and the # CapabilityBoundingSet - # (broker spawn-time `set_capabilities` in `d2b-priv-broker::sys`). + # (broker spawn-time `set_capabilities` in `d2b-broker::sys`). # The drop-ins block that lived here would target a unit that # no longer exists; deleted. systemd.services = { diff --git a/packages/d2b-provider-volume-local/nix/sync-json.nix b/packages/d2b-provider-volume-local/nix/sync-json.nix index ca3c062c9f..f7648da2bd 100644 --- a/packages/d2b-provider-volume-local/nix/sync-json.nix +++ b/packages/d2b-provider-volume-local/nix/sync-json.nix @@ -130,7 +130,7 @@ let id = "lock:store-sync:${vm}"; scope = "vm:${vm}"; path = "${toString cfg.store.stateDir}/${vm}/store-view/sync.lock"; - owner = actor "broker" "d2b-priv-broker"; + owner = actor "broker" "d2b-broker"; scopeClass = "vm"; root = "state"; normalizedPath = "vms/${vm}/store-view/sync.lock"; @@ -156,9 +156,9 @@ let pathTemplate = "/run/d2b/locks/usbip/${busid}"; resourceId = null; kind = "file-record"; - ownerProcess = actor "broker" "d2b-priv-broker"; + ownerProcess = actor "broker" "d2b-broker"; allowedHolders = [ - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "daemon" "d2bd") ]; inheritancePolicy = "close-on-exec"; @@ -174,7 +174,7 @@ let }; adoptionPolicy = "reacquire-after-proof"; degradeScope = "vm"; - releaseAuthority = actor "broker" "d2b-priv-broker"; + releaseAuthority = actor "broker" "d2b-broker"; cloexecRequired = true; }; @@ -193,7 +193,7 @@ let normalizedPath = "run/d2b"; readers = [ (actor "daemon" "d2bd") - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") ]; }) (lockRoot { @@ -219,7 +219,7 @@ let owner = actor "nix-module" "tmpfiles"; normalizedPath = "run/d2b/locks/usbip"; readers = [ - (actor "broker" "d2b-priv-broker") + (actor "broker" "d2b-broker") (actor "daemon" "d2bd") ]; }) diff --git a/packages/d2b/src/doctor.rs b/packages/d2b/src/doctor.rs index 3612c7c9e2..47dcce7536 100644 --- a/packages/d2b/src/doctor.rs +++ b/packages/d2b/src/doctor.rs @@ -1426,7 +1426,7 @@ fn check_pre_ns_posture_with_reader( /// - **Pass** otherwise. /// /// The broker's in-memory `ChildReaped` replay-buffer depth is NOT yet -/// observable via a stable CLI command (`d2b-priv-broker +/// observable via a stable CLI command (`d2b-broker /// --report-state` is not yet implemented as of v1.2). When D7 lands /// the IPC mechanism, this check will be extended to query that buffer. /// For v1.2 the zombie-count probe is sufficient: zero zombies among @@ -1487,7 +1487,7 @@ fn check_broker_reap_health(entries: &PidfdEntries, report: &mut DoctorReport) { "zombies": zombies.len(), "checked": checked, // D7 replay-buffer depth probe: not yet observable. - // When `d2b-priv-broker --report-state` lands, this + // When `d2b-broker --report-state` lands, this // field will carry the actual in-memory buffer depth. "bufferDepth": null, }); diff --git a/packages/d2b/src/host.rs b/packages/d2b/src/host.rs index ac6ac96503..751983a4d6 100644 --- a/packages/d2b/src/host.rs +++ b/packages/d2b/src/host.rs @@ -497,7 +497,7 @@ fn install( "planned_steps": [ { "step": 1, - "what": "place systemd units at /etc/systemd/system/d2bd.service + d2b-priv-broker.socket" + "what": "place systemd units at /etc/systemd/system/d2bd.service + d2b-broker.socket" }, { "step": 2, diff --git a/packages/d2b/src/legacy.rs b/packages/d2b/src/legacy.rs index 45de1fcfb3..636b81ca1f 100644 --- a/packages/d2b/src/legacy.rs +++ b/packages/d2b/src/legacy.rs @@ -4001,7 +4001,7 @@ pub(super) fn daemon_down_envelope(verb: &str) -> HostErrorEnvelope { 1, "Daemon connectivity at /run/d2b/public.sock.", "d2bd is unreachable; the daemon is the only operator surface for mutating verbs.", - "Start d2bd (systemctl start d2bd d2b-priv-broker.socket) and re-run the same command. See docs/how-to/migrate-d2b-v1-0-to-v1-1.md#recovery-broker-bring-up-troubleshooting for the full bring-up checklist.", + "Start d2bd (systemctl start d2bd d2b-broker.socket) and re-run the same command. See docs/how-to/migrate-d2b-v1-0-to-v1-1.md#recovery-broker-bring-up-troubleshooting for the full bring-up checklist.", "docs/reference/error-codes.md#daemon-down", ) } @@ -4444,7 +4444,7 @@ pub(super) fn build_storage_migration_plan(manifest: &ManifestDocument) -> Stora preflight_requirements: vec![ "all d2b VMs stopped", "d2bd.service stopped", - "d2b-priv-broker.service stopped", + "d2b-broker.service stopped", "operator accepts planned downtime for the one-time storage layout cutover", "net VMs stopped; guest routing, TAP connectivity, and dependent bridge traffic will be interrupted", ], @@ -4670,7 +4670,7 @@ pub(super) fn cmd_host_install( "command": "host install", "mode": "dry-run", "planned_steps": [ - { "step": 1, "what": "place systemd units at /etc/systemd/system/d2bd.service + d2b-priv-broker.socket" }, + { "step": 1, "what": "place systemd units at /etc/systemd/system/d2bd.service + d2b-broker.socket" }, { "step": 2, "what": "write daemon-config.json to /etc/d2b/daemon-config.json with paths matching the daemon's compiled-in defaults" }, { "step": 3, "what": "bind /run/d2b/public.sock + /run/d2b/priv.sock with socket ACLs (launcher / admin groups)" }, { "step": 4, "what": if args.enable && args.start { "systemctl enable --now d2bd.service" } else if args.enable { "systemctl enable d2bd.service" } else if args.no_start { "do NOT enable; operator starts manually" } else { "neither --enable nor --start specified: leave service inactive" } }, @@ -10257,7 +10257,7 @@ pub(super) fn redact_broker_error_for_cli( format!("{op_name} failed: trusted bundle intent missing"), "The daemon reached the broker, but the trusted bundle did not contain the requested intent row.".to_owned(), format!( - "{op_name} references a bundle intent that the broker did not find. Admin: ask `journalctl -u d2b-priv-broker` for the intent id." + "{op_name} references a bundle intent that the broker did not find. Admin: ask `journalctl -u d2b-broker` for the intent id." ), ), "Broker.StoreViewFilesystemMismatch" => ( @@ -10278,7 +10278,7 @@ pub(super) fn redact_broker_error_for_cli( format!("{op_name} failed at the broker live handler"), "The daemon reached the broker and the privileged live handler started, but the underlying host mutation failed.".to_owned(), format!( - "{op_name} failed at the broker live handler. Admin: inspect `journalctl -u d2b-priv-broker` for the underlying syscall/exit code." + "{op_name} failed at the broker live handler. Admin: inspect `journalctl -u d2b-broker` for the underlying syscall/exit code." ), ), "Broker.CoexistenceRefused" => ( @@ -10292,7 +10292,7 @@ pub(super) fn redact_broker_error_for_cli( format!("{op_name} failed: bundle nft script parse error"), "The daemon reached the broker, but the nftables batch embedded in the trusted bundle could not be parsed.".to_owned(), format!( - "{op_name} failed: bundle nft script could not be parsed. Admin: inspect `journalctl -u d2b-priv-broker` for the parse error." + "{op_name} failed: bundle nft script could not be parsed. Admin: inspect `journalctl -u d2b-broker` for the parse error." ), ), "Broker.CarveoutOrderingViolation" => ( @@ -14312,7 +14312,7 @@ mod host_install_dispatch_tests { "outcome": "broker-error", "targetWave": "legacy", "summary": "RunHostInstall failed", - "remediation": "RunHostInstall failed at the broker live handler. Admin: inspect `journalctl -u d2b-priv-broker` for the underlying syscall/exit code.", + "remediation": "RunHostInstall failed at the broker live handler. Admin: inspect `journalctl -u d2b-broker` for the underlying syscall/exit code.", }), ); diff --git a/packages/d2bd-runtime/src/broker_transport.rs b/packages/d2bd-runtime/src/broker_transport.rs index ec2bd02c89..2a187ea7c9 100644 --- a/packages/d2bd-runtime/src/broker_transport.rs +++ b/packages/d2bd-runtime/src/broker_transport.rs @@ -133,7 +133,7 @@ pub fn redact_broker_error_for_launcher( "broker is starting up / bundle not yet loaded; retry shortly. Admin: confirm the bundle path is populated.".to_owned() } "Broker.BundleIntentMissing" => format!( - "{op_name} references a bundle intent that the broker did not find. Admin: ask `journalctl -u d2b-priv-broker` for the intent id." + "{op_name} references a bundle intent that the broker did not find. Admin: ask `journalctl -u d2b-broker` for the intent id." ), "Broker.StoreViewFilesystemMismatch" => format!( "{op_name} refused: the per-VM store view is not on the same filesystem as /nix/store. Admin: check the VM state dir layout and retry." @@ -142,11 +142,11 @@ pub fn redact_broker_error_for_launcher( "{op_name} refused: the prepared store-view generation is missing its marker. Admin: rebuild the store view and retry." ), "Broker.LiveHandlerFailed" => format!( - "{op_name} failed at the broker live handler. Admin: inspect `journalctl -u d2b-priv-broker` for the underlying syscall/exit code." + "{op_name} failed at the broker live handler. Admin: inspect `journalctl -u d2b-broker` for the underlying syscall/exit code." ), "Broker.CoexistenceRefused" => "{op_name} refused: another firewall manager owns the table per FirewallCoexistencePolicy. Admin: check d2b.site.firewallCoexistencePolicy." .replace("{op_name}", op_name), - "Broker.NftScriptParseFailed" => "{op_name} failed: bundle nft script could not be parsed. Admin: inspect `journalctl -u d2b-priv-broker` for the parse error." + "Broker.NftScriptParseFailed" => "{op_name} failed: bundle nft script could not be parsed. Admin: inspect `journalctl -u d2b-broker` for the parse error." .replace("{op_name}", op_name), "Broker.CarveoutOrderingViolation" => "{op_name} refused: USBIP firewall carve-out rules are out of order relative to broad allow/drop. Admin: inspect the bundle's nft batch ordering." .replace("{op_name}", op_name), @@ -174,7 +174,7 @@ pub fn redact_broker_error_for_launcher( "broker audit export requires an authorized admin user.".to_owned() } _ => format!( - "{op_name} failed; admin should inspect `journalctl -u d2b-priv-broker` for details" + "{op_name} failed; admin should inspect `journalctl -u d2b-broker` for details" ), }; (summary, remediation) diff --git a/packages/d2bd-runtime/src/supervisor/state.rs b/packages/d2bd-runtime/src/supervisor/state.rs index dc6d80e8c5..0498b92b94 100644 --- a/packages/d2bd-runtime/src/supervisor/state.rs +++ b/packages/d2bd-runtime/src/supervisor/state.rs @@ -241,7 +241,7 @@ pub fn reconcile( /// /// Pure-ish: takes an opener trait so tests can run without /proc /// or the SYS_pidfd_open syscall. The production opener calls -/// `d2b_priv_broker::sys::pidfd_sys::pidfd_open` via a thin +/// `d2b_broker::sys::pidfd_sys::pidfd_open` via a thin /// shim (the broker crate has the only `unsafe` quarantine). /// Abstraction over "open pidfd for this pid AND verify the resulting /// pidfd still refers to the process we expect". Implementations: @@ -257,7 +257,7 @@ pub fn reconcile( /// /// The production opener lives outside this module because /// `d2bd` is `#![forbid(unsafe_code)]` and `pidfd_open(2)` -/// syscall access lives in `d2b-priv-broker::sys::pidfd_sys`. +/// syscall access lives in `d2b-broker::sys::pidfd_sys`. /// The W*-fu wiring sends a `BrokerRequest::OpenPidfd { pid, /// expected_start_time }` shim over the broker socket, and the /// broker returns the verified-fd over SCM_RIGHTS. That shim is diff --git a/packages/d2bd-runtime/src/typed_error.rs b/packages/d2bd-runtime/src/typed_error.rs index 26288333d5..483353fe70 100644 --- a/packages/d2bd-runtime/src/typed_error.rs +++ b/packages/d2bd-runtime/src/typed_error.rs @@ -1172,11 +1172,11 @@ impl TypedError { .to_owned() } Self::InternalBrokerUnavailable { .. } => { - "start d2b-priv-broker or disable audit requests until the broker is available" + "start d2b-broker or disable audit requests until the broker is available" .to_owned() } Self::InternalBrokerTimeout { .. } => { - "check that d2b-priv-broker is responsive (not backlogged or half-open) and retry; the round trip exceeded the caller's deadline" + "check that d2b-broker is responsive (not backlogged or half-open) and retry; the round trip exceeded the caller's deadline" .to_owned() } Self::InternalConfig { .. } => { diff --git a/packages/d2bd/src/composition.rs b/packages/d2bd/src/composition.rs index afb80a106d..6eb1c1ad7b 100644 --- a/packages/d2bd/src/composition.rs +++ b/packages/d2bd/src/composition.rs @@ -16257,7 +16257,7 @@ fn vm_start_runner_exited_response( If this is the swtpm (per-VM TPM) runner, the TPM state must not be wiped: \ clearing /var/lib/d2b/vms/{vm}/swtpm looks like device tampering to your \ identity provider and forces re-enrollment. Admin: inspect \ - `journalctl -u d2bd` and `journalctl -u d2b-priv-broker` for the \ + `journalctl -u d2bd` and `journalctl -u d2b-broker` for the \ swtpm exit detail before retrying `d2b vm up {vm}`." ); broker_failure_response("vm start", summary, remediation, None) diff --git a/packages/d2bd/src/security_key.rs b/packages/d2bd/src/security_key.rs index d318289da2..706d63b9cb 100644 --- a/packages/d2bd/src/security_key.rs +++ b/packages/d2bd/src/security_key.rs @@ -35,7 +35,7 @@ use tracing::{debug, info}; // Hidraw device handle // --------------------------------------------------------------------------- -/// Wrapper around the hidraw fd handed off by `d2b-priv-broker` via +/// Wrapper around the hidraw fd handed off by `d2b-broker` via /// `SCM_RIGHTS`. /// /// Built from an [`OwnedFd`] via the safe `std::fs::File: From` diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json index d3102e8c84..0e4694fc84 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -5,12 +5,12 @@ "system": "aarch64-linux", "target": "aarch64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -292,6 +292,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -356,14 +364,6 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "aarch64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1814,6 +1814,144 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", @@ -2306,144 +2444,6 @@ "kind": "dev", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 2418e4f3b9..fc598bb57c 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -40,6 +40,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -48,7 +49,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "productionPackageCount": 134, "resolveNodeCount": 470, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json index 5f79cef453..dcdaa53a54 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -5,12 +5,12 @@ "system": "aarch64-linux", "target": "aarch64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -212,6 +212,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -276,14 +284,6 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "aarch64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1470,6 +1470,120 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -1848,120 +1962,6 @@ "kind": "normal", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 2418e4f3b9..fc598bb57c 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -40,6 +40,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -48,7 +49,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "productionPackageCount": 134, "resolveNodeCount": 470, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index f23d1796ac..09aa8e81c7 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -5,14 +5,14 @@ "system": "aarch64-linux", "target": "aarch64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [ "fake-backends" ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -294,6 +294,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -358,14 +366,6 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "aarch64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1816,6 +1816,144 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", @@ -2308,144 +2446,6 @@ "kind": "dev", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index dd81d84981..1e06f2361d 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -42,6 +42,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -50,7 +51,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -176,7 +176,7 @@ "productionPackageCount": 134, "resolveNodeCount": 470, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index af88af33a1..8e838a967b 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -5,14 +5,14 @@ "system": "aarch64-linux", "target": "aarch64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [ "fake-backends" ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -214,6 +214,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -278,14 +286,6 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "aarch64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1472,6 +1472,120 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -1850,120 +1964,6 @@ "kind": "normal", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index dd81d84981..1e06f2361d 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -42,6 +42,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -50,7 +51,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -176,7 +176,7 @@ "productionPackageCount": 134, "resolveNodeCount": 470, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 2a89d46e3e..c4e4be0eef 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -5,14 +5,14 @@ "system": "aarch64-linux", "target": "aarch64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [ "layer1-bootstrap" ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -294,6 +294,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -358,14 +366,6 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "aarch64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1816,6 +1816,144 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", @@ -2308,144 +2446,6 @@ "kind": "dev", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index f923c8e8a3..fb2e624df7 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -42,6 +42,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -50,7 +51,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -176,7 +176,7 @@ "productionPackageCount": 134, "resolveNodeCount": 470, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index c911d95e9c..76ead4dd75 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -5,14 +5,14 @@ "system": "aarch64-linux", "target": "aarch64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [ "layer1-bootstrap" ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -214,6 +214,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -278,14 +286,6 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "aarch64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1472,6 +1472,120 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -1850,120 +1964,6 @@ "kind": "normal", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index f923c8e8a3..fb2e624df7 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -42,6 +42,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -50,7 +51,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -176,7 +176,7 @@ "productionPackageCount": 134, "resolveNodeCount": 470, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json index 55cdedd0c6..f56d1b30c4 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json @@ -5,12 +5,12 @@ "system": "aarch64-linux", "target": "aarch64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -292,6 +292,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -356,14 +364,6 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "aarch64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1814,6 +1814,144 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", @@ -2306,144 +2444,6 @@ "kind": "dev", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json index 2418e4f3b9..fc598bb57c 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -40,6 +40,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -48,7 +49,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "productionPackageCount": 134, "resolveNodeCount": 470, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json index 9bbc040c41..66a8771171 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json @@ -5,12 +5,12 @@ "system": "aarch64-linux", "target": "aarch64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -212,6 +212,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -276,14 +284,6 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "aarch64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1470,6 +1470,120 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -1848,120 +1962,6 @@ "kind": "normal", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json index 2418e4f3b9..fc598bb57c 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -40,6 +40,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -48,7 +49,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "productionPackageCount": 134, "resolveNodeCount": 470, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json index 21828d721f..ef17d4f5eb 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json index e228944bc7..45fc3cf018 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 427, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json index c55b5f8bf9..dbd67e47c8 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json index e228944bc7..45fc3cf018 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 427, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index 31cbdf7917..58313ed82e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index a93c530ca1..db4579b7d0 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index 031c53a7bd..47df4d28dc 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index a93c530ca1..db4579b7d0 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json index 7489db8a9d..2b3f6939e2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -5,12 +5,12 @@ "system": "x86_64-linux", "target": "x86_64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -292,6 +292,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -356,14 +364,6 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "x86_64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1808,6 +1808,144 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", @@ -2300,144 +2438,6 @@ "kind": "dev", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index a8150dddb6..a2cfa5456c 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -40,6 +40,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -48,7 +49,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "productionPackageCount": 134, "resolveNodeCount": 472, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json index 58bff05633..67b3a66ad2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -5,12 +5,12 @@ "system": "x86_64-linux", "target": "x86_64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -212,6 +212,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -276,14 +284,6 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "x86_64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1464,6 +1464,120 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -1842,120 +1956,6 @@ "kind": "normal", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json index a8150dddb6..a2cfa5456c 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -40,6 +40,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -48,7 +49,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "productionPackageCount": 134, "resolveNodeCount": 472, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index ea72b0bd9e..10c7cb75c6 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -5,14 +5,14 @@ "system": "x86_64-linux", "target": "x86_64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [ "fake-backends" ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -294,6 +294,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -358,14 +366,6 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "x86_64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1810,6 +1810,144 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", @@ -2302,144 +2440,6 @@ "kind": "dev", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index dd03a7c1ff..db7091e41e 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -42,6 +42,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -50,7 +51,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -176,7 +176,7 @@ "productionPackageCount": 134, "resolveNodeCount": 472, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index 6f3416343e..339e01ad09 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -5,14 +5,14 @@ "system": "x86_64-linux", "target": "x86_64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [ "fake-backends" ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -214,6 +214,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -278,14 +286,6 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "x86_64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1466,6 +1466,120 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -1844,120 +1958,6 @@ "kind": "normal", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index dd03a7c1ff..db7091e41e 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -42,6 +42,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -50,7 +51,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -176,7 +176,7 @@ "productionPackageCount": 134, "resolveNodeCount": 472, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 3f482049a3..98461b8a78 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -5,14 +5,14 @@ "system": "x86_64-linux", "target": "x86_64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [ "layer1-bootstrap" ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -294,6 +294,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -358,14 +366,6 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "x86_64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1810,6 +1810,144 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", @@ -2302,144 +2440,6 @@ "kind": "dev", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index 375c080318..b2f36df8e4 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -42,6 +42,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -50,7 +51,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -176,7 +176,7 @@ "productionPackageCount": 134, "resolveNodeCount": 472, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index 6b3c35a94e..5e09975ff4 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -5,14 +5,14 @@ "system": "x86_64-linux", "target": "x86_64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [ "layer1-bootstrap" ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -214,6 +214,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -278,14 +286,6 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "x86_64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1466,6 +1466,120 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -1844,120 +1958,6 @@ "kind": "normal", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index 375c080318..b2f36df8e4 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -42,6 +42,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -50,7 +51,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -176,7 +176,7 @@ "productionPackageCount": 134, "resolveNodeCount": 472, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json index 599b8296ce..57bb9d5c86 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json @@ -5,12 +5,12 @@ "system": "x86_64-linux", "target": "x86_64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -292,6 +292,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -356,14 +364,6 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "x86_64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1808,6 +1808,144 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", @@ -2300,144 +2438,6 @@ "kind": "dev", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tempfile@3.27.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "dev", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json index a8150dddb6..a2cfa5456c 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -40,6 +40,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -48,7 +49,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "productionPackageCount": 134, "resolveNodeCount": 472, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/Cargo.lock index 6eac1857f8..edda506ffd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/Cargo.lock @@ -524,6 +524,33 @@ dependencies = [ ] +[[package]] +name = "d2b-broker" +version = "0.0.0-bootstrap" +dependencies = [ + "d2b-audit", + "d2b-contracts", + "d2b-contracts-broker", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "d2b-core", + "d2b-host", + "d2b-realm-core", + "hmac", + "nix 0.29.0", + "rustix 0.38.44", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + + [[package]] name = "d2b-contracts" version = "0.0.0-bootstrap" @@ -653,33 +680,6 @@ dependencies = [ ] -[[package]] -name = "d2b-priv-broker" -version = "0.0.0-bootstrap" -dependencies = [ - "d2b-audit", - "d2b-contracts", - "d2b-contracts-broker", - "d2b-contracts-control", - "d2b-contracts-resource", - "d2b-contracts-zone-session", - "d2b-core", - "d2b-host", - "d2b-realm-core", - "hmac", - "nix 0.29.0", - "rustix 0.38.44", - "serde", - "serde_json", - "sha2", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - - [[package]] name = "d2b-realm-core" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json index 94fa81e349..a2c52d7166 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json @@ -5,12 +5,12 @@ "system": "x86_64-linux", "target": "x86_64-unknown-linux-gnu", "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -212,6 +212,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-broker@0.0.0-bootstrap#path", + "name": "d2b-broker", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-contracts-broker@0.0.0-bootstrap#path", "name": "d2b-contracts-broker", @@ -276,14 +284,6 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, - { - "id": "d2b-priv-broker@0.0.0-bootstrap#path", - "name": "d2b-priv-broker", - "version": "0.0.0-bootstrap", - "source": null, - "checksum": null, - "target": "x86_64-unknown-linux-gnu" - }, { "id": "d2b-realm-core@0.0.0-bootstrap#path", "name": "d2b-realm-core", @@ -1464,6 +1464,120 @@ "kind": "normal", "target": null }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-audit@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-broker@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-contracts@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-host@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "d2b-realm-core@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-broker@0.0.0-bootstrap#path", + "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -1842,120 +1956,6 @@ "kind": "normal", "target": null }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-audit@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-broker@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-control@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-resource@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-contracts@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-host@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "d2b-realm-core@0.0.0-bootstrap#path", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "hmac@0.12.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "nix@0.29.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing-subscriber@0.3.23#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "tracing@0.1.44#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, - { - "from": "d2b-priv-broker@0.0.0-bootstrap#path", - "to": "zbus@5.19.0#registry+https://github.com/rust-lang/crates.io-index", - "kind": "normal", - "target": null - }, { "from": "d2b-realm-core@0.0.0-bootstrap#path", "to": "d2b-contracts@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json index a8150dddb6..a2cfa5456c 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -40,6 +40,7 @@ "crossbeam@0.8.4#registry+https://github.com/rust-lang/crates.io-index", "crypto-common@0.1.7#registry+https://github.com/rust-lang/crates.io-index", "d2b-audit@0.0.0-bootstrap#path", + "d2b-broker@0.0.0-bootstrap#path", "d2b-contracts-broker@0.0.0-bootstrap#path", "d2b-contracts-control@0.0.0-bootstrap#path", "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -48,7 +49,6 @@ "d2b-contracts@0.0.0-bootstrap#path", "d2b-core@0.0.0-bootstrap#path", "d2b-host@0.0.0-bootstrap#path", - "d2b-priv-broker@0.0.0-bootstrap#path", "d2b-realm-core@0.0.0-bootstrap#path", "d2b-telemetry@0.0.0-bootstrap#path", "digest@0.10.7#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "productionPackageCount": 134, "resolveNodeCount": 472, "roots": [ - "d2b-priv-broker" + "d2b-broker" ], "schemaVersion": 1, "sourceAuthority": "Cargo.lock", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json index b9f9b0e389..4d25f13240 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json index d3d552f511..75b984cbcb 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 429, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json index 3b4eedeaf0..e5f1ce15fd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json index d3d552f511..75b984cbcb 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 429, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index 16f46af033..026177311f 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 93a1406fe3..27e534f7d9 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index 3a8376affc..63c63f7fa1 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 93a1406fe3..27e534f7d9 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "5f934c9ef42729023c20b7dd5bcf6e1bc188c145f12fcd7a4ac2cf2a51c8345a", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/xtask/src/process_marker_pin.rs b/packages/xtask/src/process_marker_pin.rs index b965b91252..0074487bb6 100644 --- a/packages/xtask/src/process_marker_pin.rs +++ b/packages/xtask/src/process_marker_pin.rs @@ -9,7 +9,7 @@ use std::{ const PIN_REL: &str = "tests/golden/pinned/process-marker-legacy-paths.json"; const SCHEMA_VERSION: u32 = 1; const FROZEN_UNIVERSE_SHA256: &str = - "0f6899e939fd8e0b49f41b56a0221f33552d79348adc2853927d338e610f8f34"; + "732bf6730ae93f66305eadb338f9f81dba190fafa553cd0a8ca21b5bae16ad5e"; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/packages/xtask/src/production_closure.rs b/packages/xtask/src/production_closure.rs index 6c4b34af18..73350a9982 100644 --- a/packages/xtask/src/production_closure.rs +++ b/packages/xtask/src/production_closure.rs @@ -28,7 +28,7 @@ const PROTECTED_CODEOWNERS_RULES: &[&str] = &[ "/Cargo.toml @vicondoa", "/Cargo.lock @vicondoa", "/packages/Cargo.guest.lock @vicondoa", - "/packages/d2b-priv-broker/Cargo.toml @vicondoa", + "/packages/d2b-broker/Cargo.toml @vicondoa", "/packages/d2b-guest-shell-runner/Cargo.toml @vicondoa", "/packages/policy-inputs/** @vicondoa", "/packages/policy-inputs/advisory-policy.json @vicondoa", @@ -177,7 +177,7 @@ pub fn context_specs(root: &Path) -> Result, String> { .filter(|name| { !matches!( *name, - "d2b-priv-broker" | "d2b-guest-shell-runner" | "d2b-contract-tests" | "xtask" + "d2b-broker" | "d2b-guest-shell-runner" | "d2b-contract-tests" | "xtask" ) && (*name == "d2b" || name.starts_with("d2b-")) }) .map(str::to_owned) @@ -206,7 +206,7 @@ pub fn context_specs(root: &Path) -> Result, String> { system: system.to_owned(), target: gnu.clone(), name: "broker-production".to_owned(), - roots: vec!["d2b-priv-broker".to_owned()], + roots: vec!["d2b-broker".to_owned()], features: Vec::new(), default_features: false, source_authority: "Cargo.lock".to_owned(), @@ -221,7 +221,7 @@ pub fn context_specs(root: &Path) -> Result, String> { system: system.to_owned(), target: gnu.clone(), name: name.to_owned(), - roots: vec!["d2b-priv-broker".to_owned()], + roots: vec!["d2b-broker".to_owned()], features: feature.into_iter().map(str::to_owned).collect(), default_features: false, source_authority: "Cargo.lock".to_owned(), diff --git a/packages/xtask/tests/policy_workspace.rs b/packages/xtask/tests/policy_workspace.rs index 73876c1ea5..65fb3982da 100644 --- a/packages/xtask/tests/policy_workspace.rs +++ b/packages/xtask/tests/policy_workspace.rs @@ -17,7 +17,7 @@ const RELEASE_BINARY_SELECTORS: &[(&str, &str, &str)] = &[ "Cargo.toml", ), ("d2b-host", "d2b-activation-helper", "Cargo.toml"), - ("d2b-priv-broker", "d2b-priv-broker", "Cargo.toml"), + ("d2b-broker", "d2b-broker", "Cargo.toml"), ]; fn repo_root() -> PathBuf { let mut candidates = Vec::new(); @@ -304,8 +304,8 @@ fn release_workflow_keeps_exact_locked_workspace_selectors() { ), ( workflow.replace( - "--manifest-path Cargo.toml \\\n --package d2b-priv-broker", - "--manifest-path packages/d2b-priv-broker/Cargo.toml \\\n --package d2b-priv-broker", + "--manifest-path Cargo.toml \\\n --package d2b-broker", + "--manifest-path packages/d2b-broker/Cargo.toml \\\n --package d2b-broker", ), "broker manifest selector", ), diff --git a/tests/fixtures/gen-w3-cli-goldens.py b/tests/fixtures/gen-w3-cli-goldens.py index 6339b2c253..e140045539 100644 --- a/tests/fixtures/gen-w3-cli-goldens.py +++ b/tests/fixtures/gen-w3-cli-goldens.py @@ -326,7 +326,7 @@ def envelope(kind, code, exit_code, what_was_checked, observed_state, remediatio "host-check-error", 1, "Whether the broker can create a TAP fd via TUNSETIFF.", "TUNSETIFF returned EPERM; CAP_NET_ADMIN missing from the broker capability set.", - "Ensure d2b-priv-broker.service carries CAP_NET_ADMIN.", + "Ensure d2b-broker.service carries CAP_NET_ADMIN.", ), ("host-check", "stale-lock"): ( "host-check-error", 1, diff --git a/tests/golden/cli-output/host-check-tap-creation-denied.json b/tests/golden/cli-output/host-check-tap-creation-denied.json index 9b5a320bfb..3faacbeb8d 100644 --- a/tests/golden/cli-output/host-check-tap-creation-denied.json +++ b/tests/golden/cli-output/host-check-tap-creation-denied.json @@ -4,6 +4,6 @@ "exit_code": 1, "kind": "host-check-error", "observed_state": "TUNSETIFF returned EPERM; CAP_NET_ADMIN missing from the broker capability set.", - "remediation": "Ensure d2b-priv-broker.service carries CAP_NET_ADMIN.", + "remediation": "Ensure d2b-broker.service carries CAP_NET_ADMIN.", "what_was_checked": "Whether the broker can create a TAP fd via TUNSETIFF." } diff --git a/tests/golden/cli-output/host-check-tap-creation-denied.txt b/tests/golden/cli-output/host-check-tap-creation-denied.txt index d3f6a346e0..bb42bbf586 100644 --- a/tests/golden/cli-output/host-check-tap-creation-denied.txt +++ b/tests/golden/cli-output/host-check-tap-creation-denied.txt @@ -3,5 +3,5 @@ error: host-check-error exit_code: 1 what_was_checked: Whether the broker can create a TAP fd via TUNSETIFF. observed_state: TUNSETIFF returned EPERM; CAP_NET_ADMIN missing from the broker capability set. - remediation: Ensure d2b-priv-broker.service carries CAP_NET_ADMIN. + remediation: Ensure d2b-broker.service carries CAP_NET_ADMIN. docs_anchor: docs/reference/error-codes.md#tap-creation-denied diff --git a/tests/golden/cli-output/host-install-dry-run-json.golden b/tests/golden/cli-output/host-install-dry-run-json.golden index 17a2356ebc..bf61cdd2bc 100644 --- a/tests/golden/cli-output/host-install-dry-run-json.golden +++ b/tests/golden/cli-output/host-install-dry-run-json.golden @@ -6,7 +6,7 @@ "planned_steps": [ { "step": 1, - "what": "place systemd units at /etc/systemd/system/d2bd.service + d2b-priv-broker.socket" + "what": "place systemd units at /etc/systemd/system/d2bd.service + d2b-broker.socket" }, { "step": 2, diff --git a/tests/golden/pinned/broker-export-audit.txt b/tests/golden/pinned/broker-export-audit.txt index 156fdd94bd..dfb2403d41 100644 --- a/tests/golden/pinned/broker-export-audit.txt +++ b/tests/golden/pinned/broker-export-audit.txt @@ -1,4 +1,4 @@ -# broker-export-audit - d2b-priv-broker integration test successor. +# broker-export-audit - d2b-broker integration test successor. # Runs under the broker workspace `cargo test --features layer1-bootstrap` # pass already exercised by tests/tools/rust-workspace-checks.sh and enumerated by # tests/tools/assert-pinned-tests.sh. One bare libtest path per line. diff --git a/tests/golden/pinned/broker-scm-rights.txt b/tests/golden/pinned/broker-scm-rights.txt index e1addd5722..5285037fdd 100644 --- a/tests/golden/pinned/broker-scm-rights.txt +++ b/tests/golden/pinned/broker-scm-rights.txt @@ -1,4 +1,4 @@ -# broker-scm-rights-fd-lifecycle (d2b-priv-broker, --features layer1-bootstrap) +# broker-scm-rights-fd-lifecycle (d2b-broker, --features layer1-bootstrap) fd_passing::tests::scm_rights_fd_lifecycle_accepts_and_returns_pipe_fd fd_passing::tests::scm_rights_fd_lifecycle_cleans_up_on_broker_restart fd_passing::tests::scm_rights_fd_lifecycle_closes_own_copy_on_error diff --git a/tests/golden/pinned/broker-socket-acl.txt b/tests/golden/pinned/broker-socket-acl.txt index 9ddf0d9809..6f5b53578b 100644 --- a/tests/golden/pinned/broker-socket-acl.txt +++ b/tests/golden/pinned/broker-socket-acl.txt @@ -1,4 +1,4 @@ -# broker-socket-acl - d2b-priv-broker integration test successor. +# broker-socket-acl - d2b-broker integration test successor. # Runs under the broker workspace `cargo test --features layer1-bootstrap` # pass already exercised by tests/tools/rust-workspace-checks.sh and enumerated by # tests/tools/assert-pinned-tests.sh. One bare libtest path per line. diff --git a/tests/golden/pinned/host-prepare-network.txt b/tests/golden/pinned/host-prepare-network.txt index 9946a524b1..efa0e3b973 100644 --- a/tests/golden/pinned/host-prepare-network.txt +++ b/tests/golden/pinned/host-prepare-network.txt @@ -1,7 +1,7 @@ -# host-prepare-network - d2b-host + d2b-priv-broker network-reconcile +# host-prepare-network - d2b-host + d2b-broker network-reconcile # #[test]s. Successor of tests/host-prepare-network.sh, which ran a set of # focused `cargo test -p d2b-host --all-features -- ` and -# `cargo test -p d2b-priv-broker --all-features -- ` invocations. +# `cargo test -p d2b-broker --all-features -- ` invocations. # Every test matched by those substring filters is present in the default # `cargo test --workspace` host list and the broker default + layer1-bootstrap # lists, all gated by tests/tools/rust-workspace-checks.sh. Pinned fail-closed via @@ -22,7 +22,7 @@ routes::tests::foreign_default_route_fails_closed routes::tests::dnsmasq_not_bound_fails_closed routes::tests::host_lan_cidr_ambiguous_for_vpn # -# d2b-priv-broker (broker-workspace cargo test --workspace [+ layer1-bootstrap]): +# d2b-broker (broker-workspace cargo test --workspace [+ layer1-bootstrap]): ops::nm::tests::managed_foreign_conflict_detected ops::nm::tests::reload_command_select_by_version ops::nft::tests::projection_apply_preserves_sibling_and_usbip_entries diff --git a/tests/golden/pinned/kernel-canaries.txt b/tests/golden/pinned/kernel-canaries.txt index 0b217a44ee..9499590e5a 100644 --- a/tests/golden/pinned/kernel-canaries.txt +++ b/tests/golden/pinned/kernel-canaries.txt @@ -18,7 +18,7 @@ modules::tests::loaded_module_passes_even_with_modules_disabled modules::tests::modules_disabled_locks_required_absent_module modules::tests::parse_modules_builtin_strips_path_and_compression_suffix modules::tests::parse_proc_modules_extracts_names -# broker workspace (d2b-priv-broker): ops::device / ops::modprobe canaries +# broker workspace (d2b-broker): ops::device / ops::modprobe canaries ops::device::tests::allowed_when_role_declares_class_and_matrix_validates ops::device::tests::denied_role_class_mismatch ops::device::tests::denied_not_in_matrix_for_undeclared_path diff --git a/tests/golden/pinned/path-safety-violation-fs.txt b/tests/golden/pinned/path-safety-violation-fs.txt index afa5baca98..91b3060f28 100644 --- a/tests/golden/pinned/path-safety-violation-fs.txt +++ b/tests/golden/pinned/path-safety-violation-fs.txt @@ -1,7 +1,7 @@ # path-safety-violation-fs - broker filesystem-mutating op path-safety # #[test]s. Successor of tests/path-safety-violation-fs.sh, whose hard # assertions ran these broker #[test]s via substring filters -# (`cargo test --all-features -- ` in packages/d2b-priv-broker); +# (`cargo test --all-features -- ` in packages/d2b-broker); # each name resolves to exactly one test below, and they already run in the # broker layer1-bootstrap pass in tests/tools/rust-workspace-checks.sh. Pinned # fail-closed via tests/tools/assert-pinned-tests.sh. diff --git a/tests/golden/pinned/process-marker-legacy-paths.json b/tests/golden/pinned/process-marker-legacy-paths.json index a7a9a2ebbf..c645c2ddba 100644 --- a/tests/golden/pinned/process-marker-legacy-paths.json +++ b/tests/golden/pinned/process-marker-legacy-paths.json @@ -11,6 +11,10 @@ "docs/reference/wave-evidence-schema.md", "nixos-modules/host-activation.nix", "nixos-modules/host.nix", + "packages/d2b-broker/Cargo.toml", + "packages/d2b-broker/src/ops/store_sync_audit.rs", + "packages/d2b-broker/src/ops/tap.rs", + "packages/d2b-broker/src/runtime.rs", "packages/d2b-contract-tests/tests/minijail_profiles.rs", "packages/d2b-contract-tests/tests/policy_guest.rs", "packages/d2b-contract-tests/tests/policy_restart_adoption.rs", @@ -36,10 +40,6 @@ "packages/d2b-guestd/src/exec_pty.rs", "packages/d2b-host/Cargo.toml", "packages/d2b-host/src/hardlink_farm.rs", - "packages/d2b-priv-broker/Cargo.toml", - "packages/d2b-priv-broker/src/ops/store_sync_audit.rs", - "packages/d2b-priv-broker/src/ops/tap.rs", - "packages/d2b-priv-broker/src/runtime.rs", "packages/d2b-realm-provider/src/credential.rs", "packages/d2b-realm-router/src/display_transport.rs", "packages/d2b-realm-router/src/secure_session.rs", @@ -65,6 +65,10 @@ "packages/d2b-contracts/tests/version_skew.rs", "packages/d2b-core/src/privileges_w3.rs", "packages/d2b-host/src/runner_shape.rs", + "packages/d2b-priv-broker/Cargo.toml", + "packages/d2b-priv-broker/src/ops/store_sync_audit.rs", + "packages/d2b-priv-broker/src/ops/tap.rs", + "packages/d2b-priv-broker/src/runtime.rs", "packages/d2b-provider-aca/Cargo.toml", "packages/d2b-provider-relay/src/bin/d2b-relay.rs", "packages/d2b/src/lib.rs", diff --git a/tests/golden/pinned/usbip-firewall-skeleton.txt b/tests/golden/pinned/usbip-firewall-skeleton.txt index fc6ee65829..fb46d52a5e 100644 --- a/tests/golden/pinned/usbip-firewall-skeleton.txt +++ b/tests/golden/pinned/usbip-firewall-skeleton.txt @@ -4,7 +4,7 @@ # --all-features -- nftables::tests::{usbip_carveout_inserted_before_generic, # comment_marker_prefix_on_every_managed_rule, # fake_backend_refuses_carveout_after_generic} -# - cargo test --manifest-path packages/d2b-priv-broker/Cargo.toml +# - cargo test --manifest-path packages/d2b-broker/Cargo.toml # --all-features -- ops::usbip_firewall::tests::{ # bind_firewall_rule_produces_audit_with_busid_and_hash, # carveout_ordering_invariant_via_op, @@ -23,7 +23,7 @@ nftables::tests::usbip_carveout_inserted_before_generic nftables::tests::comment_marker_prefix_on_every_managed_rule nftables::tests::fake_backend_refuses_carveout_after_generic # -# d2b-priv-broker (broker-workspace cargo test --workspace [+ layer1-bootstrap]): +# d2b-broker (broker-workspace cargo test --workspace [+ layer1-bootstrap]): ops::usbip_firewall::tests::bind_firewall_rule_produces_audit_with_busid_and_hash ops::usbip_firewall::tests::carveout_ordering_invariant_via_op ops::usbip_firewall::tests::w6_ops_refused_with_unknown_operation_audit diff --git a/tests/host-integration/cutover-rehearsal.nix b/tests/host-integration/cutover-rehearsal.nix index 6b61656275..0c5a4fe80e 100644 --- a/tests/host-integration/cutover-rehearsal.nix +++ b/tests/host-integration/cutover-rehearsal.nix @@ -175,7 +175,7 @@ pkgs.testers.runNixOSTest { start_all() machine.wait_for_unit("d2bd.service") - machine.wait_for_unit("d2b-priv-broker.socket") + machine.wait_for_unit("d2b-broker.socket") machine.wait_for_file("/run/d2b/public.sock") # The daemon-only end state has exactly three root-visible persistent @@ -184,8 +184,8 @@ pkgs.testers.runNixOSTest { declared = machine.succeed("cat /etc/d2b/daemon-acceptance-units").splitlines() assert declared == [ "d2bd.service", - "d2b-priv-broker.socket", - "d2b-priv-broker.service", + "d2b-broker.socket", + "d2b-broker.service", ] machine.succeed( "test -z \"$(systemctl list-unit-files --no-legend " diff --git a/tests/host-integration/daemon-smoke.nix b/tests/host-integration/daemon-smoke.nix index 6742e0bf06..15f97dd238 100644 --- a/tests/host-integration/daemon-smoke.nix +++ b/tests/host-integration/daemon-smoke.nix @@ -36,7 +36,7 @@ pkgs.testers.runNixOSTest { # 1. Broker socket is created + listening before its service (socket # activation): systemd binds/ACLs the AF_UNIX socket up front. - machine.wait_for_unit("d2b-priv-broker.socket") + machine.wait_for_unit("d2b-broker.socket") # 2. The unprivileged public daemon comes up. It Wants= (not Requires=) the # broker socket, so it serves while the broker stays idle. @@ -99,8 +99,8 @@ pkgs.testers.runNixOSTest { ) required = { "d2bd.service", - "d2b-priv-broker.socket", - "d2b-priv-broker.service", + "d2b-broker.socket", + "d2b-broker.service", } assert declared == required, ( f"unexpected framework acceptance census: {declared}" @@ -116,6 +116,6 @@ pkgs.testers.runNixOSTest { # 5. The broker service is socket-activated (not running until a request), # while the socket is listening. A clean idle posture. - machine.succeed("systemctl is-active d2b-priv-broker.socket") + machine.succeed("systemctl is-active d2b-broker.socket") ''; } diff --git a/tests/host-integration/host-realm-isolation.nix b/tests/host-integration/host-realm-isolation.nix index 81e1e2efd7..aae22491cb 100644 --- a/tests/host-integration/host-realm-isolation.nix +++ b/tests/host-integration/host-realm-isolation.nix @@ -44,7 +44,7 @@ pkgs.testers.runNixOSTest { testScript = '' start_all() machine.wait_for_unit("d2bd.service") - machine.wait_for_unit("d2b-priv-broker.socket") + machine.wait_for_unit("d2b-broker.socket") policy = "/etc/d2b/host-realm-relay-egress-policy.json" machine.succeed(f"test -r {policy}") @@ -73,10 +73,10 @@ pkgs.testers.runNixOSTest { pids = machine.succeed("pgrep -x d2bd").strip().split() assert pids, "d2bd pid missing" - machine.succeed("systemctl start d2b-priv-broker.service") + machine.succeed("systemctl start d2b-broker.service") broker_pid = machine.succeed( "for i in $(seq 1 50); do " - "pid=$(systemctl show -p MainPID --value d2b-priv-broker.service); " + "pid=$(systemctl show -p MainPID --value d2b-broker.service); " "if [ -n \"$pid\" ] && [ \"$pid\" != 0 ]; then echo \"$pid\"; exit 0; fi; " "sleep 0.2; done; exit 1" ).strip() diff --git a/tests/host-integration/lib.nix b/tests/host-integration/lib.nix index db810ed7d1..86f703e024 100644 --- a/tests/host-integration/lib.nix +++ b/tests/host-integration/lib.nix @@ -21,8 +21,8 @@ let # might have from optional or managed infrastructure. daemonAcceptanceUnits = [ "d2bd.service" - "d2b-priv-broker.socket" - "d2b-priv-broker.service" + "d2b-broker.socket" + "d2b-broker.service" ]; baseD2bConfig = { diff --git a/tests/host-integration/privilege-oracle.nix b/tests/host-integration/privilege-oracle.nix index e761eac3cb..43e74b269d 100644 --- a/tests/host-integration/privilege-oracle.nix +++ b/tests/host-integration/privilege-oracle.nix @@ -22,26 +22,26 @@ pkgs.testers.runNixOSTest { start_all() - machine.wait_for_unit("d2b-priv-broker.socket") + machine.wait_for_unit("d2b-broker.socket") machine.wait_for_unit("d2bd.service") # The broker is socket-activated, but starting the service directly keeps a # live Type=notify process long enough to read its /proc posture. - machine.succeed("systemctl start d2b-priv-broker.service") + machine.succeed("systemctl start d2b-broker.service") broker_pid = machine.succeed( "for i in $(seq 1 100); do " - "pid=$(systemctl show -p MainPID --value d2b-priv-broker.service); " + "pid=$(systemctl show -p MainPID --value d2b-broker.service); " "if [ -n \"$pid\" ] && [ \"$pid\" != 0 ] && [ -r \"/proc/$pid/status\" ]; then " "echo \"$pid\"; exit 0; fi; " "sleep 0.2; " "done; " - "systemctl status --no-pager d2b-priv-broker.service >&2; " + "systemctl status --no-pager d2b-broker.service >&2; " "exit 1" ).strip() - print(f"live d2b-priv-broker PID: {broker_pid}") + print(f"live d2b-broker PID: {broker_pid}") unit_raw = machine.succeed( - "systemctl show d2b-priv-broker.service " + "systemctl show d2b-broker.service " "-p CapabilityBoundingSet " "-p AmbientCapabilities " "-p NoNewPrivileges " @@ -50,7 +50,7 @@ pkgs.testers.runNixOSTest { "-p Slice " "-p SystemCallFilter" ) - print("rendered d2b-priv-broker.service posture:\n" + unit_raw) + print("rendered d2b-broker.service posture:\n" + unit_raw) unit = dict(line.split("=", 1) for line in unit_raw.strip().splitlines() if "=" in line) status_raw = machine.succeed(f"cat /proc/{broker_pid}/status") diff --git a/tests/host-integration/resource-operator-activation.nix b/tests/host-integration/resource-operator-activation.nix index 697c14d2e7..ee58c399d0 100644 --- a/tests/host-integration/resource-operator-activation.nix +++ b/tests/host-integration/resource-operator-activation.nix @@ -487,7 +487,7 @@ pkgs.testers.runNixOSTest { start_all() machine.wait_for_unit("nftables.service") machine.succeed("nft list table inet d2b") - machine.wait_for_unit("d2b-priv-broker.socket") + machine.wait_for_unit("d2b-broker.socket") machine.wait_for_unit("d2bd.service") machine.wait_for_file("/run/d2b/public.sock") machine.succeed("runuser -u alice -- d2b auth status --json >/run/d2b-auth-before.json") @@ -752,8 +752,8 @@ pkgs.testers.runNixOSTest { ) required = { "d2bd.service", - "d2b-priv-broker.socket", - "d2b-priv-broker.service", + "d2b-broker.socket", + "d2b-broker.service", } assert declared == required, ( f"unexpected framework acceptance census: {declared}" diff --git a/tests/host-integration/unsafe-local-helper.nix b/tests/host-integration/unsafe-local-helper.nix index 41bf215ca3..3d9fdf8ab3 100644 --- a/tests/host-integration/unsafe-local-helper.nix +++ b/tests/host-integration/unsafe-local-helper.nix @@ -836,8 +836,8 @@ PY machine.succeed(f"kill {unrelated_pid}") machine.succeed("systemctl show d2bd.service >/dev/null") - machine.succeed("systemctl show d2b-priv-broker.service >/dev/null") - machine.succeed("systemctl show d2b-priv-broker.socket >/dev/null") + machine.succeed("systemctl show d2b-broker.service >/dev/null") + machine.succeed("systemctl show d2b-broker.socket >/dev/null") machine.succeed( "! systemctl list-units --all --no-pager --no-legend " "| grep -E 'd2b-unsafe-local-(helper|shell)'" diff --git a/tests/integration/containers/README.md b/tests/integration/containers/README.md index 537172fd7b..d8b6f1c2b4 100644 --- a/tests/integration/containers/README.md +++ b/tests/integration/containers/README.md @@ -27,7 +27,7 @@ already exists, far more cheaply and without containers: | What | Where | Cost | | --- | --- | --- | -| Broker adopts the socket-activated fd (`LISTEN_FDS` fd-3 handoff) + serves a Hello round-trip | `packages/d2b-priv-broker/tests/socket_activation.rs` | ~0.4 s, unprivileged | +| Broker adopts the socket-activated fd (`LISTEN_FDS` fd-3 handoff) + serves a Hello round-trip | `packages/d2b-broker/tests/socket_activation.rs` | ~0.4 s, unprivileged | | Daemon binds `public.sock`, serves Hello/vmStart, `SO_PEERCRED` authz, writes the version file | `packages/d2bd/tests/daemon_*.rs` | native, hermetic | | Unit shape + `Wants=`/ordering, broker capability set, tmpfiles, evidence-record shape | `tests/unit/nix/cases/{broker-socket-activation,d2bd-startup-smoke}.nix` | nix eval, fail-closed | diff --git a/tests/integration/containers/lib.sh b/tests/integration/containers/lib.sh index 2c5ad37acf..399b4c0077 100644 --- a/tests/integration/containers/lib.sh +++ b/tests/integration/containers/lib.sh @@ -11,7 +11,7 @@ # - e.g. proving a static d2b binary runs on stock Ubuntu, matching CI. # It is deliberately NOT used to boot systemd and exercise daemon/socket # activation: that is covered far more cheaply by the native -# `packages/d2b-priv-broker/tests/socket_activation.rs` test (real +# `packages/d2b-broker/tests/socket_activation.rs` test (real # LISTEN_FDS fd-3 handoff + Hello round-trip, ~0.4 s, unprivileged) plus the # nix-unit unit-shape cases. A faithful systemd-boot container was measured at # ~1.4 G to ship (the d2b bundle drags the full per-VM runtime substrate, diff --git a/tests/integration/live/live-vm-smoke.sh b/tests/integration/live/live-vm-smoke.sh index e1951973ca..f9acc3de4a 100755 --- a/tests/integration/live/live-vm-smoke.sh +++ b/tests/integration/live/live-vm-smoke.sh @@ -88,8 +88,8 @@ if [ ! -e /dev/kvm ]; then exit 77 fi -if ! systemctl is-active --quiet d2b-priv-broker 2>/dev/null; then - log "==> SKIP: d2b-priv-broker is not active (systemctl is-active returned non-zero)" +if ! systemctl is-active --quiet d2b-broker 2>/dev/null; then + log "==> SKIP: d2b-broker is not active (systemctl is-active returned non-zero)" exit 77 fi diff --git a/tests/migration-ledger.toml b/tests/migration-ledger.toml index 60737514a6..c849460e8d 100644 --- a/tests/migration-ledger.toml +++ b/tests/migration-ledger.toml @@ -238,7 +238,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["vmChecks..privilege-oracle (tests/host-integration/privilege-oracle.nix - hermetic runNixOSTest asserting the live d2b-priv-broker /proc posture: bounded CapBnd, empty CapAmb, seccomp filter active, d2b.slice cgroup placement, derived from the rendered systemd unit)"] +successor_ids = ["vmChecks..privilege-oracle (tests/host-integration/privilege-oracle.nix - hermetic runNixOSTest asserting the live d2b-broker /proc posture: bounded CapBnd, empty CapAmb, seccomp filter active, d2b.slice cgroup placement, derived from the rendered systemd unit)"] exercised_today = "skip" [[script]] @@ -274,7 +274,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["flake.checks..nix-unit (tests/unit/nix/cases/d2bd-startup-smoke.nix - 17 cases: daemon/broker tmpfiles + systemd socket/service surface + evidence-record shape)", "packages/d2bd/tests/daemon_socket_acl.rs::daemon_socket_acl::real_current_peer_in_configured_classifier_is_accepted (spawns CARGO_BIN_EXE_d2bd over AF_UNIX, waits for public.sock, and asserts Hello -> helloOk)", "packages/d2bd/tests/daemon_version_file.rs::daemon_version_file::startup_writes_version_file_next_to_public_socket (spawns CARGO_BIN_EXE_d2bd over AF_UNIX and asserts the startup version file fields)", "packages/d2bd/tests/bundle_tampered_envelope.rs::daemon_vm_start_returns_bundle_tampered_envelope (spawns CARGO_BIN_EXE_d2bd over AF_UNIX and asserts vmStart returns kind=bundle-tampered for a tampered bundle.json)", "tests/golden/pinned/d2bd-startup-smoke.txt + tests/golden/pinned/daemon-socket-acl.txt (fail-closed native-test presence pins)", "packages/d2b-priv-broker/tests/socket_activation.rs::broker_adopts_socket_activated_fd_and_serves_hello (real LISTEN_FDS fd-3 socket-activation handoff + Hello round-trip; covers the live .socket -> .service activation path natively, replacing the dropped systemd-boot container - see tests/integration/containers/README.md)"] +successor_ids = ["flake.checks..nix-unit (tests/unit/nix/cases/d2bd-startup-smoke.nix - 17 cases: daemon/broker tmpfiles + systemd socket/service surface + evidence-record shape)", "packages/d2bd/tests/daemon_socket_acl.rs::daemon_socket_acl::real_current_peer_in_configured_classifier_is_accepted (spawns CARGO_BIN_EXE_d2bd over AF_UNIX, waits for public.sock, and asserts Hello -> helloOk)", "packages/d2bd/tests/daemon_version_file.rs::daemon_version_file::startup_writes_version_file_next_to_public_socket (spawns CARGO_BIN_EXE_d2bd over AF_UNIX and asserts the startup version file fields)", "packages/d2bd/tests/bundle_tampered_envelope.rs::daemon_vm_start_returns_bundle_tampered_envelope (spawns CARGO_BIN_EXE_d2bd over AF_UNIX and asserts vmStart returns kind=bundle-tampered for a tampered bundle.json)", "tests/golden/pinned/d2bd-startup-smoke.txt + tests/golden/pinned/daemon-socket-acl.txt (fail-closed native-test presence pins)", "packages/d2b-broker/tests/socket_activation.rs::broker_adopts_socket_activated_fd_and_serves_hello (real LISTEN_FDS fd-3 socket-activation handoff + Hello round-trip; covers the live .socket -> .service activation path natively, replacing the dropped systemd-boot container - see tests/integration/containers/README.md)"] exercised_today = "manual" [[script]] @@ -472,7 +472,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["broker-workspace cargo test --workspace --features layer1-bootstrap (d2b-priv-broker broker_export_audit::export_audit_requires_admin_and_exports_op_audit_records) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/broker-export-audit.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across the broker workspace layer1-bootstrap list)"] +successor_ids = ["broker-workspace cargo test --workspace --features layer1-bootstrap (d2b-broker broker_export_audit::export_audit_requires_admin_and_exports_op_audit_records) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/broker-export-audit.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across the broker workspace layer1-bootstrap list)"] exercised_today = "yes" [[script]] @@ -481,7 +481,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["broker-workspace cargo test --workspace --features layer1-bootstrap (d2b-priv-broker broker_socket_acl::socket_acl_rejects_non_d2bd_peers_and_accepts_daemon) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/broker-socket-acl.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across the broker workspace layer1-bootstrap list)"] +successor_ids = ["broker-workspace cargo test --workspace --features layer1-bootstrap (d2b-broker broker_socket_acl::socket_acl_rejects_non_d2bd_peers_and_accepts_daemon) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/broker-socket-acl.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across the broker workspace layer1-bootstrap list)"] exercised_today = "yes" [[script]] @@ -544,7 +544,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["cargo test --workspace (d2b-host nftables::tests::{usbip_carveout_inserted_before_generic,comment_marker_prefix_on_every_managed_rule,fake_backend_refuses_carveout_after_generic}) - already gated in tests/tools/rust-workspace-checks.sh", "broker-workspace cargo test --workspace [+ --features layer1-bootstrap] (d2b-priv-broker ops::usbip_firewall::tests::{bind_firewall_rule_produces_audit_with_busid_and_hash,carveout_ordering_invariant_via_op,w6_ops_refused_with_unknown_operation_audit}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/usbip-firewall-skeleton.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across both workspaces)", "retired script's UsbipBind/UsbipUnbind/UsbipProxyReconcile static grep is covered by ops::usbip_firewall::tests::w6_ops_refused_with_unknown_operation_audit, which enumerates those variants and asserts unknown-operation refusal audit payloads"] +successor_ids = ["cargo test --workspace (d2b-host nftables::tests::{usbip_carveout_inserted_before_generic,comment_marker_prefix_on_every_managed_rule,fake_backend_refuses_carveout_after_generic}) - already gated in tests/tools/rust-workspace-checks.sh", "broker-workspace cargo test --workspace [+ --features layer1-bootstrap] (d2b-broker ops::usbip_firewall::tests::{bind_firewall_rule_produces_audit_with_busid_and_hash,carveout_ordering_invariant_via_op,w6_ops_refused_with_unknown_operation_audit}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/usbip-firewall-skeleton.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across both workspaces)", "retired script's UsbipBind/UsbipUnbind/UsbipProxyReconcile static grep is covered by ops::usbip_firewall::tests::w6_ops_refused_with_unknown_operation_audit, which enumerates those variants and asserts unknown-operation refusal audit payloads"] exercised_today = "yes" [[script]] @@ -562,7 +562,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["packages/d2b-contract-tests/tests/policy_broker_schema.rs::broker_delegates_bundle_validation_to_core (asserts the broker delegates bundle validation to d2b-core only: packages/d2b-priv-broker/src exists, some source under it imports `d2b_core::manifest` (delegation target), and some source calls `validate_bundle` (the single validation entry point) - the gate's two positive `grep -R -q` delegation checks ported as substring scans over the recursively-collected broker src tree. Spec correction (existing code is canon): the bash gate's 4th assertion forbidding ALL `serde_json::from_str`/`from_value` under the broker src is red against current code - the broker legitimately parses subprocess JSON (nft / `ip route` / store-view runner output / audit lines) in ops/{store_view_farm,route,tap,store_sync_*}.rs (pre-existing breakage per tests/README.md), so the over-broad blanket negative is dropped while the gate's stated delegation purpose is preserved); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_broker_schema.rs::broker_delegates_bundle_validation_to_core (asserts the broker delegates bundle validation to d2b-core only: packages/d2b-broker/src exists, some source under it imports `d2b_core::manifest` (delegation target), and some source calls `validate_bundle` (the single validation entry point) - the gate's two positive `grep -R -q` delegation checks ported as substring scans over the recursively-collected broker src tree. Spec correction (existing code is canon): the bash gate's 4th assertion forbidding ALL `serde_json::from_str`/`from_value` under the broker src is red against current code - the broker legitimately parses subprocess JSON (nft / `ip route` / store-view runner output / audit lines) in ops/{store_view_farm,route,tap,store_sync_*}.rs (pre-existing breakage per tests/README.md), so the over-broad blanket negative is dropped while the gate's stated delegation purpose is preserved); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] exercised_today = "yes" [[script]] @@ -571,7 +571,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["cargo test --workspace (d2b-host cgroup::tests::* delegation canaries) + broker test pass (d2b-priv-broker ops::cgroup::tests::* delegation/audit canaries) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/cgroup-delegation-oracle.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] +successor_ids = ["cargo test --workspace (d2b-host cgroup::tests::* delegation canaries) + broker test pass (d2b-broker ops::cgroup::tests::* delegation/audit canaries) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/cgroup-delegation-oracle.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] exercised_today = "yes" [[script]] @@ -589,7 +589,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["cargo test --workspace (d2b-host netlink::tests::{ipv6_off_sequence_runs_in_order,bridge_port_readback_matches_defaults}, bridge_port::tests::readback_matches_defaults, routes::tests::{no_default_route_fails_closed,foreign_default_route_fails_closed,dnsmasq_not_bound_fails_closed,host_lan_cidr_ambiguous_for_vpn}) - already gated in tests/tools/rust-workspace-checks.sh", "broker-workspace cargo test --workspace [+ --features layer1-bootstrap] (d2b-priv-broker ops::nm::tests::{managed_foreign_conflict_detected,reload_command_select_by_version}, ops::tap::tests::set_bridge_port_flags_readback_drift_fails_closed) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/host-prepare-network.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across both workspaces)", "ch-net-handoff-not-supported docs-anchor (retired gate's trailing grep) independently guarded by packages/d2b/tests/cli_contract_coverage.rs::host_cli_error_golden_table_is_closed_and_complete + committed host-check goldens (ch-net-handoff-canary.sh retired W3fu10)"] +successor_ids = ["cargo test --workspace (d2b-host netlink::tests::{ipv6_off_sequence_runs_in_order,bridge_port_readback_matches_defaults}, bridge_port::tests::readback_matches_defaults, routes::tests::{no_default_route_fails_closed,foreign_default_route_fails_closed,dnsmasq_not_bound_fails_closed,host_lan_cidr_ambiguous_for_vpn}) - already gated in tests/tools/rust-workspace-checks.sh", "broker-workspace cargo test --workspace [+ --features layer1-bootstrap] (d2b-broker ops::nm::tests::{managed_foreign_conflict_detected,reload_command_select_by_version}, ops::tap::tests::set_bridge_port_flags_readback_drift_fails_closed) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/host-prepare-network.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across both workspaces)", "ch-net-handoff-not-supported docs-anchor (retired gate's trailing grep) independently guarded by packages/d2b/tests/cli_contract_coverage.rs::host_cli_error_golden_table_is_closed_and_complete + committed host-check goldens (ch-net-handoff-canary.sh retired W3fu10)"] exercised_today = "yes" [[script]] @@ -607,7 +607,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["cargo test --workspace (d2b-host nftables::tests::{coexistence_matrix_all_7_rows,detector_clean_host,detector_single_manager_unambiguous,detector_multiple_managers_unknown}) + broker test pass (d2b-priv-broker ops::nft::tests::{refuses_when_firewalld_declared_coexist,applies_on_clean_host_with_coexist,nft_script_includes_all_four_chains}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/nft-coexistence.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] +successor_ids = ["cargo test --workspace (d2b-host nftables::tests::{coexistence_matrix_all_7_rows,detector_clean_host,detector_single_manager_unambiguous,detector_multiple_managers_unknown}) + broker test pass (d2b-broker ops::nft::tests::{refuses_when_firewalld_declared_coexist,applies_on_clean_host_with_coexist,nft_script_includes_all_four_chains}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/nft-coexistence.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] exercised_today = "yes" [[script]] @@ -625,7 +625,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["broker test pass (d2b-priv-broker ops::hosts::tests::{refuses_symlink_target,writes_managed_block_into_fresh_file,idempotent_when_block_matches}, ops::nm::tests::reload_failure_rolls_back, ops::state_dir::tests::{refuses_absolute_relative_path,refuses_parent_dir_escape,idempotent_reuses_existing_dirs,creates_base_and_relative_paths}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/path-safety-violation-fs.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] +successor_ids = ["broker test pass (d2b-broker ops::hosts::tests::{refuses_symlink_target,writes_managed_block_into_fresh_file,idempotent_when_block_matches}, ops::nm::tests::reload_failure_rolls_back, ops::state_dir::tests::{refuses_absolute_relative_path,refuses_parent_dir_escape,idempotent_reuses_existing_dirs,creates_base_and_relative_paths}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/path-safety-violation-fs.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] exercised_today = "skip" [[script]] @@ -733,7 +733,7 @@ group = "C" make_target = "test-contract" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["packages/d2b-contract-tests/tests/minijail_gpu.rs (gated via the D2B_FIXTURES / D2B_FIXTURES_FULL contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh -> static.sh) - wayland_proxy_profile_source_shape (Layer-1 assert_profile_source over nixos-modules/minijail-profiles.nix: role = \"wayland-proxy\" declaration, mandatory seccompPolicyRef = \"w1-wayland-proxy\", empty capabilities/no CAP_ in the profile block, requiresStartRoot never true, no userNamespace, writable path /run/d2b-wlproxy present, no PipeWire/Pulse bind in the block, umask = 7, deviceBinds = [ ] empty), wayland_proxy_policy_ref_device_classes_source (Layer-1 assert_policy_ref_entry: \"w1-wayland-proxy\" in packages/d2b-priv-broker/src/live_handlers.rs policy_ref_device_classes), wayland_proxy_rust_variants_declared_source (Layer-1 assert_rust_variant_exists: ProcessRole::WaylandProxy in packages/d2b-core/src/processes.rs and RunnerRole::WaylandProxy in packages/d2b-contracts/src/broker_wire.rs), wayland_proxy_rendered_profile_shape (Layer-1 assert_installed_profiles_consistent applied to the REAL rendered RoleProfile - strictly stronger than the bash gate which scanned /etc/d2b/minijail-profiles/*wayland-proxy*.json and skipped when absent: empty caps, w1-wayland-proxy seccomp, empty deviceBinds + bindMounts, dedicated runtime dir /run/d2b-wlproxy/ as the sole writable path, no PipeWire/Pulse path access, umask 0o007, no broker-pre-NS userNamespace). The bash gate's D2B_LIVE=1 Layer-2 (minijail0 /bin/true positive probe; /var/lib/d2b/validated/p1-wayland-proxy.json evidence write) is a live host-execution layer and does not port to the contract layer."] +successor_ids = ["packages/d2b-contract-tests/tests/minijail_gpu.rs (gated via the D2B_FIXTURES / D2B_FIXTURES_FULL contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh -> static.sh) - wayland_proxy_profile_source_shape (Layer-1 assert_profile_source over nixos-modules/minijail-profiles.nix: role = \"wayland-proxy\" declaration, mandatory seccompPolicyRef = \"w1-wayland-proxy\", empty capabilities/no CAP_ in the profile block, requiresStartRoot never true, no userNamespace, writable path /run/d2b-wlproxy present, no PipeWire/Pulse bind in the block, umask = 7, deviceBinds = [ ] empty), wayland_proxy_policy_ref_device_classes_source (Layer-1 assert_policy_ref_entry: \"w1-wayland-proxy\" in packages/d2b-broker/src/live_handlers.rs policy_ref_device_classes), wayland_proxy_rust_variants_declared_source (Layer-1 assert_rust_variant_exists: ProcessRole::WaylandProxy in packages/d2b-core/src/processes.rs and RunnerRole::WaylandProxy in packages/d2b-contracts/src/broker_wire.rs), wayland_proxy_rendered_profile_shape (Layer-1 assert_installed_profiles_consistent applied to the REAL rendered RoleProfile - strictly stronger than the bash gate which scanned /etc/d2b/minijail-profiles/*wayland-proxy*.json and skipped when absent: empty caps, w1-wayland-proxy seccomp, empty deviceBinds + bindMounts, dedicated runtime dir /run/d2b-wlproxy/ as the sole writable path, no PipeWire/Pulse path access, umask 0o007, no broker-pre-NS userNamespace). The bash gate's D2B_LIVE=1 Layer-2 (minijail0 /bin/true positive probe; /var/lib/d2b/validated/p1-wayland-proxy.json evidence write) is a live host-execution layer and does not port to the contract layer."] exercised_today = "manual" [[script]] @@ -805,7 +805,7 @@ group = "D" make_target = "test-nix-unit" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["packages/d2b-contract-tests/tests/policy_misc.rs::polkit_allowlist_daemon_only_singletons (nixos-modules/host-polkit.nix: launcher-group allowlist names ONLY the daemon-only singletons d2bd.service/d2b-priv-broker.service/d2b-priv-broker.socket, the executable region (awk '/^in$/,0' scope) carries no retired per-VM/per-env unit shapes, and the surviving grant keeps the manage-units action-id guard, the `d2b` group guard, the start/stop/restart verb allowlist, and exactly one polkit.addRule callback); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_misc.rs::polkit_allowlist_daemon_only_singletons (nixos-modules/host-polkit.nix: launcher-group allowlist names ONLY the daemon-only singletons d2bd.service/d2b-broker.service/d2b-broker.socket, the executable region (awk '/^in$/,0' scope) carries no retired per-VM/per-env unit shapes, and the surviving grant keeps the manage-units action-id guard, the `d2b` group guard, the start/stop/restart verb allowlist, and exactly one polkit.addRule callback); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] exercised_today = "yes" [[script]] @@ -868,7 +868,7 @@ group = "H" make_target = "test-policy" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["packages/d2b-contract-tests/tests/policy_docs.rs::agents_md_reflects_daemon_only_end_state (AGENTS.md daemon-only end-state invariant: positive - '## Daemon-only end-state (P6 onward)' section + ADR 0015 cross-ref + d2bd / d2b-priv-broker.socket / SpawnRunner mentions; negative - per-line forbidden legacy-as-live scan exempting lines that carry an explicit historical/retired marker, forbidden_re + allowed_marker_re ported verbatim); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_docs.rs::agents_md_reflects_daemon_only_end_state (AGENTS.md daemon-only end-state invariant: positive - '## Daemon-only end-state (P6 onward)' section + ADR 0015 cross-ref + d2bd / d2b-broker.socket / SpawnRunner mentions; negative - per-line forbidden legacy-as-live scan exempting lines that carry an explicit historical/retired marker, forbidden_re + allowed_marker_re ported verbatim); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] exercised_today = "yes" [[script]] @@ -922,7 +922,7 @@ group = "D" make_target = "test-nix-unit" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["packages/d2b-contract-tests/tests/policy_daemon.rs::broker_systemd_unit_declarations (nixos-modules/host-broker.nix: d2b-priv-broker socket/service shape - config NOT gated behind cfg.daemonExperimental.enable, ListenSequentialPacket=/run/d2b/priv.sock, SocketGroup=d2bd, SocketMode=0660, serviceConfig.Type=notify, socket unit wantedBy sockets.target); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_daemon.rs::broker_systemd_unit_declarations (nixos-modules/host-broker.nix: d2b-broker socket/service shape - config NOT gated behind cfg.daemonExperimental.enable, ListenSequentialPacket=/run/d2b/priv.sock, SocketGroup=d2bd, SocketMode=0660, serviceConfig.Type=notify, socket unit wantedBy sockets.target); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] exercised_today = "yes" [[script]] @@ -1048,7 +1048,7 @@ group = "H" make_target = "test-policy" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["packages/d2b-contract-tests/tests/policy_guest.rs::otel_relay_acl_module_not_publicly_imported_and_broker_handler_present (asserts nixos-modules/default.nix carries no uncommented ./host-otel-relay-acl.nix import and that packages/d2b-priv-broker/src/runtime.rs still carries the RunnerRole::OtelHostBridge spawn handler); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_guest.rs::otel_relay_acl_module_not_publicly_imported_and_broker_handler_present (asserts nixos-modules/default.nix carries no uncommented ./host-otel-relay-acl.nix import and that packages/d2b-broker/src/runtime.rs still carries the RunnerRole::OtelHostBridge spawn handler); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] exercised_today = "yes" [[script]] @@ -1237,7 +1237,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["tests/tools/rust-workspace-checks.sh (gated `cargo check --workspace --manifest-path d2b-priv-broker default features = real wire dispatch` - the broker default-features build is already a gate step)"] +successor_ids = ["tests/tools/rust-workspace-checks.sh (gated `cargo check --workspace --manifest-path d2b-broker default features = real wire dispatch` - the broker default-features build is already a gate step)"] exercised_today = "skip" [[script]] @@ -1246,7 +1246,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["cargo test --workspace --features layer1-bootstrap (d2b-priv-broker fd_passing::tests::scm_rights_fd_lifecycle_* - runs in tests/tools/rust-workspace-checks.sh broker layer1-bootstrap test pass)", "tests/tools/assert-pinned-tests.sh (fail-closed pin via the broker --features layer1-bootstrap nextest list)"] +successor_ids = ["cargo test --workspace --features layer1-bootstrap (d2b-broker fd_passing::tests::scm_rights_fd_lifecycle_* - runs in tests/tools/rust-workspace-checks.sh broker layer1-bootstrap test pass)", "tests/tools/assert-pinned-tests.sh (fail-closed pin via the broker --features layer1-bootstrap nextest list)"] exercised_today = "yes" [[script]] @@ -1489,7 +1489,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["cargo nextest run --workspace (d2b-host devices #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-priv-broker ops::device #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] +successor_ids = ["cargo nextest run --workspace (d2b-host devices #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-broker ops::device #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] exercised_today = "yes" [[script]] @@ -1507,7 +1507,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["cargo nextest run --workspace (d2b-host ioctl_policy #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-priv-broker ops::device #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] +successor_ids = ["cargo nextest run --workspace (d2b-host ioctl_policy #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-broker ops::device #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] exercised_today = "manual" [[script]] @@ -1516,7 +1516,7 @@ group = "A" make_target = "test-rust" tier = "ci-l1" status = "retired" # legacy | porting | ported | retired -successor_ids = ["cargo nextest run --workspace (d2b-host modules #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-priv-broker ops::modprobe #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] +successor_ids = ["cargo nextest run --workspace (d2b-host modules #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-broker ops::modprobe #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] exercised_today = "skip" [[script]] diff --git a/tests/migration-state.d/agents-md-rewrite-eval.toml b/tests/migration-state.d/agents-md-rewrite-eval.toml index 218353154f..0075978855 100644 --- a/tests/migration-state.d/agents-md-rewrite-eval.toml +++ b/tests/migration-state.d/agents-md-rewrite-eval.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["packages/d2b-contract-tests/tests/policy_docs.rs::agents_md_reflects_daemon_only_end_state (AGENTS.md daemon-only end-state invariant: positive - '## Daemon-only end-state (P6 onward)' section + ADR 0015 cross-ref + d2bd / d2b-priv-broker.socket / SpawnRunner mentions; negative - per-line forbidden legacy-as-live scan exempting lines that carry an explicit historical/retired marker, forbidden_re + allowed_marker_re ported verbatim); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_docs.rs::agents_md_reflects_daemon_only_end_state (AGENTS.md daemon-only end-state invariant: positive - '## Daemon-only end-state (P6 onward)' section + ADR 0015 cross-ref + d2bd / d2b-broker.socket / SpawnRunner mentions; negative - per-line forbidden legacy-as-live scan exempting lines that carry an explicit historical/retired marker, forbidden_re + allowed_marker_re ported verbatim); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] diff --git a/tests/migration-state.d/broker-default-features-build.toml b/tests/migration-state.d/broker-default-features-build.toml index 35f6bcf1ff..c648ccfac0 100644 --- a/tests/migration-state.d/broker-default-features-build.toml +++ b/tests/migration-state.d/broker-default-features-build.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["tests/tools/rust-workspace-checks.sh (gated `cargo check --workspace --manifest-path d2b-priv-broker default features = real wire dispatch` - the broker default-features build is already a gate step)"] +successor_ids = ["tests/tools/rust-workspace-checks.sh (gated `cargo check --workspace --manifest-path d2b-broker default features = real wire dispatch` - the broker default-features build is already a gate step)"] diff --git a/tests/migration-state.d/broker-export-audit.toml b/tests/migration-state.d/broker-export-audit.toml index d7cb123274..1bf78e8f5f 100644 --- a/tests/migration-state.d/broker-export-audit.toml +++ b/tests/migration-state.d/broker-export-audit.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["broker-workspace cargo test --workspace --features layer1-bootstrap (d2b-priv-broker broker_export_audit::export_audit_requires_admin_and_exports_op_audit_records) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/broker-export-audit.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across the broker workspace layer1-bootstrap list)"] +successor_ids = ["broker-workspace cargo test --workspace --features layer1-bootstrap (d2b-broker broker_export_audit::export_audit_requires_admin_and_exports_op_audit_records) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/broker-export-audit.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across the broker workspace layer1-bootstrap list)"] diff --git a/tests/migration-state.d/broker-scm-rights-fd-lifecycle.toml b/tests/migration-state.d/broker-scm-rights-fd-lifecycle.toml index e6ae850afd..b7575a0294 100644 --- a/tests/migration-state.d/broker-scm-rights-fd-lifecycle.toml +++ b/tests/migration-state.d/broker-scm-rights-fd-lifecycle.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["cargo test --workspace --features layer1-bootstrap (d2b-priv-broker fd_passing::tests::scm_rights_fd_lifecycle_* - runs in tests/tools/rust-workspace-checks.sh broker layer1-bootstrap test pass)", "tests/tools/assert-pinned-tests.sh (fail-closed pin via the broker --features layer1-bootstrap nextest list)"] +successor_ids = ["cargo test --workspace --features layer1-bootstrap (d2b-broker fd_passing::tests::scm_rights_fd_lifecycle_* - runs in tests/tools/rust-workspace-checks.sh broker layer1-bootstrap test pass)", "tests/tools/assert-pinned-tests.sh (fail-closed pin via the broker --features layer1-bootstrap nextest list)"] diff --git a/tests/migration-state.d/broker-socket-acl.toml b/tests/migration-state.d/broker-socket-acl.toml index e94b923c6f..3ed71a0e79 100644 --- a/tests/migration-state.d/broker-socket-acl.toml +++ b/tests/migration-state.d/broker-socket-acl.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["broker-workspace cargo test --workspace --features layer1-bootstrap (d2b-priv-broker broker_socket_acl::socket_acl_rejects_non_d2bd_peers_and_accepts_daemon) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/broker-socket-acl.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across the broker workspace layer1-bootstrap list)"] +successor_ids = ["broker-workspace cargo test --workspace --features layer1-bootstrap (d2b-broker broker_socket_acl::socket_acl_rejects_non_d2bd_peers_and_accepts_daemon) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/broker-socket-acl.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across the broker workspace layer1-bootstrap list)"] diff --git a/tests/migration-state.d/broker-systemd-unit-eval.toml b/tests/migration-state.d/broker-systemd-unit-eval.toml index 11505bd3c4..13c652634f 100644 --- a/tests/migration-state.d/broker-systemd-unit-eval.toml +++ b/tests/migration-state.d/broker-systemd-unit-eval.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["packages/d2b-contract-tests/tests/policy_daemon.rs::broker_systemd_unit_declarations (nixos-modules/host-broker.nix: d2b-priv-broker socket/service shape - config NOT gated behind cfg.daemonExperimental.enable, ListenSequentialPacket=/run/d2b/priv.sock, SocketGroup=d2bd, SocketMode=0660, serviceConfig.Type=notify, socket unit wantedBy sockets.target); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_daemon.rs::broker_systemd_unit_declarations (nixos-modules/host-broker.nix: d2b-broker socket/service shape - config NOT gated behind cfg.daemonExperimental.enable, ListenSequentialPacket=/run/d2b/priv.sock, SocketGroup=d2bd, SocketMode=0660, serviceConfig.Type=notify, socket unit wantedBy sockets.target); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] diff --git a/tests/migration-state.d/broker-validate-bundle.toml b/tests/migration-state.d/broker-validate-bundle.toml index f5401e5092..5b0fece16b 100644 --- a/tests/migration-state.d/broker-validate-bundle.toml +++ b/tests/migration-state.d/broker-validate-bundle.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["packages/d2b-contract-tests/tests/policy_broker_schema.rs::broker_delegates_bundle_validation_to_core (asserts the broker delegates bundle validation to d2b-core only: packages/d2b-priv-broker/src exists, some source under it imports `d2b_core::manifest` (delegation target), and some source calls `validate_bundle` (the single validation entry point) - the gate's two positive `grep -R -q` delegation checks ported as substring scans over the recursively-collected broker src tree. Spec correction (existing code is canon): the bash gate's 4th assertion forbidding ALL `serde_json::from_str`/`from_value` under the broker src is red against current code - the broker legitimately parses subprocess JSON (nft / `ip route` / store-view runner output / audit lines) in ops/{store_view_farm,route,tap,store_sync_*}.rs (pre-existing breakage per tests/README.md), so the over-broad blanket negative is dropped while the gate's stated delegation purpose is preserved); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_broker_schema.rs::broker_delegates_bundle_validation_to_core (asserts the broker delegates bundle validation to d2b-core only: packages/d2b-broker/src exists, some source under it imports `d2b_core::manifest` (delegation target), and some source calls `validate_bundle` (the single validation entry point) - the gate's two positive `grep -R -q` delegation checks ported as substring scans over the recursively-collected broker src tree. Spec correction (existing code is canon): the bash gate's 4th assertion forbidding ALL `serde_json::from_str`/`from_value` under the broker src is red against current code - the broker legitimately parses subprocess JSON (nft / `ip route` / store-view runner output / audit lines) in ops/{store_view_farm,route,tap,store_sync_*}.rs (pre-existing breakage per tests/README.md), so the over-broad blanket negative is dropped while the gate's stated delegation purpose is preserved); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] diff --git a/tests/migration-state.d/cgroup-delegation-oracle.toml b/tests/migration-state.d/cgroup-delegation-oracle.toml index dbdb31e368..d25e7c84db 100644 --- a/tests/migration-state.d/cgroup-delegation-oracle.toml +++ b/tests/migration-state.d/cgroup-delegation-oracle.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["cargo test --workspace (d2b-host cgroup::tests::* delegation canaries) + broker test pass (d2b-priv-broker ops::cgroup::tests::* delegation/audit canaries) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/cgroup-delegation-oracle.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] +successor_ids = ["cargo test --workspace (d2b-host cgroup::tests::* delegation canaries) + broker test pass (d2b-broker ops::cgroup::tests::* delegation/audit canaries) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/cgroup-delegation-oracle.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] diff --git a/tests/migration-state.d/d2bd-startup-smoke.toml b/tests/migration-state.d/d2bd-startup-smoke.toml index 4c98b8ecad..1b134aa5e2 100644 --- a/tests/migration-state.d/d2bd-startup-smoke.toml +++ b/tests/migration-state.d/d2bd-startup-smoke.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["flake.checks..nix-unit (tests/unit/nix/cases/d2bd-startup-smoke.nix - 17 cases: daemon/broker tmpfiles + systemd socket/service surface + evidence-record shape)", "packages/d2bd/tests/daemon_socket_acl.rs::daemon_socket_acl::real_current_peer_in_configured_classifier_is_accepted (spawns CARGO_BIN_EXE_d2bd over AF_UNIX, waits for public.sock, and asserts Hello -> helloOk)", "packages/d2bd/tests/daemon_version_file.rs::daemon_version_file::startup_writes_version_file_next_to_public_socket (spawns CARGO_BIN_EXE_d2bd over AF_UNIX and asserts the startup version file fields)", "packages/d2bd/tests/bundle_tampered_envelope.rs::daemon_vm_start_returns_bundle_tampered_envelope (spawns CARGO_BIN_EXE_d2bd over AF_UNIX and asserts vmStart returns kind=bundle-tampered for a tampered bundle.json)", "tests/golden/pinned/d2bd-startup-smoke.txt + tests/golden/pinned/daemon-socket-acl.txt (fail-closed native-test presence pins)", "packages/d2b-priv-broker/tests/socket_activation.rs::broker_adopts_socket_activated_fd_and_serves_hello (real LISTEN_FDS fd-3 socket-activation handoff + Hello round-trip; covers the live .socket -> .service activation path natively, replacing the dropped systemd-boot container - see tests/integration/containers/README.md)"] +successor_ids = ["flake.checks..nix-unit (tests/unit/nix/cases/d2bd-startup-smoke.nix - 17 cases: daemon/broker tmpfiles + systemd socket/service surface + evidence-record shape)", "packages/d2bd/tests/daemon_socket_acl.rs::daemon_socket_acl::real_current_peer_in_configured_classifier_is_accepted (spawns CARGO_BIN_EXE_d2bd over AF_UNIX, waits for public.sock, and asserts Hello -> helloOk)", "packages/d2bd/tests/daemon_version_file.rs::daemon_version_file::startup_writes_version_file_next_to_public_socket (spawns CARGO_BIN_EXE_d2bd over AF_UNIX and asserts the startup version file fields)", "packages/d2bd/tests/bundle_tampered_envelope.rs::daemon_vm_start_returns_bundle_tampered_envelope (spawns CARGO_BIN_EXE_d2bd over AF_UNIX and asserts vmStart returns kind=bundle-tampered for a tampered bundle.json)", "tests/golden/pinned/d2bd-startup-smoke.txt + tests/golden/pinned/daemon-socket-acl.txt (fail-closed native-test presence pins)", "packages/d2b-broker/tests/socket_activation.rs::broker_adopts_socket_activated_fd_and_serves_hello (real LISTEN_FDS fd-3 socket-activation handoff + Hello round-trip; covers the live .socket -> .service activation path natively, replacing the dropped systemd-boot container - see tests/integration/containers/README.md)"] diff --git a/tests/migration-state.d/device-node-matrix.toml b/tests/migration-state.d/device-node-matrix.toml index d7ff075768..c33763f129 100644 --- a/tests/migration-state.d/device-node-matrix.toml +++ b/tests/migration-state.d/device-node-matrix.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["cargo nextest run --workspace (d2b-host devices #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-priv-broker ops::device #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] +successor_ids = ["cargo nextest run --workspace (d2b-host devices #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-broker ops::device #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] diff --git a/tests/migration-state.d/host-prepare-network.toml b/tests/migration-state.d/host-prepare-network.toml index b0c35cae65..6b309e7506 100644 --- a/tests/migration-state.d/host-prepare-network.toml +++ b/tests/migration-state.d/host-prepare-network.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["cargo test --workspace (d2b-host netlink::tests::{ipv6_off_sequence_runs_in_order,bridge_port_readback_matches_defaults}, bridge_port::tests::readback_matches_defaults, routes::tests::{no_default_route_fails_closed,foreign_default_route_fails_closed,dnsmasq_not_bound_fails_closed,host_lan_cidr_ambiguous_for_vpn}) - already gated in tests/tools/rust-workspace-checks.sh", "broker-workspace cargo test --workspace [+ --features layer1-bootstrap] (d2b-priv-broker ops::nm::tests::{managed_foreign_conflict_detected,reload_command_select_by_version}, ops::tap::tests::set_bridge_port_flags_readback_drift_fails_closed) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/host-prepare-network.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across both workspaces)", "ch-net-handoff-not-supported docs-anchor (retired gate's trailing grep) independently guarded by packages/d2b/tests/cli_contract_coverage.rs::host_cli_error_golden_table_is_closed_and_complete + committed host-check goldens (ch-net-handoff-canary.sh retired W3fu10)"] +successor_ids = ["cargo test --workspace (d2b-host netlink::tests::{ipv6_off_sequence_runs_in_order,bridge_port_readback_matches_defaults}, bridge_port::tests::readback_matches_defaults, routes::tests::{no_default_route_fails_closed,foreign_default_route_fails_closed,dnsmasq_not_bound_fails_closed,host_lan_cidr_ambiguous_for_vpn}) - already gated in tests/tools/rust-workspace-checks.sh", "broker-workspace cargo test --workspace [+ --features layer1-bootstrap] (d2b-broker ops::nm::tests::{managed_foreign_conflict_detected,reload_command_select_by_version}, ops::tap::tests::set_bridge_port_flags_readback_drift_fails_closed) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/host-prepare-network.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across both workspaces)", "ch-net-handoff-not-supported docs-anchor (retired gate's trailing grep) independently guarded by packages/d2b/tests/cli_contract_coverage.rs::host_cli_error_golden_table_is_closed_and_complete + committed host-check goldens (ch-net-handoff-canary.sh retired W3fu10)"] diff --git a/tests/migration-state.d/ioctl-negative.toml b/tests/migration-state.d/ioctl-negative.toml index 8a5757bee9..ec5671557d 100644 --- a/tests/migration-state.d/ioctl-negative.toml +++ b/tests/migration-state.d/ioctl-negative.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["cargo nextest run --workspace (d2b-host ioctl_policy #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-priv-broker ops::device #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] +successor_ids = ["cargo nextest run --workspace (d2b-host ioctl_policy #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-broker ops::device #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] diff --git a/tests/migration-state.d/kernel-module-matrix.toml b/tests/migration-state.d/kernel-module-matrix.toml index a4dfc2e8f1..170208978b 100644 --- a/tests/migration-state.d/kernel-module-matrix.toml +++ b/tests/migration-state.d/kernel-module-matrix.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["cargo nextest run --workspace (d2b-host modules #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-priv-broker ops::modprobe #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] +successor_ids = ["cargo nextest run --workspace (d2b-host modules #[test]s)", "tests/tools/rust-workspace-checks.sh (d2b-broker ops::modprobe #[test]s via broker-workspace cargo test --workspace)", "tests/tools/assert-pinned-tests.sh (fail-closed pin across both workspaces)"] diff --git a/tests/migration-state.d/l1c-privilege-oracle.toml b/tests/migration-state.d/l1c-privilege-oracle.toml index 129ba668bc..1793fe3fc5 100644 --- a/tests/migration-state.d/l1c-privilege-oracle.toml +++ b/tests/migration-state.d/l1c-privilege-oracle.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["vmChecks..privilege-oracle (tests/host-integration/privilege-oracle.nix - hermetic runNixOSTest asserting the live d2b-priv-broker /proc posture: bounded CapBnd, empty CapAmb, seccomp filter active, d2b.slice cgroup placement, derived from the rendered systemd unit)"] +successor_ids = ["vmChecks..privilege-oracle (tests/host-integration/privilege-oracle.nix - hermetic runNixOSTest asserting the live d2b-broker /proc posture: bounded CapBnd, empty CapAmb, seccomp filter active, d2b.slice cgroup placement, derived from the rendered systemd unit)"] diff --git a/tests/migration-state.d/minijail-validator-wayland-proxy.toml b/tests/migration-state.d/minijail-validator-wayland-proxy.toml index 9b756b3cfb..44a0b088bd 100644 --- a/tests/migration-state.d/minijail-validator-wayland-proxy.toml +++ b/tests/migration-state.d/minijail-validator-wayland-proxy.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["packages/d2b-contract-tests/tests/minijail_gpu.rs (gated via the D2B_FIXTURES / D2B_FIXTURES_FULL contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh -> static.sh) - wayland_proxy_profile_source_shape (Layer-1 assert_profile_source over nixos-modules/minijail-profiles.nix: role = \"wayland-proxy\" declaration, mandatory seccompPolicyRef = \"w1-wayland-proxy\", empty capabilities/no CAP_ in the profile block, requiresStartRoot never true, no userNamespace, writable path /run/d2b-wlproxy present, no PipeWire/Pulse bind in the block, umask = 7, deviceBinds = [ ] empty), wayland_proxy_policy_ref_device_classes_source (Layer-1 assert_policy_ref_entry: \"w1-wayland-proxy\" in packages/d2b-priv-broker/src/live_handlers.rs policy_ref_device_classes), wayland_proxy_rust_variants_declared_source (Layer-1 assert_rust_variant_exists: ProcessRole::WaylandProxy in packages/d2b-core/src/processes.rs and RunnerRole::WaylandProxy in packages/d2b-contracts/src/broker_wire.rs), wayland_proxy_rendered_profile_shape (Layer-1 assert_installed_profiles_consistent applied to the REAL rendered RoleProfile - strictly stronger than the bash gate which scanned /etc/d2b/minijail-profiles/*wayland-proxy*.json and skipped when absent: empty caps, w1-wayland-proxy seccomp, empty deviceBinds + bindMounts, dedicated runtime dir /run/d2b-wlproxy/ as the sole writable path, no PipeWire/Pulse path access, umask 0o007, no broker-pre-NS userNamespace). The bash gate's D2B_LIVE=1 Layer-2 (minijail0 /bin/true positive probe; /var/lib/d2b/validated/p1-wayland-proxy.json evidence write) is a live host-execution layer and does not port to the contract layer."] +successor_ids = ["packages/d2b-contract-tests/tests/minijail_gpu.rs (gated via the D2B_FIXTURES / D2B_FIXTURES_FULL contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh -> static.sh) - wayland_proxy_profile_source_shape (Layer-1 assert_profile_source over nixos-modules/minijail-profiles.nix: role = \"wayland-proxy\" declaration, mandatory seccompPolicyRef = \"w1-wayland-proxy\", empty capabilities/no CAP_ in the profile block, requiresStartRoot never true, no userNamespace, writable path /run/d2b-wlproxy present, no PipeWire/Pulse bind in the block, umask = 7, deviceBinds = [ ] empty), wayland_proxy_policy_ref_device_classes_source (Layer-1 assert_policy_ref_entry: \"w1-wayland-proxy\" in packages/d2b-broker/src/live_handlers.rs policy_ref_device_classes), wayland_proxy_rust_variants_declared_source (Layer-1 assert_rust_variant_exists: ProcessRole::WaylandProxy in packages/d2b-core/src/processes.rs and RunnerRole::WaylandProxy in packages/d2b-contracts/src/broker_wire.rs), wayland_proxy_rendered_profile_shape (Layer-1 assert_installed_profiles_consistent applied to the REAL rendered RoleProfile - strictly stronger than the bash gate which scanned /etc/d2b/minijail-profiles/*wayland-proxy*.json and skipped when absent: empty caps, w1-wayland-proxy seccomp, empty deviceBinds + bindMounts, dedicated runtime dir /run/d2b-wlproxy/ as the sole writable path, no PipeWire/Pulse path access, umask 0o007, no broker-pre-NS userNamespace). The bash gate's D2B_LIVE=1 Layer-2 (minijail0 /bin/true positive probe; /var/lib/d2b/validated/p1-wayland-proxy.json evidence write) is a live host-execution layer and does not port to the contract layer."] diff --git a/tests/migration-state.d/nft-coexistence.toml b/tests/migration-state.d/nft-coexistence.toml index c14c8281a0..4e0381847b 100644 --- a/tests/migration-state.d/nft-coexistence.toml +++ b/tests/migration-state.d/nft-coexistence.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["cargo test --workspace (d2b-host nftables::tests::{coexistence_matrix_all_7_rows,detector_clean_host,detector_single_manager_unambiguous,detector_multiple_managers_unknown}) + broker test pass (d2b-priv-broker ops::nft::tests::{refuses_when_firewalld_declared_coexist,applies_on_clean_host_with_coexist,nft_script_includes_all_four_chains}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/nft-coexistence.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] +successor_ids = ["cargo test --workspace (d2b-host nftables::tests::{coexistence_matrix_all_7_rows,detector_clean_host,detector_single_manager_unambiguous,detector_multiple_managers_unknown}) + broker test pass (d2b-broker ops::nft::tests::{refuses_when_firewalld_declared_coexist,applies_on_clean_host_with_coexist,nft_script_includes_all_four_chains}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/nft-coexistence.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] diff --git a/tests/migration-state.d/otel-acl-migration-eval.toml b/tests/migration-state.d/otel-acl-migration-eval.toml index 15d8058876..c611bb06bf 100644 --- a/tests/migration-state.d/otel-acl-migration-eval.toml +++ b/tests/migration-state.d/otel-acl-migration-eval.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["packages/d2b-contract-tests/tests/policy_guest.rs::otel_relay_acl_module_not_publicly_imported_and_broker_handler_present (asserts nixos-modules/default.nix carries no uncommented ./host-otel-relay-acl.nix import and that packages/d2b-priv-broker/src/runtime.rs still carries the RunnerRole::OtelHostBridge spawn handler); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_guest.rs::otel_relay_acl_module_not_publicly_imported_and_broker_handler_present (asserts nixos-modules/default.nix carries no uncommented ./host-otel-relay-acl.nix import and that packages/d2b-broker/src/runtime.rs still carries the RunnerRole::OtelHostBridge spawn handler); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] diff --git a/tests/migration-state.d/path-safety-violation-fs.toml b/tests/migration-state.d/path-safety-violation-fs.toml index d0b6b65b36..946cae1a2d 100644 --- a/tests/migration-state.d/path-safety-violation-fs.toml +++ b/tests/migration-state.d/path-safety-violation-fs.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["broker test pass (d2b-priv-broker ops::hosts::tests::{refuses_symlink_target,writes_managed_block_into_fresh_file,idempotent_when_block_matches}, ops::nm::tests::reload_failure_rolls_back, ops::state_dir::tests::{refuses_absolute_relative_path,refuses_parent_dir_escape,idempotent_reuses_existing_dirs,creates_base_and_relative_paths}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/path-safety-violation-fs.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] +successor_ids = ["broker test pass (d2b-broker ops::hosts::tests::{refuses_symlink_target,writes_managed_block_into_fresh_file,idempotent_when_block_matches}, ops::nm::tests::reload_failure_rolls_back, ops::state_dir::tests::{refuses_absolute_relative_path,refuses_parent_dir_escape,idempotent_reuses_existing_dirs,creates_base_and_relative_paths}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/path-safety-violation-fs.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin)"] diff --git a/tests/migration-state.d/polkit-allowlist-eval.toml b/tests/migration-state.d/polkit-allowlist-eval.toml index c614e1ee42..e5f9fe17c1 100644 --- a/tests/migration-state.d/polkit-allowlist-eval.toml +++ b/tests/migration-state.d/polkit-allowlist-eval.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["packages/d2b-contract-tests/tests/policy_misc.rs::polkit_allowlist_daemon_only_singletons (nixos-modules/host-polkit.nix: launcher-group allowlist names ONLY the daemon-only singletons d2bd.service/d2b-priv-broker.service/d2b-priv-broker.socket, the executable region (awk '/^in$/,0' scope) carries no retired per-VM/per-env unit shapes, and the surviving grant keeps the manage-units action-id guard, the `d2b` group guard, the start/stop/restart verb allowlist, and exactly one polkit.addRule callback); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] +successor_ids = ["packages/d2b-contract-tests/tests/policy_misc.rs::polkit_allowlist_daemon_only_singletons (nixos-modules/host-polkit.nix: launcher-group allowlist names ONLY the daemon-only singletons d2bd.service/d2b-broker.service/d2b-broker.socket, the executable region (awk '/^in$/,0' scope) carries no retired per-VM/per-env unit shapes, and the surviving grant keeps the manage-units action-id guard, the `d2b` group guard, the start/stop/restart verb allowlist, and exactly one polkit.addRule callback); gated via the D2B_FIXTURES contract step `cargo test -p d2b-contract-tests` in tests/tools/rust-workspace-checks.sh"] diff --git a/tests/migration-state.d/usbip-firewall-skeleton.toml b/tests/migration-state.d/usbip-firewall-skeleton.toml index 7ecf229af1..7182153186 100644 --- a/tests/migration-state.d/usbip-firewall-skeleton.toml +++ b/tests/migration-state.d/usbip-firewall-skeleton.toml @@ -1,2 +1,2 @@ status = "retired" -successor_ids = ["cargo test --workspace (d2b-host nftables::tests::{usbip_carveout_inserted_before_generic,comment_marker_prefix_on_every_managed_rule,fake_backend_refuses_carveout_after_generic}) - already gated in tests/tools/rust-workspace-checks.sh", "broker-workspace cargo test --workspace [+ --features layer1-bootstrap] (d2b-priv-broker ops::usbip_firewall::tests::{bind_firewall_rule_produces_audit_with_busid_and_hash,carveout_ordering_invariant_via_op,w6_ops_refused_with_unknown_operation_audit}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/usbip-firewall-skeleton.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across both workspaces)", "retired script's UsbipBind/UsbipUnbind/UsbipProxyReconcile static grep is covered by ops::usbip_firewall::tests::w6_ops_refused_with_unknown_operation_audit, which enumerates those variants and asserts unknown-operation refusal audit payloads"] +successor_ids = ["cargo test --workspace (d2b-host nftables::tests::{usbip_carveout_inserted_before_generic,comment_marker_prefix_on_every_managed_rule,fake_backend_refuses_carveout_after_generic}) - already gated in tests/tools/rust-workspace-checks.sh", "broker-workspace cargo test --workspace [+ --features layer1-bootstrap] (d2b-broker ops::usbip_firewall::tests::{bind_firewall_rule_produces_audit_with_busid_and_hash,carveout_ordering_invariant_via_op,w6_ops_refused_with_unknown_operation_audit}) - already gated in tests/tools/rust-workspace-checks.sh", "tests/golden/pinned/usbip-firewall-skeleton.txt + tests/tools/assert-pinned-tests.sh (fail-closed presence pin across both workspaces)", "retired script's UsbipBind/UsbipUnbind/UsbipProxyReconcile static grep is covered by ops::usbip_firewall::tests::w6_ops_refused_with_unknown_operation_audit, which enumerates those variants and asserts unknown-operation refusal audit payloads"] diff --git a/tests/static.sh b/tests/static.sh index 63d123b98f..94b5ac4694 100755 --- a/tests/static.sh +++ b/tests/static.sh @@ -1187,7 +1187,7 @@ d2b_static_gate_end "W1 bundle/schema static gates" # ----------------------------------------------------------------------------- # Control-plane skeleton gates (per the plan-of-record, plan.md § # "###: Rust workspace and API skeleton"). These cover the d2b-contracts -# wire types, d2b-priv-broker dispatch, d2bd socket auth + state +# wire types, d2b-broker dispatch, d2bd socket auth + state # lock + version negotiation, the Rust-native CLI shim, generated docs + # error-codes, and bounded fuzz of the manifest_v04 / bundle parsers. # ----------------------------------------------------------------------------- @@ -1198,7 +1198,7 @@ if [ -d "$ROOT/packages" ]; then _W2_WORKSPACE_TARGET=$(d2b_cargo_target_dir workspace) _W2_BROKER_TARGET=$(d2b_cargo_target_dir broker) CARGO_TARGET_DIR="$_W2_WORKSPACE_TARGET" cargo build --manifest-path "$ROOT/Cargo.toml" --quiet -p d2b -p d2bd -p xtask --bins - CARGO_TARGET_DIR="$_W2_BROKER_TARGET" cargo build --manifest-path "$ROOT/Cargo.toml" --quiet -p d2b-priv-broker --features layer1-bootstrap + CARGO_TARGET_DIR="$_W2_BROKER_TARGET" cargo build --manifest-path "$ROOT/Cargo.toml" --quiet -p d2b-broker --features layer1-bootstrap fi d2b_time_end "W2 cargo prebuild" d2b_time_begin "W2 CLI smoke prewarm" diff --git a/tests/tools/layer1-jobs.py b/tests/tools/layer1-jobs.py index 20b98c0549..0b300bfb42 100644 --- a/tests/tools/layer1-jobs.py +++ b/tests/tools/layer1-jobs.py @@ -417,11 +417,11 @@ def rust_job(job: dict[str, Any]) -> str: with: workspaces: | packages -> target - packages/d2b-priv-broker -> target + packages/d2b-broker -> target packages/d2b-guest-shell-runner -> target cache-directories: | - packages/d2b-priv-broker/target-layer1 - packages/d2b-priv-broker/target-fakebackends + packages/d2b-broker/target-layer1 + packages/d2b-broker/target-fakebackends tests/tools/no-bash-ast-walker/target .scratch/rust-test-cache prefix-key: "v3-rust" diff --git a/tests/tools/tier0-first-pass.sh b/tests/tools/tier0-first-pass.sh index d6d0635587..f44aac69bc 100755 --- a/tests/tools/tier0-first-pass.sh +++ b/tests/tools/tier0-first-pass.sh @@ -77,7 +77,7 @@ PROCESS_MARKER_FILENAME_RE='(^|[-_.])(W|w|P)[0-9]+((fu|a)[0-9]*|-(fu|followup)([ # and fails both this gate and the independent xtask checker. There is no mutable # count budget to raise alongside an added exemption. PROCESS_MARKER_PIN=tests/golden/pinned/process-marker-legacy-paths.json -PROCESS_MARKER_UNIVERSE_SHA256=0f6899e939fd8e0b49f41b56a0221f33552d79348adc2853927d338e610f8f34 +PROCESS_MARKER_UNIVERSE_SHA256=732bf6730ae93f66305eadb338f9f81dba190fafa553cd0a8ca21b5bae16ad5e LEGACY_PROCESS_MARKER_PATHS=() load_process_marker_pin() { diff --git a/tests/unit/gates/BUILD.bazel b/tests/unit/gates/BUILD.bazel index ae599c6b1f..ca4dbf0c1c 100644 --- a/tests/unit/gates/BUILD.bazel +++ b/tests/unit/gates/BUILD.bazel @@ -17,14 +17,14 @@ sh_test( srcs = ["performance-budgets.sh"], data = [ "//packages/d2b:d2b", - "//packages/d2b-priv-broker:d2b_priv_broker", + "//packages/d2b-broker:d2b_broker", "//packages/d2bd:d2bd", "//packages/xtask:xtask", "//:tests_root_sources", "//bazel/checks/rust:source_hygiene_sources", ], env = { - "D2B_BROKER_BIN": "$(execpath //packages/d2b-priv-broker:d2b_priv_broker)", + "D2B_BROKER_BIN": "$(execpath //packages/d2b-broker:d2b_broker)", "D2B_CLI_BIN": "$(execpath //packages/d2b:d2b)", "D2B_DAEMON_BIN": "$(execpath //packages/d2bd:d2bd)", "D2B_XTASK_BIN": "$(execpath //packages/xtask:xtask)", diff --git a/tests/unit/gates/performance-budgets.sh b/tests/unit/gates/performance-budgets.sh index 6f42b5fac5..4ffb984e3e 100755 --- a/tests/unit/gates/performance-budgets.sh +++ b/tests/unit/gates/performance-budgets.sh @@ -44,7 +44,7 @@ add_cleanup "rm -rf -- \"$scratch\"" bundle_root=$(d2b_cli_smoke_bundle_tree) cli_bin=$(d2b_cli_native_bin) daemon_bin=$(d2b_daemon_native_bin) -[ -x "${D2B_BROKER_BIN:-}" ] || fail "D2B_BROKER_BIN must name the declared //packages/d2b-priv-broker:d2b_priv_broker artifact" +[ -x "${D2B_BROKER_BIN:-}" ] || fail "D2B_BROKER_BIN must name the declared //packages/d2b-broker:d2b_broker artifact" broker_bin="$D2B_BROKER_BIN" wait_for_socket() { diff --git a/tests/unit/nix/BUILD.bazel b/tests/unit/nix/BUILD.bazel index 343b2d933e..4145940f37 100644 --- a/tests/unit/nix/BUILD.bazel +++ b/tests/unit/nix/BUILD.bazel @@ -9,6 +9,7 @@ exports_files([ "cases/broker-caps.nix", "cases/broker-service-posture.nix", "cases/broker-socket-activation.nix", + "cases/guest-broker.nix", "cases/bundle-artifacts-compiler.nix", "cases/bundle-artifacts-digest.nix", "cases/bundle-artifacts-envelope.nix", @@ -42,6 +43,7 @@ exports_files([ "cases/provider-projection-fields.nix", "cases/provider-runtime-contracts.nix", "cases/provider-system-providers.nix", + "cases/prebuilt-broker.nix", "cases/readiness-waves.nix", "cases/realms-artifacts.nix", "cases/realms-controller.nix", @@ -65,6 +67,7 @@ exports_files([ "helpers/surface.nix", "run-surface.nix", "surfaces/daemon.nix", + "surfaces/broker-profiles.nix", "surfaces/desktop-interaction.nix", "surfaces/eval-jobs.nix", "surfaces/examples.nix", diff --git a/tests/unit/nix/cases/broker-caps.nix b/tests/unit/nix/cases/broker-caps.nix index 074194c58f..b04e9ff004 100644 --- a/tests/unit/nix/cases/broker-caps.nix +++ b/tests/unit/nix/cases/broker-caps.nix @@ -1,6 +1,6 @@ # nix-unit cases migrated from tests/broker-caps-eval.sh. # -# Asserts that systemd.services.d2b-priv-broker.serviceConfig +# Asserts that systemd.services.d2b-broker.serviceConfig # .CapabilityBoundingSet matches the canonical broker bounding set EXACTLY # (no additions, no omissions) and that AmbientCapabilities carries the # sentinel empty-string entry that drops all ambient caps. @@ -62,7 +62,7 @@ let d2b.daemonExperimental.enable = true; }; - brokerService = (mkEval [ base ]).config.systemd.services.d2b-priv-broker; + brokerService = (mkEval [ base ]).config.systemd.services.d2b-broker; svc = brokerService.serviceConfig; env = brokerService.environment; cbs = svc.CapabilityBoundingSet or null; diff --git a/tests/unit/nix/cases/broker-service-posture.nix b/tests/unit/nix/cases/broker-service-posture.nix index 8e8d891cee..e043739fb3 100644 --- a/tests/unit/nix/cases/broker-service-posture.nix +++ b/tests/unit/nix/cases/broker-service-posture.nix @@ -18,7 +18,7 @@ let }; cfg = (mkEval [ minimal ]).config; - svcCfg = cfg.systemd.services.d2b-priv-broker.serviceConfig or { }; + svcCfg = cfg.systemd.services.d2b-broker.serviceConfig or { }; sliceCfg = cfg.systemd.slices.d2b.sliceConfig or { }; tmpfiles = cfg.systemd.tmpfiles.rules; in @@ -56,7 +56,7 @@ in expected = { execStartPreAvoidsSetEnvironment = true; execStartAvoidsSetEnvironment = true; - usesOptionalUnitLocalEnvironmentFile = "-/run/d2b/broker/priv-broker.env"; + usesOptionalUnitLocalEnvironmentFile = "-/run/d2b/broker/broker.env"; environmentFileParentNotGroupWritable = true; }; }; diff --git a/tests/unit/nix/cases/broker-socket-activation.nix b/tests/unit/nix/cases/broker-socket-activation.nix index e11780d132..3769ca2fc1 100644 --- a/tests/unit/nix/cases/broker-socket-activation.nix +++ b/tests/unit/nix/cases/broker-socket-activation.nix @@ -1,10 +1,10 @@ # nix-unit cases migrated from tests/broker-socket-activation-eval.sh. # -# Asserts host-broker.nix wires d2b-priv-broker for socket activation: +# Asserts host-broker.nix wires d2b-broker for socket activation: # # (A) ExecStart does NOT pass --socket-path (with SD_LISTEN_FDS the broker # MUST adopt the inherited fd, not self-bind the path). -# (B) systemd.sockets.d2b-priv-broker exists (socket-activated). +# (B) systemd.sockets.d2b-broker exists (socket-activated). # (C) socketConfig.FileDescriptorName is "priv.sock" (matched by # adopt_listen_fd() against LISTEN_FDNAMES). # (D) socketConfig.ListenSequentialPacket is /run/d2b/priv.sock. @@ -41,7 +41,7 @@ let }; cfg = (mkEval [ minimal ]).config; - sockCfg = cfg.systemd.sockets.d2b-priv-broker.socketConfig or { }; + sockCfg = cfg.systemd.sockets.d2b-broker.socketConfig or { }; in { # (A) No uncommented --socket-path in host-broker.nix. @@ -57,7 +57,7 @@ in # (B) Socket unit exists (broker is socket-activated). "broker-socket-activation/has-socket" = { - expr = cfg.systemd.sockets ? d2b-priv-broker; + expr = cfg.systemd.sockets ? d2b-broker; expected = true; }; # (C) FileDescriptorName matches the name adopt_listen_fd() validates. @@ -73,13 +73,13 @@ in "broker-socket-activation/socket-after-tmpfiles" = { expr = builtins.elem "systemd-tmpfiles-setup.service" - (cfg.systemd.sockets.d2b-priv-broker.after or [ ]); + (cfg.systemd.sockets.d2b-broker.after or [ ]); expected = true; }; "broker-socket-activation/socket-requires-tmpfiles" = { expr = builtins.elem "systemd-tmpfiles-setup.service" - (cfg.systemd.sockets.d2b-priv-broker.requires or [ ]); + (cfg.systemd.sockets.d2b-broker.requires or [ ]); expected = true; }; } diff --git a/tests/unit/nix/cases/d2bd-startup-smoke.nix b/tests/unit/nix/cases/d2bd-startup-smoke.nix index 0c76cadb55..f481f84398 100644 --- a/tests/unit/nix/cases/d2bd-startup-smoke.nix +++ b/tests/unit/nix/cases/d2bd-startup-smoke.nix @@ -1,7 +1,7 @@ # nix-unit cases migrated from tests/d2bd-startup-smoke.sh Phase 1. # # Eval-only daemon/broker startup surface: systemd tmpfiles rules, -# d2b-priv-broker.{socket,service}, d2bd.service, and the small +# d2b-broker.{socket,service}, d2bd.service, and the small # evidence-record shape assertion the retired shell gate carried before its # opt-in D2B_LIVE section. { mkEval, lib, ... }: @@ -61,9 +61,9 @@ let tmpfiles = cfg.systemd.tmpfiles.rules; services = cfg.systemd.services; sockets = cfg.systemd.sockets; - brokerService = services.d2b-priv-broker; + brokerService = services.d2b-broker; daemonService = services.d2bd; - brokerSocket = sockets.d2b-priv-broker; + brokerSocket = sockets.d2b-broker; rulesForPath = path: builtins.filter (lib.hasInfix (" " + path + " ")) tmpfiles; @@ -233,7 +233,7 @@ in }; "d2bd-startup-smoke/daemon-wants-broker-socket" = { - expr = builtins.elem "d2b-priv-broker.socket" (daemonService.wants or [ ]); + expr = builtins.elem "d2b-broker.socket" (daemonService.wants or [ ]); expected = true; }; diff --git a/tests/unit/nix/cases/daemon-autostart.nix b/tests/unit/nix/cases/daemon-autostart.nix index b596882dab..bea3a68bca 100644 --- a/tests/unit/nix/cases/daemon-autostart.nix +++ b/tests/unit/nix/cases/daemon-autostart.nix @@ -204,8 +204,8 @@ in }; "daemon-lifecycle/broker-shutdown-ordering" = { expr = - builtins.elem "d2b-priv-broker.service" lifecycleCfg.systemd.services.d2bd.after - && builtins.elem "d2b-priv-broker.socket" lifecycleCfg.systemd.services.d2bd.after + builtins.elem "d2b-broker.service" lifecycleCfg.systemd.services.d2bd.after + && builtins.elem "d2b-broker.socket" lifecycleCfg.systemd.services.d2bd.after && builtins.elem "dbus.service" lifecycleCfg.systemd.services.d2bd.after; expected = true; }; diff --git a/tests/unit/nix/cases/guest-broker.nix b/tests/unit/nix/cases/guest-broker.nix new file mode 100644 index 0000000000..d2fc406abb --- /dev/null +++ b/tests/unit/nix/cases/guest-broker.nix @@ -0,0 +1,156 @@ +# Guest broker profile and instance-boundary evaluation contract. +# +# The fake host-tool package keeps this case focused on module composition. +# Host-broker's metadata-only overrideAttrs must still select the same output +# path as the Guest profile. +{ mkGuestEval, lib, pkgs, flakeRoot, ... }: + +let + broker = pkgs.runCommand "d2b-broker-guest-broker-test" { } '' + mkdir -p "$out/bin" + touch "$out/bin/d2b-broker" + ''; + cutoverRunner = pkgs.runCommand "d2b-cutover-guest-broker-test" { } '' + mkdir -p "$out/bin" + touch "$out/bin/d2b-cutover-runner" + ''; + d2bHostTools = { + inherit broker cutoverRunner; + }; + + optionSinks = { lib, ... }: { + options.d2b = lib.mkOption { + type = lib.types.submodule { + freeformType = lib.types.attrsOf lib.types.anything; + }; + default = { }; + }; + options.environment.systemPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + }; + options.users.users = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + }; + options.users.groups = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + }; + options.systemd.services = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + }; + options.systemd.sockets = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + }; + options.systemd.slices = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + }; + options.systemd.tmpfiles.rules = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; + + common = usePrebuilt: { ... }: { + config = { + d2b.site = { + stateDir = "/var/lib/d2b"; + usePrebuiltHostTools = usePrebuilt; + bundle = { }; + audit = { + retentionDays = 14; + }; + }; + d2b._index = { + realms.enabledList = [ ]; + }; + }; + }; + + eval = usePrebuilt: module: + (mkGuestEval { + modules = [ + optionSinks + (common usePrebuilt) + module + ]; + specialArgs = { + inherit d2bHostTools; + name = "guest-test"; + d2bUsePrebuiltHostTools = usePrebuilt; + }; + }).config; + + host = eval false (import (flakeRoot + "/nixos-modules/host-broker.nix") { + inputs = { }; + }); + guest = eval false (import (flakeRoot + "/nixos-modules/guest-broker.nix")); + prebuiltHost = eval true (import (flakeRoot + "/nixos-modules/host-broker.nix") { + inputs = { }; + }); + prebuiltGuest = eval true (import (flakeRoot + "/nixos-modules/guest-broker.nix")); + + brokerFrom = packages: + lib.findFirst (package: package.outPath == broker.outPath) null packages; + hostBroker = brokerFrom host.environment.systemPackages; + guestBroker = brokerFrom guest.environment.systemPackages; + prebuiltHostBroker = brokerFrom prebuiltHost.environment.systemPackages; + prebuiltGuestBroker = brokerFrom prebuiltGuest.environment.systemPackages; + prebuiltBroker = + (import (flakeRoot + "/nix/prebuilt.nix") { inherit pkgs lib; })."d2b-broker"; + hostService = host.systemd.services.d2b-broker.serviceConfig; + guestService = guest.systemd.services.d2b-broker-guest.serviceConfig; + hostExecStart = hostService.ExecStart; + guestExecStart = guestService.ExecStart; + prebuiltHostExecStart = + prebuiltHost.systemd.services.d2b-broker.serviceConfig.ExecStart; + prebuiltGuestExecStart = + prebuiltGuest.systemd.services.d2b-broker-guest.serviceConfig.ExecStart; + hostSocket = + host.systemd.sockets.d2b-broker.socketConfig.ListenSequentialPacket; + guestSocket = + guest.systemd.sockets.d2b-broker-guest.socketConfig.ListenSequentialPacket; +in +{ + "guest-broker/broker-package-out-path-equality" = { + expr = hostBroker != null + && guestBroker != null + && hostBroker.outPath == guestBroker.outPath; + expected = true; + }; + "guest-broker/exec-start-binary-store-relation" = { + expr = hostBroker != null + && guestBroker != null + && lib.hasPrefix "${hostBroker.outPath}/bin/d2b-broker host " hostExecStart + && lib.hasPrefix "${guestBroker.outPath}/bin/d2b-broker guest " guestExecStart; + expected = true; + }; + "guest-broker/legacy-prebuilt-selects-built-profile-package" = { + expr = prebuiltHostBroker != null + && prebuiltGuestBroker != null + && prebuiltHostBroker.outPath == broker.outPath + && prebuiltGuestBroker.outPath == broker.outPath + && prebuiltHostBroker.outPath != prebuiltBroker.outPath + && lib.hasPrefix "${broker.outPath}/bin/d2b-broker host " prebuiltHostExecStart + && lib.hasPrefix "${broker.outPath}/bin/d2b-broker guest " prebuiltGuestExecStart; + expected = true; + }; + "guest-broker/instance-roots-are-distinct" = { + expr = hostSocket == "/run/d2b/priv.sock" + && guestSocket == "/run/d2b/guest-broker.sock" + && hostSocket != guestSocket + && lib.hasInfix "--state-dir /var/lib/d2b" hostExecStart + && lib.hasInfix "--state-dir /var/lib/d2b/guest-broker" guestExecStart + && lib.hasInfix "--audit-dir /var/lib/d2b/audit" hostExecStart + && lib.hasInfix "--audit-dir /var/lib/d2b/guest-audit" guestExecStart; + expected = true; + }; + "guest-broker/service-kill-mode-process" = { + expr = guestService.KillMode; + expected = "process"; + }; +} diff --git a/tests/unit/nix/cases/prebuilt-broker.nix b/tests/unit/nix/cases/prebuilt-broker.nix new file mode 100644 index 0000000000..22df777fac --- /dev/null +++ b/tests/unit/nix/cases/prebuilt-broker.nix @@ -0,0 +1,29 @@ +# Prebuilt broker release compatibility and output-shape contract. +# +# The v1.4.1 release asset still contains the legacy broker executable. The +# consumer-facing derivation must keep the released fetch identity while +# exposing the current d2b-broker binary name. +{ lib, pkgs, flakeRoot, ... }: + +let + manifest = builtins.fromJSON (builtins.readFile (flakeRoot + "/nix/prebuilt.json")); + spec = manifest.binaries."d2b-broker"; + prebuilt = import (flakeRoot + "/nix/prebuilt.nix") { inherit pkgs lib; }; + broker = prebuilt."d2b-broker"; + installPhase = broker.installPhase; +in +{ + "prebuilt-broker/released-asset-identity" = { + expr = spec.url + == "https://github.com/vicondoa/d2b/releases/download/v1.4.1/d2b-priv-broker-1.4.1-x86_64-linux.tar.gz" + && spec.hash == "sha256-qY5eXFpenVG6UrgwwV4nQC+QQxFr3O+mJHVdOSj256A="; + expected = true; + }; + "prebuilt-broker/legacy-binary-renamed-to-current-output" = { + expr = (spec.sourceBinary or null) == "d2b-priv-broker" + && broker.pname == "d2b-broker" + && lib.hasInfix "d2b-priv-broker" installPhase + && lib.hasInfix "$out/bin/d2b-broker" installPhase; + expected = true; + }; +} diff --git a/tests/unit/nix/cases/realms.nix b/tests/unit/nix/cases/realms.nix index 7c6251536d..d67c332b09 100644 --- a/tests/unit/nix/cases/realms.nix +++ b/tests/unit/nix/cases/realms.nix @@ -723,8 +723,8 @@ let daemonPrincipalIsShared = false; }; broker = { - socketUnitName = "${homeUnitPrefix}-priv-broker.socket"; - serviceUnitName = "${homeUnitPrefix}-priv-broker.service"; + socketUnitName = "${homeUnitPrefix}-broker.socket"; + serviceUnitName = "${homeUnitPrefix}-broker.service"; materializedSocket = true; materializedService = true; }; @@ -987,9 +987,9 @@ let units = { homeDaemon = { wantedBy = homeDaemonUnit.wantedBy; - wantsRootBrokerSocket = builtins.elem "d2b-priv-broker.socket" homeDaemonUnit.wants; - afterRootBrokerSocket = builtins.elem "d2b-priv-broker.socket" homeDaemonUnit.after; - afterRootBrokerService = builtins.elem "d2b-priv-broker.service" homeDaemonUnit.after; + wantsRootBrokerSocket = builtins.elem "d2b-broker.socket" homeDaemonUnit.wants; + afterRootBrokerSocket = builtins.elem "d2b-broker.socket" homeDaemonUnit.after; + afterRootBrokerService = builtins.elem "d2b-broker.service" homeDaemonUnit.after; wantsBrokerSocket = builtins.elem home.broker.socketUnitName homeDaemonUnit.wants; afterBrokerSocket = builtins.elem home.broker.socketUnitName homeDaemonUnit.after; afterBrokerService = builtins.elem home.broker.serviceUnitName homeDaemonUnit.after; @@ -1188,8 +1188,8 @@ let user = "root"; group = realms.byPath.home.controller.broker.group; socketPath = "/run/d2b/realms/home/broker.sock"; - socketUnitName = "${homeUnitPrefix}-priv-broker.socket"; - serviceUnitName = "${homeUnitPrefix}-priv-broker.service"; + socketUnitName = "${homeUnitPrefix}-broker.socket"; + serviceUnitName = "${homeUnitPrefix}-broker.service"; auditDir = "/var/lib/d2b/audit/realms/home"; materializedSocket = true; materializedService = true; @@ -1448,8 +1448,8 @@ let unitOrdering = { childAfterParent = builtins.elem home.daemon.serviceName devDaemonUnit.after; parentDoesNotAfterChild = !(builtins.elem dev.daemon.serviceName homeDaemonUnit.after); - parentAfterRootBrokerSocket = builtins.elem "d2b-priv-broker.socket" homeDaemonUnit.after; - parentAfterRootBrokerService = builtins.elem "d2b-priv-broker.service" homeDaemonUnit.after; + parentAfterRootBrokerSocket = builtins.elem "d2b-broker.socket" homeDaemonUnit.after; + parentAfterRootBrokerService = builtins.elem "d2b-broker.service" homeDaemonUnit.after; }; socketAccess = { inherit (homeBrokerSocket.socketConfig) ListenSequentialPacket SocketGroup SocketMode; diff --git a/tests/unit/nix/surfaces/broker-profiles.nix b/tests/unit/nix/surfaces/broker-profiles.nix new file mode 100644 index 0000000000..03175f935d --- /dev/null +++ b/tests/unit/nix/surfaces/broker-profiles.nix @@ -0,0 +1,10 @@ +{ lib, pkgs, system, nixpkgs, inputs, d2bModule, d2bLib, flakeRoot, modules }: + +import ../helpers/surface.nix { + inherit lib pkgs system nixpkgs inputs d2bModule d2bLib flakeRoot modules; + name = "broker-profiles"; + caseFiles = [ + ../cases/guest-broker.nix + ../cases/prebuilt-broker.nix + ]; +} diff --git a/tests/unit/smoke/smoke-eval-tpm.nix b/tests/unit/smoke/smoke-eval-tpm.nix index ccd25e1443..7274810567 100644 --- a/tests/unit/smoke/smoke-eval-tpm.nix +++ b/tests/unit/smoke/smoke-eval-tpm.nix @@ -130,7 +130,7 @@ let # host-sidecars.nix. The TPM sidecar is now spawned by the # d2b priv-broker as `SpawnRunner{role: Swtpm}`; the # equivalent pre-start session-flush + socket ACL handoff lives - # in `packages/d2b-priv-broker/src/runners/swtpm.rs`. The + # in `packages/d2b-broker/src/runners/swtpm.rs`. The # legacy per-VM systemd assertions (ExecStartPre/ExecStartPost, # microvm@ wants/after wiring, host-sidecars.nix flush helper # source check) are deferred to a forthcoming From bcdc8540741ab3960b602131fd25dbdae449eccd Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 12:21:18 -0700 Subject: [PATCH 03/56] resources: fence controller assignments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 5 + .../2026-08-21-resource-assignment-routing.md | 4 + packages/d2b-bus/BUILD.bazel | 4 + packages/d2b-bus/Cargo.toml | 4 + packages/d2b-bus/src/authorization.rs | 519 ++++- packages/d2b-bus/src/production_rss.rs | 27 +- packages/d2b-bus/src/router.rs | 199 +- packages/d2b-bus/src/session_seam_tests.rs | 719 +++++- .../d2b-bus/tests/production_watch_rss.rs | 1 + .../proto/d2b-resource-v3.proto | 3 + .../src/generated/d2b_resource_v3.rs | 51 +- .../benches/support/reaction.rs | 2 + .../tests/support/mod.rs | 1 + .../d2b-core-controller/src/api_catalog.rs | 50 +- packages/d2b-core-controller/src/audit.rs | 11 +- packages/d2b-core-controller/src/authority.rs | 12 +- packages/d2b-core-controller/src/authz.rs | 32 +- packages/d2b-core-controller/src/cleanup.rs | 11 +- .../src/configuration/bundle_apply.rs | 24 +- .../configuration/generation_transition.rs | 7 +- .../src/configuration/mod.rs | 13 +- .../src/controller_assignment.rs | 2039 +++++++++++++++++ .../d2b-core-controller/src/controllers.rs | 6 +- .../d2b-core-controller/src/coordinator.rs | 5 +- .../d2b-core-controller/src/dependencies.rs | 10 +- .../src/emergency_policy.rs | 12 +- .../d2b-core-controller/src/export_import.rs | 39 +- .../src/export_import_projection.rs | 32 +- packages/d2b-core-controller/src/hints.rs | 9 +- packages/d2b-core-controller/src/lib.rs | 7 + .../src/optional_state_admission.rs | 18 +- .../src/owner_reconcile.rs | 11 +- packages/d2b-core-controller/src/ownership.rs | 17 +- packages/d2b-core-controller/src/providers.rs | 43 +- packages/d2b-core-controller/src/quota.rs | 16 +- packages/d2b-core-controller/src/rbac.rs | 7 +- packages/d2b-core-controller/src/runtime.rs | 13 +- .../d2b-core-controller/src/zone_links.rs | 6 +- .../d2b-core-controller/src/zone_status.rs | 11 +- packages/d2b-resource-api/src/adapter.rs | 219 +- packages/d2b-resource-api/src/admission.rs | 17 +- packages/d2b-resource-api/src/authz.rs | 82 +- packages/d2b-resource-api/src/client.rs | 13 + .../d2b-resource-api/src/emergency_gate.rs | 6 +- packages/d2b-resource-api/src/error.rs | 5 +- packages/d2b-resource-api/src/identity.rs | 27 +- packages/d2b-resource-api/src/lib.rs | 2 + packages/d2b-resource-api/src/quota_gate.rs | 11 +- packages/d2b-resource-api/src/service.rs | 120 +- packages/d2b-resource-api/src/watch.rs | 20 +- packages/d2b-resource-api/src/zone_service.rs | 5 +- packages/d2b-resource-client/BUILD.bazel | 1 + packages/d2b-resource-client/Cargo.toml | 1 + packages/d2b-resource-client/src/dispatch.rs | 10 +- packages/d2b-resource-client/src/error.rs | 5 +- packages/d2b-resource-client/src/lib.rs | 8 +- .../d2b-resource-client/src/process_attach.rs | 8 +- packages/d2b-resource-client/src/target.rs | 25 +- .../d2b-resource-client/src/zone_client.rs | 144 +- packages/d2b-resource-store-redb/src/actor.rs | 41 +- .../src/revision_log.rs | 45 +- packages/d2b-resource-store-redb/src/tests.rs | 4 + .../src/transaction.rs | 691 +++++- packages/d2b-resource-store/src/lib.rs | 34 + packages/d2b/src/context.rs | 52 +- .../src/resource_runtime_support.rs | 12 +- packages/d2bd/BUILD.bazel | 1 + packages/d2bd/src/resource_runtime.rs | 95 +- .../tests/resource_operator_activation.rs | 265 ++- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 2 +- .../broker-production/production/closure.json | 2 +- .../production/metadata.json | 2 +- .../main-product/policy/Cargo.lock | 3 + .../main-product/policy/closure.json | 20 +- .../main-product/policy/metadata.json | 2 +- .../main-product/production/Cargo.lock | 3 + .../main-product/production/closure.json | 14 +- .../main-product/production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 2 +- .../broker-production/production/closure.json | 2 +- .../production/metadata.json | 2 +- .../main-product/policy/Cargo.lock | 3 + .../main-product/policy/closure.json | 20 +- .../main-product/policy/metadata.json | 2 +- .../main-product/production/Cargo.lock | 3 + .../main-product/production/closure.json | 14 +- .../main-product/production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- 121 files changed, 5435 insertions(+), 702 deletions(-) create mode 100644 changelog.d/2026-08-21-resource-assignment-routing.md create mode 100644 packages/d2b-core-controller/src/controller_assignment.rs diff --git a/Cargo.lock b/Cargo.lock index 0fa0f7edac..895788cda6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1023,9 +1023,11 @@ dependencies = [ "async-trait", "d2b-audit", "d2b-contracts", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-controller-toolkit", + "d2b-core-controller", "d2b-resource-api", "d2b-resource-store", "d2b-resource-store-redb", @@ -1033,6 +1035,7 @@ dependencies = [ "d2b-session-unix", "d2b-telemetry", "getrandom 0.3.4", + "protobuf", "quote", "serde_json", "sha2", @@ -1040,6 +1043,7 @@ dependencies = [ "syn 2.0.119", "tempfile", "tokio", + "ttrpc", ] [[package]] @@ -1874,6 +1878,7 @@ dependencies = [ "d2b-contracts", "d2b-contracts-resource", "d2b-contracts-zone-session", + "d2b-core-controller", "tokio", ] diff --git a/changelog.d/2026-08-21-resource-assignment-routing.md b/changelog.d/2026-08-21-resource-assignment-routing.md new file mode 100644 index 0000000000..62ae43c363 --- /dev/null +++ b/changelog.d/2026-08-21-resource-assignment-routing.md @@ -0,0 +1,4 @@ +### Changed + +- Fence Provider controller assignments to one resource target and route scoped + Resource API watches and mutations through the existing Zone bus. diff --git a/packages/d2b-bus/BUILD.bazel b/packages/d2b-bus/BUILD.bazel index ea898f8d89..f932afd625 100644 --- a/packages/d2b-bus/BUILD.bazel +++ b/packages/d2b-bus/BUILD.bazel @@ -28,7 +28,9 @@ d2b_bus_srcs = glob(["src/**/*.rs"], allow_empty = True) d2b_bus_deps = [ "//packages/d2b-audit:d2b_audit", + "//packages/d2b-core-controller:d2b_core_controller", "//packages/d2b-contracts:d2b_contracts", + "//packages/d2b-contracts-provider:d2b_contracts_provider", "//packages/d2b-resource-api:d2b_resource_api", "//packages/d2b-resource-store:d2b_resource_store", "//packages/d2b-session:d2b_session", @@ -66,9 +68,11 @@ rust_library( ], visibility = ["//visibility:public"], deps = [ + "//packages/d2b-contracts-provider:d2b_contracts_provider", "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-audit:d2b_audit_test_support", + "//packages/d2b-core-controller:d2b_core_controller_test_support", "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-resource-api:d2b_resource_api_test_support", "//packages/d2b-resource-store:d2b_resource_store_test_support", diff --git a/packages/d2b-bus/Cargo.toml b/packages/d2b-bus/Cargo.toml index 3544e15461..8ba3cd7e17 100644 --- a/packages/d2b-bus/Cargo.toml +++ b/packages/d2b-bus/Cargo.toml @@ -17,6 +17,7 @@ d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0- async-trait = "0.1" d2b-audit = { path = "../d2b-audit", version = "0.0.0-bootstrap" } d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } +d2b-core-controller = { path = "../d2b-core-controller", version = "0.0.0-bootstrap" } d2b-resource-api = { path = "../d2b-resource-api", version = "0.0.0-bootstrap" } d2b-resource-store = { path = "../d2b-resource-store", version = "0.0.0-bootstrap" } d2b-telemetry = { path = "../d2b-telemetry", version = "0.0.0-bootstrap" } @@ -30,11 +31,14 @@ tokio = { workspace = true, features = ["io-util", "rt", "sync", "time"] } [dev-dependencies] d2b-controller-toolkit = { path = "../d2b-controller-toolkit", version = "0.0.0-bootstrap" } +d2b-contracts-provider = { path = "../d2b-contracts-provider", version = "0.0.0-bootstrap" } d2b-resource-store-redb = { path = "../d2b-resource-store-redb", version = "0.0.0-bootstrap" } quote = "1" syn = { version = "2", features = ["full", "visit"] } tempfile = "3" tokio = { workspace = true, features = ["macros", "rt", "sync", "test-util"] } +protobuf = "3.7.2" +ttrpc = { workspace = true, features = ["async"] } [[test]] name = "production_watch_rss" diff --git a/packages/d2b-bus/src/authorization.rs b/packages/d2b-bus/src/authorization.rs index 8c1560aa3e..505707d20b 100644 --- a/packages/d2b-bus/src/authorization.rs +++ b/packages/d2b-bus/src/authorization.rs @@ -12,6 +12,9 @@ use d2b_resource_api::authz::{ AuthorizationDenial, AuthorizationPolicyError, AuthorizationState, NativeAuthorizer, PolicySet, SessionVerb, }; +use d2b_core_controller::controller_assignment::{ + AssignmentError, AssignmentVerb, ControllerAssignmentRegistry, +}; use d2b_session::{ OperationMember, SessionAuthorizationRequest, SessionError, SessionOperation, contract::{AuthorizationLease, SessionErrorCode}, @@ -30,6 +33,7 @@ struct AuthorizationRuntime { /// Single-owner native authorizer and trusted policy state for one bus. pub struct BusAuthorizer { runtime: Mutex, + assignments: Option>>, } impl BusAuthorizer { @@ -52,9 +56,20 @@ impl BusAuthorizer { } Ok(Self { runtime: Mutex::new(AuthorizationRuntime { native, state }), + assignments: None, }) } + /// Bind the Core-owned assignment registry used to fence controller + /// watches and mutations at the existing bus admission seam. + pub fn with_assignment_registry( + mut self, + assignments: std::sync::Arc>, + ) -> Self { + self.assignments = Some(assignments); + self + } + /// Borrow the single native authorizer shared with the Resource API. /// /// The bus and generated resource handlers must evaluate the same policy @@ -130,6 +145,7 @@ impl BusAuthorizer { } if let Some(call) = resource_call { + self.authorize_assignment(context, call)?; let request = call.authorization_request(route.zone().clone())?; runtime .native @@ -138,6 +154,46 @@ impl BusAuthorizer { Ok(()) } + fn authorize_assignment( + &self, + context: &AuthenticatedSubjectContext, + call: &ResourceCall, + ) -> Result<(), AuthorizationError> { + let Some(assignment) = call.assignment() else { + return Ok(()); + }; + if context.reconnect_generation() != assignment.session_generation() + || context.provider_generation() != Some(assignment.provider_generation()) + || context.controller_generation() != Some(assignment.controller_generation()) + { + return Err(AuthorizationError::Assignment( + AssignmentError::SessionBindingMismatch, + )); + } + if let Some(target) = assignment.target().execution_ref() + && context.execution_ref() != Some(target) + && context.subject_ref() != target + { + return Err(AuthorizationError::Assignment( + AssignmentError::TargetMismatch, + )); + } + let verb = match call { + ResourceCall::List(_) => AssignmentVerb::List, + ResourceCall::Watch(_) => AssignmentVerb::Watch, + ResourceCall::ScopedCommitBatch { .. } => AssignmentVerb::CommitBatch, + _ => return Err(AuthorizationError::Assignment(AssignmentError::VerbNotAllowed)), + }; + if let Some(registry) = &self.assignments { + registry + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .validate_scope(assignment, verb) + .map_err(AuthorizationError::Assignment)?; + } + Ok(()) + } + pub(crate) fn authorize_cancel( &self, context: &AuthenticatedSubjectContext, @@ -313,6 +369,7 @@ pub enum AuthorizationError { Native(AuthorizationDenial), Policy(AuthorizationPolicyError), InvalidResourceCall, + Assignment(AssignmentError), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -371,6 +428,7 @@ impl AuthorizationError { Self::ZoneMismatch | Self::SessionBindingMismatch | Self::DiagnosticBinding + | Self::Assignment(_) | Self::Native(AuthorizationDenial::ZoneMismatch) => { AuthorizationErrorClass::SessionBinding } @@ -466,16 +524,26 @@ impl From for AuthorizationError { #[cfg(test)] mod tests { + use d2b_contracts_provider::v3::{ + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, ProviderManifest, + ResourceApiBinding, RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, + UpgradeDisposition, UpgradePolicy, + }; use d2b_contracts_resource::v3::{ ConfigurationGeneration, ControllerGeneration, + ResourceEnvelope, ResourceGeneration, ResourceRef, ResourceTypeName, ResourceUid, SchemaFingerprint, + SchemaVersion, ZoneRevision, }; +use d2b_contracts_resource::v3::execution_policy::BoundedToken; use d2b_contracts_resource::v3::identity::{ BindingDigest, ReconnectGeneration, @@ -490,6 +558,10 @@ use d2b_contracts_resource::v3::identity::{ PolicyRule, RelayGrantAuthority, ResourceVerb, }; use d2b_resource_store::PolicySnapshot; + use d2b_core_controller::controller_assignment::{ + AssignmentIdentity, AssignmentRequest, ControllerAssignmentRegistry, ControllerRoleContract, + ScopedResourceMutation, ScopedResourceQuery, + }; use super::*; use crate::{ @@ -531,6 +603,16 @@ use d2b_contracts_resource::v3::identity::{ service: &str, locality: Locality, evidence: EvidenceClass, + ) -> AuthenticatedSubjectContext { + context_with_session(zone, service, locality, evidence, 1) + } + + fn context_with_session( + zone: &str, + service: &str, + locality: Locality, + evidence: EvidenceClass, + session_generation: u64, ) -> AuthenticatedSubjectContext { AuthenticatedSubjectContext::new( ResourceRef::parse(if locality == Locality::Local { @@ -550,7 +632,7 @@ use d2b_contracts_resource::v3::identity::{ locality, BindingDigest::parse(format!("sha256:{}", "2".repeat(64))).unwrap(), ), - ReconnectGeneration::new(1).unwrap(), + ReconnectGeneration::new(session_generation).unwrap(), TranscriptHash::from_bytes([3; 32]), ), ) @@ -560,6 +642,15 @@ use d2b_contracts_resource::v3::identity::{ } fn route(zone: &str, service: &str, member: RouteMember) -> RouteKey { + route_with_session(zone, service, member, 1) + } + + fn route_with_session( + zone: &str, + service: &str, + member: RouteMember, + session_generation: u64, + ) -> RouteKey { RouteKey::new( ZoneId::parse(zone).unwrap(), ServiceName::parse(service).unwrap(), @@ -569,7 +660,7 @@ use d2b_contracts_resource::v3::identity::{ RouteGenerations::new( Some(ResourceGeneration::new(2).unwrap()), Some(ControllerGeneration::new(3).unwrap()), - ReconnectGeneration::new(1).unwrap(), + ReconnectGeneration::new(session_generation).unwrap(), ), ) } @@ -654,6 +745,430 @@ use d2b_contracts_resource::v3::identity::{ BusAuthorizer::new(native, state(1)).unwrap() } + const ASSIGNMENT_DIGEST: &str = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + + fn assignment_digest() -> ArtifactDigest { + ArtifactDigest::parse(ASSIGNMENT_DIGEST).unwrap() + } + + fn assignment_fingerprint() -> SchemaFingerprint { + SchemaFingerprint::parse(ASSIGNMENT_DIGEST).unwrap() + } + + fn assignment_manifest() -> ProviderManifest { + let process = ResourceTypeName::parse("Process").unwrap(); + let component = ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [process.clone()], + [], + [d2b_contracts_resource::v3::ExecutionDomain::System], + 8, + assignment_digest(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + assignment_digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + assignment_digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + assignment_fingerprint(), + SchemaVersion::new(1, 0).unwrap(), + assignment_fingerprint(), + Default::default(), + None, + None, + d2b_contracts_resource::v3::PlacementAnchor::ExecutionRef, + ) + .unwrap(); + let trust = TrustEvidence { + publisher: BoundedToken::parse("trusted").unwrap(), + root_epoch: 1, + publisher_trusted: true, + signature: SignatureState::Valid, + revocation: RevocationState::Clear, + emergency_deny: false, + provenance: PolicyEvaluation::Accepted, + sbom: PolicyEvaluation::Accepted, + license: PolicyEvaluation::Accepted, + vulnerability: PolicyEvaluation::Accepted, + conformance: PolicyEvaluation::Accepted, + support_channel: BoundedToken::parse("stable").unwrap(), + }; + ProviderManifest::new( + d2b_contracts_resource::v3::ArtifactId::parse("provider-runtime").unwrap(), + ArtifactDigestSet { + package: assignment_digest(), + executable: assignment_digest(), + manifest: assignment_digest(), + config: assignment_digest(), + schema: assignment_digest(), + service: assignment_digest(), + }, + trust, + CompatibilityRange { + api_major: 3, + api_minor: 0, + descriptor_fingerprint: assignment_fingerprint(), + state_schema_version: SchemaVersion::new(1, 0).unwrap(), + }, + [component], + [binding], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + assignment_digest(), + assignment_digest(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + assignment_digest(), + assignment_digest(), + ) + .unwrap(), + ]) + .unwrap() + } + + fn assignment_role() -> ControllerRoleContract { + ControllerRoleContract::from_signed_manifest( + ResourceRef::parse("Provider/system-core").unwrap(), + ResourceRef::parse("Process/process-controller").unwrap(), + &assignment_manifest(), + ) + .unwrap() + } + + fn assignment_resource() -> ResourceEnvelope { + let value = serde_json::json!({ + "apiVersion": "resources.d2bus.org/v3", + "type": "Process", + "metadata": { + "name": "process-one", + "zone": "dev", + "uid": "123e4567-e89b-42d3-a456-426614174000", + "generation": 1, + "revision": 1, + "ownerRef": null, + "finalizers": [], + "deletionRequestedAt": null, + "createdAt": "2026-07-22T00:00:00.000Z", + "updatedAt": "2026-07-22T00:00:00.000Z", + "managedBy": "api", + "configurationGeneration": null, + "controllerGeneration": null, + "providerGeneration": null + }, + "spec": { + "providerRef": "Provider/system-core", + "executionRef": "Host/host-system", + "argv": ["true"] + }, + "status": { + "completedAt": null, + "conditions": [], + "lastReconciledAt": null, + "observedGeneration": 0, + "outcome": null, + "phase": "Pending", + "resource": {}, + "startedAt": null, + "update": { + "dependencies": {"count": 0, "refs": []}, + "disruption": "None", + "lastAssessedAt": null, + "observedGeneration": 0, + "operationId": null, + "owned": {"count": 0, "refs": []}, + "preserveState": true, + "reasons": [], + "state": "Unknown", + "targetGeneration": 1 + } + } + }); + ResourceEnvelope::from_json(&serde_json::to_vec(&value).unwrap()).unwrap() + } + + fn assignment_fixture() -> ( + AuthenticatedSubjectContext, + std::sync::Arc>, + AssignmentIdentity, + ScopedResourceQuery, + ScopedResourceMutation, + ScopedResourceMutation, + ) { + let resource = assignment_resource(); + let role = assignment_role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry + .admit(AssignmentRequest::new( + &resource, + &role, + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + true, + )) + .unwrap(); + let query = lease + .query( + vec![ResourceTypeName::parse("Process").unwrap()], + vec![resource.metadata().name().clone()], + Vec::new(), + ) + .unwrap(); + let mutation = lease + .mutation( + ResourceRef::parse("Process/process-one").unwrap(), + AssignmentVerb::UpdateStatus, + ) + .unwrap(); + let unsupported = lease + .mutation( + ResourceRef::parse("Process/process-one").unwrap(), + AssignmentVerb::Get, + ) + .unwrap(); + let identity = lease.identity().clone(); + let context = context_with_session( + "dev", + "d2b.resource.v3", + Locality::Local, + EvidenceClass::UnixPeer, + 4, + ) + .with_execution_ref(ResourceRef::parse("Host/host-system").unwrap()); + ( + context, + std::sync::Arc::new(Mutex::new(registry)), + identity, + query, + mutation, + unsupported, + ) + } + + fn assignment_authorizer( + context: &AuthenticatedSubjectContext, + registry: std::sync::Arc>, + resource_verbs: &[ResourceVerb], + ) -> BusAuthorizer { + let native = NativeAuthorizer::new( + ApiCatalog::standard(), + Some(policy( + 1, + context, + &[ + SessionVerb::Connect, + SessionVerb::Invoke, + SessionVerb::OpenStream, + ], + &[ResourceTypeName::parse("Process").unwrap()], + resource_verbs, + &[], + )), + ) + .unwrap(); + BusAuthorizer::new(native, state(1)) + .unwrap() + .with_assignment_registry(registry) + } + + #[test] + fn assignment_authorization_uses_registry_for_scoped_commit_and_watch() { + let (context, registry, identity, query, mutation, unsupported) = assignment_fixture(); + let watch_call = ResourceCall::Watch(ResourceQuery::from_scoped(query).unwrap()); + let commit_call = ResourceCall::ScopedCommitBatch { + assignment: identity.clone(), + mutations: vec![mutation], + }; + let authorizer = assignment_authorizer( + &context, + std::sync::Arc::clone(®istry), + &[ + ResourceVerb::Watch, + ResourceVerb::UpdateStatus, + ResourceVerb::Delete, + ], + ); + let watch_route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::stream("ResourceService/Watch").unwrap(), + 4, + ); + let commit_route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::method("ResourceService/CommitBatch").unwrap(), + 4, + ); + assert_eq!( + authorizer.authorize_dispatch(&context, &watch_route, Some(&watch_call), true), + Ok(()) + ); + assert_eq!( + authorizer.authorize_dispatch(&context, &commit_route, Some(&commit_call), false), + Ok(()) + ); + + let unsupported_call = ResourceCall::ScopedCommitBatch { + assignment: identity, + mutations: vec![unsupported], + }; + assert_eq!( + authorizer.authorize_dispatch( + &context, + &commit_route, + Some(&unsupported_call), + false, + ), + Err(AuthorizationError::InvalidResourceCall) + ); + } + + #[test] + fn assignment_authorization_rejects_session_and_target_mismatch() { + let (valid_context, registry, _identity, query, _, _) = assignment_fixture(); + let call = ResourceCall::Watch(ResourceQuery::from_scoped(query).unwrap()); + let route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::stream("ResourceService/Watch").unwrap(), + 1, + ); + let session_mismatch = context( + "dev", + "d2b.resource.v3", + Locality::Local, + EvidenceClass::UnixPeer, + ) + .with_execution_ref(ResourceRef::parse("Host/host-system").unwrap()); + let authorizer = assignment_authorizer( + &session_mismatch, + std::sync::Arc::clone(®istry), + &[ResourceVerb::Watch], + ); + assert_eq!( + authorizer.authorize_dispatch(&session_mismatch, &route, Some(&call), true), + Err(AuthorizationError::Assignment( + AssignmentError::SessionBindingMismatch + )) + ); + + let target_mismatch = valid_context + .clone() + .with_execution_ref(ResourceRef::parse("Host/other").unwrap()); + let target_authorizer = assignment_authorizer( + &target_mismatch, + registry, + &[ResourceVerb::Watch], + ); + let route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::stream("ResourceService/Watch").unwrap(), + 4, + ); + assert_eq!( + target_authorizer.authorize_dispatch(&target_mismatch, &route, Some(&call), true), + Err(AuthorizationError::Assignment( + AssignmentError::TargetMismatch + )) + ); + } + + #[test] + fn assignment_authorization_rejects_revoked_and_stale_registry_scope() { + let (context, registry, identity, _query, mutation, _) = assignment_fixture(); + let commit_call = ResourceCall::ScopedCommitBatch { + assignment: identity.clone(), + mutations: vec![mutation], + }; + let commit_route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::method("ResourceService/CommitBatch").unwrap(), + 4, + ); + registry + .lock() + .unwrap() + .revoke_session(ReconnectGeneration::new(4).unwrap()); + let revoked_authorizer = assignment_authorizer( + &context, + std::sync::Arc::clone(®istry), + &[ResourceVerb::UpdateStatus], + ); + assert_eq!( + revoked_authorizer.authorize_dispatch( + &context, + &commit_route, + Some(&commit_call), + false, + ), + Err(AuthorizationError::Assignment( + AssignmentError::SessionRevoked + )) + ); + + let (context, registry, identity, query, _, _) = assignment_fixture(); + let watch_call = ResourceCall::Watch(ResourceQuery::from_scoped(query).unwrap()); + registry + .lock() + .unwrap() + .begin_drain(&identity) + .unwrap(); + let stale_authorizer = + assignment_authorizer(&context, registry, &[ResourceVerb::Watch]); + let watch_route = route_with_session( + "dev", + "d2b.resource.v3", + RouteMember::stream("ResourceService/Watch").unwrap(), + 4, + ); + assert_eq!( + stale_authorizer.authorize_dispatch(&context, &watch_route, Some(&watch_call), true), + Err(AuthorizationError::Assignment( + AssignmentError::StaleAssignment + )) + ); + } + #[test] fn cross_zone_dispatch_fails_only_on_authenticated_zone_inequality() { let claims = context( diff --git a/packages/d2b-bus/src/production_rss.rs b/packages/d2b-bus/src/production_rss.rs index 2c337e12d6..2c1f06589b 100644 --- a/packages/d2b-bus/src/production_rss.rs +++ b/packages/d2b-bus/src/production_rss.rs @@ -8,28 +8,13 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; -use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ControllerGeneration, - ResourceGeneration, - ResourceRef, - ResourceTypeName, - ResourceUid, - SchemaFingerprint, - ZoneId, - ZoneRevision, -}; use d2b_contracts_resource::v3::identity::{ - AuthenticatedSubjectContext, - BindingDigest, - EvidenceClass, - Locality, - ReconnectGeneration, - ServiceName, - SessionBinding, - SessionPurpose, - TranscriptHash, - TransportBinding, + AuthenticatedSubjectContext, BindingDigest, EvidenceClass, Locality, ReconnectGeneration, + ServiceName, SessionBinding, SessionPurpose, TranscriptHash, TransportBinding, +}; +use d2b_contracts_resource::v3::{ + ConfigurationGeneration, ControllerGeneration, ResourceGeneration, ResourceRef, + ResourceTypeName, ResourceUid, SchemaFingerprint, ZoneId, ZoneRevision, }; use d2b_resource_api::authz::{ ApiCatalog, BindingScope, BootstrapPhase, BoundSubject, CompiledRole, CompiledRoleBinding, diff --git a/packages/d2b-bus/src/router.rs b/packages/d2b-bus/src/router.rs index 6238a47a3d..7572737fb9 100644 --- a/packages/d2b-bus/src/router.rs +++ b/packages/d2b-bus/src/router.rs @@ -25,6 +25,10 @@ use d2b_contracts_resource::v3::identity::{ ServiceName, SessionBinding, }; +use d2b_core_controller::controller_assignment::{ + AssignmentIdentity, AssignmentVerb, ScopedCommitTransport, ScopedResourceFilter, + ScopedResourceMutation, ScopedResourceQuery, +}; use d2b_resource_api::authz::{ ApiMethod, AuthorizationRequest, AuthorizationState, AuthorizationTarget, PolicySet, ResourceVerb, SessionVerb, @@ -217,6 +221,7 @@ pub struct ResourceQuery { resource_types: Vec, resource_names: Vec, filters: Vec, + assignment: Option, } impl ResourceQuery { @@ -237,6 +242,36 @@ impl ResourceQuery { resource_types, resource_names, filters, + assignment: None, + }) + } + + /// Consume a controller-minted query without allowing its assignment + /// filter to be dropped or widened. + pub fn from_scoped(query: ScopedResourceQuery) -> Result { + let (assignment, resource_types, resource_names, scoped_filters) = query.into_parts(); + if resource_types.is_empty() + || resource_types.len() > 64 + || resource_names.len() > 64 + || scoped_filters.len() > 64 + { + return Err(BusError::InvalidResourceCall); + } + let filters = scoped_filters + .into_iter() + .map(resource_filter_from_scoped) + .collect::, _>>()?; + if !filters + .iter() + .any(|filter| filter.field == "assignment.resourceUid") + { + return Err(BusError::InvalidResourceCall); + } + Ok(Self { + resource_types, + resource_names, + filters, + assignment: Some(assignment), }) } @@ -254,6 +289,11 @@ impl ResourceQuery { pub fn filters(&self) -> &[ResourceFilter] { &self.filters } + + /// Borrow the assignment evidence, when this query is controller-scoped. + pub const fn assignment(&self) -> Option<&AssignmentIdentity> { + self.assignment.as_ref() + } } impl core::fmt::Debug for ResourceQuery { @@ -279,6 +319,10 @@ pub enum ResourceCall { UpdateFinalizers(ResourceRef), Delete(ResourceRef), CommitBatch(Vec<(ResourceRef, ResourceVerb)>), + ScopedCommitBatch { + assignment: AssignmentIdentity, + mutations: Vec, + }, ResolveRef(ResourceRef), InspectSchema(ResourceTypeName), Upgrade(ResourceRef), @@ -360,6 +404,39 @@ impl ResourceCall { .collect(), ) } + Self::ScopedCommitBatch { + assignment, + mutations, + } => { + if mutations.is_empty() + || mutations.len() > 128 + || mutations.iter().any(|mutation| { + mutation.assignment() != assignment + || !mutation.verb().is_mutating() + || mutation.verb() == AssignmentVerb::CommitBatch + }) + { + return Err(BusError::InvalidResourceCall); + } + ( + ApiMethod::CommitBatch, + mutations + .iter() + .map(|mutation| { + let subresource = match mutation.verb() { + AssignmentVerb::UpdateStatus => Some("status"), + AssignmentVerb::UpdateFinalizers => Some("finalizers"), + _ => None, + }; + exact_target( + mutation.target(), + resource_verb_for_assignment(mutation.verb()), + subresource, + ) + }) + .collect(), + ) + } Self::ResolveRef(target) => ( ApiMethod::ResolveRef, vec![exact_target(target, ResourceVerb::Get, None)], @@ -402,12 +479,30 @@ impl ResourceCall { Self::UpdateFinalizers(_) => ApiMethod::UpdateFinalizers, Self::Delete(_) => ApiMethod::Delete, Self::CommitBatch(_) => ApiMethod::CommitBatch, + Self::ScopedCommitBatch { .. } => ApiMethod::CommitBatch, Self::ResolveRef(_) => ApiMethod::ResolveRef, Self::InspectSchema(_) => ApiMethod::InspectSchema, Self::Upgrade(_) => ApiMethod::Upgrade, } } + /// Borrow the assignment evidence attached to this resource call. + pub fn assignment(&self) -> Option<&AssignmentIdentity> { + match self { + Self::List(query) | Self::Watch(query) => query.assignment(), + Self::ScopedCommitBatch { assignment, .. } => Some(assignment), + _ => None, + } + } + + /// Borrow the controller mutations carried by a scoped commit call. + pub fn scoped_mutations(&self) -> Option<&[ScopedResourceMutation]> { + match self { + Self::ScopedCommitBatch { mutations, .. } => Some(mutations), + _ => None, + } + } + fn session_target(&self) -> Option<&ResourceRef> { match self { Self::Get(target) @@ -419,7 +514,11 @@ impl ResourceCall { | Self::Delete(target) | Self::ResolveRef(target) | Self::Upgrade(target) => Some(target), - Self::List(_) | Self::Watch(_) | Self::CommitBatch(_) | Self::InspectSchema(_) => None, + Self::List(_) + | Self::Watch(_) + | Self::CommitBatch(_) + | Self::ScopedCommitBatch { .. } + | Self::InspectSchema(_) => None, } } @@ -437,7 +536,11 @@ impl ResourceCall { | Self::Delete(target) | Self::ResolveRef(target) | Self::Upgrade(target) => target == route_target, - Self::List(_) | Self::Watch(_) | Self::CommitBatch(_) | Self::InspectSchema(_) => false, + Self::List(_) + | Self::Watch(_) + | Self::CommitBatch(_) + | Self::ScopedCommitBatch { .. } + | Self::InspectSchema(_) => false, } } } @@ -454,7 +557,7 @@ impl core::fmt::Debug for ResourceCall { Self::UpdateMetadata(_) => "UpdateMetadata", Self::UpdateFinalizers(_) => "UpdateFinalizers", Self::Delete(_) => "Delete", - Self::CommitBatch(_) => "CommitBatch", + Self::CommitBatch(_) | Self::ScopedCommitBatch { .. } => "CommitBatch", Self::ResolveRef(_) => "ResolveRef", Self::InspectSchema(_) => "InspectSchema", Self::Upgrade(_) => "Upgrade", @@ -488,7 +591,10 @@ fn query_targets(query: &ResourceQuery, verb: ResourceVerb) -> Vec Vec Vec Result { + if filter.field().is_empty() + || filter.field().len() > 64 + || !filter + .field() + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_')) + || filter.values().is_empty() + || filter.values().len() > 64 + || (filter.field() == "assignment.resourceUid" && !filter.assignment_bound()) + { + return Err(BusError::InvalidResourceCall); + } + Ok(ResourceFilter { + field: filter.field().to_owned(), + values: filter.values().to_vec(), + }) +} + +fn resource_verb_for_assignment(verb: AssignmentVerb) -> ResourceVerb { + match verb { + AssignmentVerb::Get => ResourceVerb::Get, + AssignmentVerb::List => ResourceVerb::List, + AssignmentVerb::Watch => ResourceVerb::Watch, + AssignmentVerb::Create => ResourceVerb::Create, + AssignmentVerb::UpdateSpec => ResourceVerb::UpdateSpec, + AssignmentVerb::UpdateStatus => ResourceVerb::UpdateStatus, + AssignmentVerb::UpdateMetadata => ResourceVerb::UpdateMetadata, + AssignmentVerb::UpdateFinalizers => ResourceVerb::UpdateFinalizers, + AssignmentVerb::Delete => ResourceVerb::Delete, + AssignmentVerb::CommitBatch => unreachable!("batch wrapper is not a mutation target"), + } +} + /// Method invocation delivered only after exact route and authorization checks. pub struct DeliveredInvocation { route: RouteKey, @@ -2279,6 +2422,23 @@ impl crate::registry::BusEndpoint for ComponentEndpoint { let mut outbound_frame = request.payload().to_vec(); rewrite_ttrpc_stream_id(&mut outbound_frame, internal_stream_id) .map_err(|_| EndpointError::Rejected)?; + if let Some(ResourceCall::ScopedCommitBatch { + assignment, + mutations, + }) = request.resource_call() + { + let transport = ScopedCommitTransport::new(assignment.clone(), mutations.clone()) + .map_err(|_| EndpointError::Rejected)?; + outbound_frame = + d2b_resource_api::attach_scoped_commit_frame(&outbound_frame, &transport) + .map_err(|_| EndpointError::Rejected)?; + } else if matches!( + request.resource_call(), + Some(ResourceCall::CommitBatch(_)) + ) { + d2b_resource_api::reject_scoped_commit_frame(&outbound_frame) + .map_err(|_| EndpointError::Rejected)?; + } let request_id = ttrpc_request_id(self.generation, &outbound_frame) .map_err(|_| EndpointError::Rejected)?; let target = request @@ -3031,6 +3191,28 @@ impl BusIngress { result } + /// Invoke one assignment-scoped atomic commit over the existing Resource + /// bus route. + pub async fn invoke_scoped_commit_batch( + &self, + route: RouteKey, + operation: OperationSpec, + assignment: AssignmentIdentity, + mutations: Vec, + payload: Vec, + ) -> Result { + self.invoke_resource( + route, + operation, + ResourceCall::ScopedCommitBatch { + assignment, + mutations, + }, + payload, + ) + .await + } + async fn invoke_inner( &self, route: RouteKey, @@ -3651,7 +3833,7 @@ use d2b_contracts_resource::v3::identity::{ SessionPurpose, TranscriptHash, TransportBinding, -}; + }; use d2b_controller_toolkit::{ OperationContext, PendingQueue, PriorityLane, QueueHint, ResourceKey, TriggerReason, TriggerSet, @@ -4481,6 +4663,7 @@ use d2b_contracts_resource::v3::identity::{ remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(digest), @@ -6157,9 +6340,7 @@ use d2b_contracts_resource::v3::identity::{ #[test] fn endpoint_session_failures_preserve_actionable_details_and_closed_labels() { use crate::registry::EndpointFailureClass; - use d2b_contracts_zone_session::v3::{ - component_session::{Remediation, SessionErrorCode}, -}; + use d2b_contracts_zone_session::v3::component_session::{Remediation, SessionErrorCode}; let cases = [ ( diff --git a/packages/d2b-bus/src/session_seam_tests.rs b/packages/d2b-bus/src/session_seam_tests.rs index 02f2b4354c..1eb0cd1639 100644 --- a/packages/d2b-bus/src/session_seam_tests.rs +++ b/packages/d2b-bus/src/session_seam_tests.rs @@ -8,40 +8,57 @@ use std::{ use async_trait::async_trait; use d2b_bus::{ - BusAuthorizer, BusConfig, BusError, ComponentSessionAdmission, OperationId, OperationSpec, - ResourceCall, RouteGenerations, RouteKey, RouteMember, RouteTarget, ZoneBus, ZoneRegistrar, + AuthorizationError, BusAuthorizer, BusConfig, BusError, ComponentSessionAdmission, OperationId, + EndpointError, OperationSpec, ResourceCall, RouteGenerations, RouteKey, RouteMember, + RouteTarget, ZoneBus, ZoneRegistrar, }; -use d2b_contracts_zone_session::v3::{ - component_session::{AttachmentPolicy, CloseReason, EndpointPolicy, EndpointPurpose, EndpointRole, IdentityEvidenceRequirement, LimitProfile, Locality, NoiseProfile, PurposeClass, Remediation, ServicePackage, TransportBinding, TransportClass}, +use d2b_contracts_provider::v3::{ + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, ProviderManifest, ResourceApiBinding, + RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, UpgradeDisposition, + UpgradePolicy, +}; +use d2b_contracts_resource::v3::identity::{ + BindingDigest, EvidenceClass, ReconnectGeneration, ServiceName, }; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ControllerGeneration, - ResourceGeneration, - ResourceRef, - ResourceUid, - SchemaFingerprint, - ZoneId, + ConfigurationGeneration, ControllerGeneration, ExecutionDomain, PlacementAnchor, + ResourceGeneration, ResourceRef, ResourceUid, SchemaFingerprint, SchemaVersion, ZoneId, ZoneRevision, }; -use d2b_contracts_resource::v3::identity::{ - BindingDigest, - EvidenceClass, - ServiceName, +use d2b_contracts_zone_session::v3::component_session::{ + AttachmentPolicy, CloseReason, EndpointPolicy, EndpointPurpose, EndpointRole, + IdentityEvidenceRequirement, LimitProfile, Locality, NoiseProfile, PurposeClass, Remediation, + ServicePackage, TransportBinding, TransportClass, +}; +use d2b_core_controller::controller_assignment::{ + AssignmentError, AssignmentRequest, AssignmentVerb, ControllerAssignmentRegistry, + ControllerRoleContract, ScopedCommitTransport, }; use d2b_resource_api::authz::{ ApiCatalog, AuthorizationState, BindingScope, BootstrapPhase, BoundSubject, CompiledRole, CompiledRoleBinding, NativeAuthorizer, PolicyRule, PolicySet, RelayGrantAuthority, ResourceVerb, SessionVerb, }; +use d2b_resource_api::{ResourceBusAdapter, ResourceService, ResourceStoreBackend}; use d2b_resource_store::PolicySnapshot; +use d2b_resource_store::mutation_seal::{MutationSealAcceptor, MutationSealBody}; +use d2b_resource_store::{ + ResourceMutationKind, StoreCommitResult, StoreError, StoreGetRequest, + StoreInspectSchemaRequest, StoreListRequest, StoreListResult, StoreResolveRequest, + StoreResolvedIdentity, StoreSealIdentity, StoreSlot, StoreWatchReceipt, StoreWatchRequest, + StoredResource, StoredSchema, +}; use d2b_session::{ AuthenticatedComponentSession, ComponentSessionDriver, HandshakeCredentials, OwnedTransport, SessionDriverHandle, SessionEngine, TransportDescriptor, TransportError, TransportEvidence, - TransportPacket, ttrpc_request_id, ttrpc_stream_id, + TransportPacket, serve_ttrpc_services, ttrpc_request_id, ttrpc_stream_id, }; use d2b_session_unix::{SeqpacketSocket, VerifiedUnixPeer, prearmed_seqpacket_pair}; +use protobuf::Message; use tokio::sync::{Notify, mpsc}; +use ttrpc::proto::{MessageHeader, Request as TtrpcRequest}; use crate::router::UnixSubjectRecord; @@ -211,7 +228,28 @@ async fn admit( SessionDriverHandle, tokio::task::JoinHandle<()>, ) { - admit_with_writer_pause(registrar, policy, subject, uid, provider, _allowed, None).await + admit_inner( + registrar, policy, subject, uid, provider, _allowed, None, true, + ) + .await +} + +async fn admit_without_echo( + registrar: &ZoneRegistrar, + policy: EndpointPolicy, + subject: &str, + uid: &str, + provider: &str, + allowed: impl IntoIterator, +) -> ( + AuthenticatedComponentSession, + SessionDriverHandle, + tokio::task::JoinHandle<()>, +) { + admit_inner( + registrar, policy, subject, uid, provider, allowed, None, false, + ) + .await } async fn admit_with_writer_pause( @@ -226,6 +264,33 @@ async fn admit_with_writer_pause( AuthenticatedComponentSession, SessionDriverHandle, tokio::task::JoinHandle<()>, +) { + admit_inner( + registrar, + policy, + subject, + uid, + provider, + _allowed, + writer_pause, + true, + ) + .await +} + +async fn admit_inner( + registrar: &ZoneRegistrar, + policy: EndpointPolicy, + subject: &str, + uid: &str, + provider: &str, + _allowed: impl IntoIterator, + writer_pause: Option>, + start_echo: bool, +) -> ( + AuthenticatedComponentSession, + SessionDriverHandle, + tokio::task::JoinHandle<()>, ) { let descriptor = TransportDescriptor { class: policy.transport_binding.transport, @@ -262,14 +327,18 @@ async fn admit_with_writer_pause( ), ); let remote = responder.unwrap().into_driver(); - let echo_remote = remote.clone(); - let echo = tokio::spawn(async move { - while let Ok(frame) = echo_remote.receive_ttrpc().await { - if echo_remote.send_ttrpc(frame).await.is_err() { - break; + let echo = if start_echo { + let echo_remote = remote.clone(); + tokio::spawn(async move { + while let Ok(frame) = echo_remote.receive_ttrpc().await { + if echo_remote.send_ttrpc(frame).await.is_err() { + break; + } } - } - }); + }) + } else { + tokio::spawn(async {}) + }; let subject_ref = ResourceRef::parse(subject).unwrap(); let subject_uid = ResourceUid::parse(uid).unwrap(); let provider_ref = ResourceRef::parse(provider).unwrap(); @@ -303,6 +372,8 @@ async fn admit_with_writer_pause( ) .unwrap() .with_provider(provider_ref, provider_generation) + .unwrap() + .with_execution_ref(ResourceRef::parse("Host/host-system").unwrap()) .unwrap(), } .with_controller_generation(ControllerGeneration::new(CONTROLLER_GENERATION).unwrap()); @@ -455,6 +526,606 @@ fn route(service: &str, member: &str, generation: u64, provider: &str) -> RouteK ) } +const ASSIGNMENT_DIGEST: &str = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + +fn assignment_digest() -> ArtifactDigest { + ArtifactDigest::parse(ASSIGNMENT_DIGEST).unwrap() +} + +fn assignment_fingerprint() -> SchemaFingerprint { + SchemaFingerprint::parse(ASSIGNMENT_DIGEST).unwrap() +} + +fn assignment_manifest() -> ProviderManifest { + let process = d2b_contracts_resource::v3::ResourceTypeName::parse("Process").unwrap(); + let component = ComponentDescriptor::new( + d2b_contracts_resource::v3::execution_policy::BoundedToken::parse("process-controller") + .unwrap(), + ComponentType::Controller, + [process.clone()], + [], + [ExecutionDomain::System], + 1, + assignment_digest(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + assignment_digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + assignment_digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + assignment_fingerprint(), + SchemaVersion::new(1, 0).unwrap(), + assignment_fingerprint(), + Default::default(), + None, + None, + PlacementAnchor::ExecutionRef, + ) + .unwrap(); + let trust = TrustEvidence { + publisher: d2b_contracts_resource::v3::execution_policy::BoundedToken::parse("trusted") + .unwrap(), + root_epoch: 1, + publisher_trusted: true, + signature: SignatureState::Valid, + revocation: RevocationState::Clear, + emergency_deny: false, + provenance: PolicyEvaluation::Accepted, + sbom: PolicyEvaluation::Accepted, + license: PolicyEvaluation::Accepted, + vulnerability: PolicyEvaluation::Accepted, + conformance: PolicyEvaluation::Accepted, + support_channel: d2b_contracts_resource::v3::execution_policy::BoundedToken::parse( + "stable", + ) + .unwrap(), + }; + ProviderManifest::new( + d2b_contracts_resource::v3::ArtifactId::parse("provider-system-core").unwrap(), + ArtifactDigestSet { + package: assignment_digest(), + executable: assignment_digest(), + manifest: assignment_digest(), + config: assignment_digest(), + schema: assignment_digest(), + service: assignment_digest(), + }, + trust, + CompatibilityRange { + api_major: 3, + api_minor: 0, + descriptor_fingerprint: assignment_fingerprint(), + state_schema_version: SchemaVersion::new(1, 0).unwrap(), + }, + [component], + [binding], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new( + ControllerTargetKind::Host, + assignment_digest(), + assignment_digest(), + ) + .unwrap(), + TargetRuntimeArtifacts::new( + ControllerTargetKind::Guest, + assignment_digest(), + assignment_digest(), + ) + .unwrap(), + ]) + .unwrap() +} + +fn assignment_resource() -> d2b_contracts_resource::v3::ResourceEnvelope { + d2b_contracts_resource::v3::ResourceEnvelope::from_json( + br#"{"apiVersion":"resources.d2bus.org/v3","type":"Process","metadata":{"configurationGeneration":null,"createdAt":"2026-07-22T00:00:00.000Z","deletionRequestedAt":null,"finalizers":[],"generation":1,"managedBy":"api","name":"work","ownerRef":null,"revision":7,"uid":"123e4567-e89b-42d3-a456-426614174000","updatedAt":"2026-07-22T00:00:00.000Z","zone":"dev"},"spec":{"executionRef":"Host/host-system","providerRef":"Provider/system-core","argv":["true"]},"status":{"completedAt":null,"conditions":[],"lastReconciledAt":null,"observedGeneration":0,"outcome":null,"phase":"Pending","resource":{},"startedAt":null,"update":{"dependencies":{"count":0,"refs":[]},"disruption":"None","lastAssessedAt":null,"observedGeneration":0,"operationId":null,"owned":{"count":0,"refs":[]},"preserveState":true,"reasons":[],"state":"Unknown","targetGeneration":1}}}"#, + + ) + .unwrap() +} + +fn scoped_bus() -> ( + ZoneBus, + ZoneRegistrar, + std::sync::Arc>, + std::sync::Arc, + AuthorizationState, +) { + let catalog = ApiCatalog::standard(); + let zone = ZoneId::parse("dev").unwrap(); + let rule = PolicyRule::new( + &catalog, + [d2b_contracts_resource::v3::ResourceTypeName::parse("Process").unwrap()], + [ResourceVerb::UpdateStatus, ResourceVerb::UpdateFinalizers], + [ + SessionVerb::Connect, + SessionVerb::Invoke, + SessionVerb::Cancel, + ], + [], + [], + [zone.clone()], + [], + ) + .unwrap(); + let role = CompiledRole::new( + ResourceRef::parse("Role/scoped-commit").unwrap(), + vec![rule], + ) + .unwrap(); + let subjects = [ + ( + "Provider/system-core", + "11111111-1111-4111-8111-111111111111", + ), + ("Host/alice", "22222222-2222-4222-8222-222222222222"), + ] + .into_iter() + .map(|(subject, uid)| BoundSubject { + subject_ref: ResourceRef::parse(subject).unwrap(), + subject_uid: ResourceUid::parse(uid).unwrap(), + }); + let binding = CompiledRoleBinding::new( + role.role_ref.clone(), + subjects, + BindingScope::default(), + RelayGrantAuthority::None, + ) + .unwrap(); + let policy_set = PolicySet::new(&catalog, 1, vec![role], vec![binding]).unwrap(); + let native = std::sync::Arc::new(NativeAuthorizer::new(catalog, Some(policy_set)).unwrap()); + let state = AuthorizationState { + snapshot: PolicySnapshot { + policy_revision: 1, + api_catalog_revision: 1, + active_configuration_revision: ConfigurationGeneration::new(1).unwrap(), + controller_generation: Some(ControllerGeneration::new(CONTROLLER_GENERATION).unwrap()), + }, + zone_policy_revision: ZoneRevision::new(1), + bootstrap_phase: BootstrapPhase::Disabled, + now_tick: 1, + }; + let assignments = std::sync::Arc::new(std::sync::Mutex::new( + ControllerAssignmentRegistry::default(), + )); + let authorizer = BusAuthorizer::from_shared(std::sync::Arc::clone(&native), state.clone()) + .unwrap() + .with_assignment_registry(std::sync::Arc::clone(&assignments)); + let (bus, registrar) = ZoneBus::new(zone, authorizer, BusConfig::default()).unwrap(); + (bus, registrar, assignments, native, state) +} + +struct ScopedStore { + acceptor: MutationSealAcceptor, + commits: std::sync::Arc>>>, +} + +impl ResourceStoreBackend for ScopedStore { + async fn get(&self, _: StoreGetRequest) -> Result { + unreachable!("scoped commit proof does not read through the backend") + } + + async fn list(&self, _: StoreListRequest) -> Result { + unreachable!("scoped commit proof does not list through the backend") + } + + async fn watch(&self, _: StoreWatchRequest) -> Result { + unreachable!("scoped commit proof does not watch through the backend") + } + + async fn resolve_ref( + &self, + _: StoreResolveRequest, + ) -> Result { + unreachable!("scoped commit proof does not resolve refs through the backend") + } + + async fn inspect_schema( + &self, + _: StoreInspectSchemaRequest, + ) -> Result { + unreachable!("scoped commit proof does not inspect schemas through the backend") + } + + async fn commit_verified( + &self, + mutation: d2b_resource_store::SealedMutation, + ) -> Result { + let body: MutationSealBody = self.acceptor.open(mutation).unwrap().into_body(); + let observed = body + .mutations + .iter() + .map(|mutation| { + ( + mutation.mutation().kind, + mutation.mutation().assignment.is_some(), + ) + }) + .collect::>(); + self.commits.lock().unwrap().push(observed); + Ok(StoreCommitResult { + resources: Vec::new(), + revision: ZoneRevision::new(8), + }) + } +} + +fn commit_batch_frame(operation_id: &str) -> Vec { + let envelope = assignment_resource(); + let identity = d2b_contracts_resource::resource_proto::ResourceIdentity { + zone: "dev".to_owned(), + resource_type: "Process".to_owned(), + name: "work".to_owned(), + uid: Some(envelope.metadata().uid().as_str().to_owned()), + generation: Some(envelope.metadata().generation().get()), + revision: Some(envelope.metadata().revision().get()), + special_fields: protobuf::SpecialFields::new(), + }; + let mut precondition = d2b_contracts_resource::resource_proto::Precondition::new(); + precondition.kind = protobuf::EnumOrUnknown::new( + d2b_contracts_resource::resource_proto::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION, + ); + precondition.expected_revision = Some(envelope.metadata().revision().get()); + precondition.expected_uid = Some(envelope.metadata().uid().as_str().to_owned()); + let mut status = d2b_contracts_resource::resource_proto::Mutation::new(); + status.kind = protobuf::EnumOrUnknown::new( + d2b_contracts_resource::resource_proto::MutationKind::MUTATION_KIND_UPDATE_STATUS, + ); + status.target = protobuf::MessageField::some(identity.clone()); + status.precondition = protobuf::MessageField::some(precondition.clone()); + let mut status_body = d2b_contracts_resource::resource_proto::ResourceEnvelopeBytes::new(); + status_body.identity = protobuf::MessageField::some(identity.clone()); + status_body.canonical_json = envelope.canonical_bytes().unwrap(); + status_body.payload_digest = envelope.digest().unwrap(); + status.resource = protobuf::MessageField::some(status_body); + + let mut finalizers = d2b_contracts_resource::resource_proto::Mutation::new(); + finalizers.kind = protobuf::EnumOrUnknown::new( + d2b_contracts_resource::resource_proto::MutationKind::MUTATION_KIND_UPDATE_FINALIZERS, + ); + finalizers.target = protobuf::MessageField::some(identity); + finalizers.precondition = protobuf::MessageField::some(precondition); + finalizers + .add_finalizers + .push("process.d2bus.org/reconcile".to_owned()); + + let mut meta = d2b_contracts_resource::resource_proto::RequestMeta::new(); + meta.operation_id = operation_id.to_owned(); + meta.idempotency_key = format!("{operation_id}-key"); + meta.correlation_id = format!("{operation_id}-correlation"); + meta.deadline_ms = 10_000; + let request = d2b_contracts_resource::resource_proto::CommitBatchRequest { + meta: protobuf::MessageField::some(meta), + mutations: vec![status, finalizers], + scoped_admission: Vec::new(), + special_fields: protobuf::SpecialFields::new(), + }; + let rpc = TtrpcRequest { + service: "d2b.resource.v3.ResourceService".to_owned(), + method: "CommitBatch".to_owned(), + payload: request.write_to_bytes().unwrap(), + ..TtrpcRequest::default() + }; + let body = rpc.write_to_bytes().unwrap(); + let header = MessageHeader::new_request(1, body.len() as u32); + let mut frame = Vec::with_capacity(ttrpc::proto::MESSAGE_HEADER_LENGTH + body.len()); + frame.extend_from_slice(&Vec::from(header)); + frame.extend_from_slice(&body); + frame +} + +#[tokio::test] +async fn production_scoped_commit_chain_authorizes_and_fences_store_writes() { + let (_bus, mut registrar, assignments, native, state) = scoped_bus(); + let resource = assignment_resource(); + let role = ControllerRoleContract::from_signed_manifest( + ResourceRef::parse("Provider/system-core").unwrap(), + ResourceRef::parse("Process/process-controller").unwrap(), + &assignment_manifest(), + ) + .unwrap(); + let old_lease = assignments + .lock() + .unwrap() + .admit(AssignmentRequest::new( + &resource, + &role, + ResourceGeneration::new(PROVIDER_GENERATION).unwrap(), + ControllerGeneration::new(CONTROLLER_GENERATION).unwrap(), + ReconnectGeneration::new(1).unwrap(), + true, + )) + .unwrap(); + let old_identity = old_lease.identity().clone(); + let target = ResourceRef::parse("Process/work").unwrap(); + let old_mutations = vec![ + old_lease + .mutation(target.clone(), AssignmentVerb::UpdateStatus) + .unwrap(), + old_lease + .mutation(target.clone(), AssignmentVerb::UpdateFinalizers) + .unwrap(), + ]; + + let (resource_endpoint, resource_remote, resource_echo) = admit_without_echo( + ®istrar, + policy( + ServicePackage::ResourceV3, + EndpointPurpose::ResourceService, + 1, + ), + "Provider/system-core", + "11111111-1111-4111-8111-111111111111", + "Provider/system-core", + [SessionVerb::Invoke], + ) + .await; + let store_identity = StoreSealIdentity::new( + StoreSlot::new(0).unwrap(), + ZoneId::parse("dev").unwrap(), + ResourceUid::parse("99999999-9999-4999-8999-999999999999").unwrap(), + ); + let acceptor = native.take_store_seal(store_identity).unwrap(); + let commits = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let store = std::sync::Arc::new(ScopedStore { + acceptor, + commits: std::sync::Arc::clone(&commits), + }); + let service = std::sync::Arc::new( + ResourceService::new( + std::sync::Arc::clone(&store), + std::sync::Arc::clone(&native), + ) + .unwrap(), + ); + let endpoint_subject = native + .issue_authenticated_subject( + resource_endpoint.route_binding().context().clone(), + state.clone(), + ) + .unwrap(); + let adapter = std::sync::Arc::new( + ResourceBusAdapter::bind_component_session(service, endpoint_subject).unwrap(), + ); + let service_task = tokio::spawn(serve_ttrpc_services( + std::sync::Arc::new(resource_remote), + adapter.ttrpc_services(), + )); + for _ in 0..16 { + tokio::task::yield_now().await; + } + let _resource_ingress = registrar + .register_component_session(resource_endpoint) + .await + .unwrap(); + + let (caller, _caller_remote, caller_echo) = admit( + ®istrar, + policy( + ServicePackage::ResourceV3, + EndpointPurpose::ResourceService, + 1, + ), + "Host/alice", + "22222222-2222-4222-8222-222222222222", + "Provider/system-core", + [SessionVerb::Invoke], + ) + .await; + let caller = registrar.register_component_session(caller).await.unwrap(); + let route = route( + "d2b.resource.v3", + "ResourceService/CommitBatch", + 1, + "Provider/system-core", + ); + + assignments + .lock() + .unwrap() + .begin_drain(&old_identity) + .unwrap(); + let stale = caller + .invoke_scoped_commit_batch( + route.clone(), + OperationSpec::new(OperationId::parse("scoped-stale").unwrap(), 10_000).unwrap(), + old_identity.clone(), + old_mutations.clone(), + commit_batch_frame("scoped-stale"), + ) + .await + .unwrap_err(); + assert!(matches!( + stale, + BusError::Authorization(AuthorizationError::Assignment( + AssignmentError::StaleAssignment + )) + )); + + assignments.lock().unwrap().release(&old_identity).unwrap(); + let new_lease = assignments + .lock() + .unwrap() + .admit(AssignmentRequest::new( + &resource, + &role, + ResourceGeneration::new(PROVIDER_GENERATION).unwrap(), + ControllerGeneration::new(CONTROLLER_GENERATION).unwrap(), + ReconnectGeneration::new(1).unwrap(), + true, + )) + .unwrap(); + let new_identity = new_lease.identity().clone(); + let new_mutations = vec![ + new_lease + .mutation(target.clone(), AssignmentVerb::UpdateStatus) + .unwrap(), + new_lease + .mutation(target.clone(), AssignmentVerb::UpdateFinalizers) + .unwrap(), + ]; + let revoked_mutations = new_mutations.clone(); + let mut scoped_response = None; + for attempt in 0..32 { + let operation_id = format!("scoped-valid-{attempt}"); + let response = caller + .invoke_scoped_commit_batch( + route.clone(), + OperationSpec::new(OperationId::parse(&operation_id).unwrap(), 10_000).unwrap(), + new_identity.clone(), + new_mutations.clone(), + commit_batch_frame(&operation_id), + ) + .await + .unwrap(); + let response = ttrpc::proto::Response::parse_from_bytes( + &response.as_bytes()[ttrpc::proto::MESSAGE_HEADER_LENGTH..], + ) + .unwrap(); + let response = + d2b_contracts_resource::resource_proto::CommitBatchResponse::parse_from_bytes( + &response.payload, + ) + .unwrap(); + if response.error.is_some() { + scoped_response = Some(response); + break; + } + if response.revision == 8 { + scoped_response = Some(response); + break; + } + tokio::task::yield_now().await; + } + let response = scoped_response.expect("scoped commit response"); + assert!(response.error.is_none()); + assert_eq!(response.revision, 8); + + let forged_scope = + ScopedCommitTransport::new(new_identity.clone(), new_mutations.clone()).unwrap(); + let forged_frame = d2b_resource_api::attach_scoped_commit_frame( + &commit_batch_frame("plain-forged"), + &forged_scope, + ) + .unwrap(); + let forged = caller + .invoke_resource( + route.clone(), + OperationSpec::new(OperationId::parse("plain-forged").unwrap(), 10_000).unwrap(), + ResourceCall::CommitBatch(vec![ + (target.clone(), ResourceVerb::UpdateStatus), + (target.clone(), ResourceVerb::UpdateFinalizers), + ]), + forged_frame, + ) + .await + .unwrap_err(); + assert!(matches!(forged, BusError::Endpoint(EndpointError::Rejected))); + + for attempt in 0..8 { + if commits.lock().unwrap().len() >= 2 { + break; + } + let operation_id = format!("plain-commit-{attempt}"); + caller + .invoke_resource( + route.clone(), + OperationSpec::new(OperationId::parse(&operation_id).unwrap(), 10_000).unwrap(), + ResourceCall::CommitBatch(vec![ + (target.clone(), ResourceVerb::UpdateStatus), + (target.clone(), ResourceVerb::UpdateFinalizers), + ]), + commit_batch_frame(&operation_id), + ) + .await + .unwrap(); + for _ in 0..8 { + if commits.lock().unwrap().len() >= 2 { + break; + } + tokio::task::yield_now().await; + } + } + + assignments + .lock() + .unwrap() + .revoke_session(ReconnectGeneration::new(1).unwrap()); + let revoked = caller + .invoke_scoped_commit_batch( + route, + OperationSpec::new(OperationId::parse("scoped-revoked").unwrap(), 10_000).unwrap(), + new_identity, + revoked_mutations, + commit_batch_frame("scoped-revoked"), + ) + .await + .unwrap_err(); + assert!(matches!( + revoked, + BusError::Authorization(AuthorizationError::Assignment( + AssignmentError::SessionRevoked + )) + )); + + let commits = commits.lock().unwrap().clone(); + assert_eq!(commits.len(), 2); + assert_eq!( + commits[0], + vec![ + (ResourceMutationKind::UpdateStatus, true), + (ResourceMutationKind::UpdateFinalizers, true), + ] + ); + assert_eq!( + commits[1], + vec![ + (ResourceMutationKind::UpdateStatus, false), + (ResourceMutationKind::UpdateFinalizers, false), + ] + ); + assert!(commits[0].iter().all(|(_, fenced)| *fenced)); + assert!(commits[1].iter().all(|(_, fenced)| !*fenced)); + + service_task.abort(); + let _ = service_task.await; + resource_echo.abort(); + caller_echo.abort(); +} + fn ttrpc_frame(stream_id: u32, payload: &[u8]) -> Vec { let mut frame = Vec::with_capacity(10 + payload.len()); frame.extend_from_slice(&(payload.len() as u32).to_be_bytes()); diff --git a/packages/d2b-bus/tests/production_watch_rss.rs b/packages/d2b-bus/tests/production_watch_rss.rs index d7b897c79f..cefbb78972 100644 --- a/packages/d2b-bus/tests/production_watch_rss.rs +++ b/packages/d2b-bus/tests/production_watch_rss.rs @@ -218,6 +218,7 @@ fn mutation( remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(digest), diff --git a/packages/d2b-contracts-resource/proto/d2b-resource-v3.proto b/packages/d2b-contracts-resource/proto/d2b-resource-v3.proto index e5427a80dc..446b4289ad 100644 --- a/packages/d2b-contracts-resource/proto/d2b-resource-v3.proto +++ b/packages/d2b-contracts-resource/proto/d2b-resource-v3.proto @@ -213,6 +213,9 @@ message DeleteResponse { message CommitBatchRequest { RequestMeta meta = 1; repeated Mutation mutations = 2; + // Bus-admitted assignment evidence attached to the existing CommitBatch + // transport. This field is populated only after ResourceCall admission. + bytes scoped_admission = 3; } message CommitBatchResponse { diff --git a/packages/d2b-contracts-resource/src/generated/d2b_resource_v3.rs b/packages/d2b-contracts-resource/src/generated/d2b_resource_v3.rs index 5713ae0669..9b1cbe3abf 100644 --- a/packages/d2b-contracts-resource/src/generated/d2b_resource_v3.rs +++ b/packages/d2b-contracts-resource/src/generated/d2b_resource_v3.rs @@ -5166,6 +5166,8 @@ pub struct CommitBatchRequest { pub meta: ::protobuf::MessageField, // @@protoc_insertion_point(field:d2b.resource.v3.CommitBatchRequest.mutations) pub mutations: ::std::vec::Vec, + // @@protoc_insertion_point(field:d2b.resource.v3.CommitBatchRequest.scoped_admission) + pub scoped_admission: ::std::vec::Vec, // special fields // @@protoc_insertion_point(special_field:d2b.resource.v3.CommitBatchRequest.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -5183,7 +5185,7 @@ impl CommitBatchRequest { } fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(2); + let mut fields = ::std::vec::Vec::with_capacity(3); let mut oneofs = ::std::vec::Vec::with_capacity(0); fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, RequestMeta>( "meta", @@ -5195,6 +5197,11 @@ impl CommitBatchRequest { |m: &CommitBatchRequest| { &m.mutations }, |m: &mut CommitBatchRequest| { &mut m.mutations }, )); + fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>( + "scoped_admission", + |m: &CommitBatchRequest| { &m.scoped_admission }, + |m: &mut CommitBatchRequest| { &mut m.scoped_admission }, + )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "CommitBatchRequest", fields, @@ -5219,6 +5226,9 @@ impl ::protobuf::Message for CommitBatchRequest { 18 => { self.mutations.push(is.read_message()?); }, + 26 => { + self.scoped_admission = is.read_bytes()?; + }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; }, @@ -5239,6 +5249,9 @@ impl ::protobuf::Message for CommitBatchRequest { let len = value.compute_size(); my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; }; + if !self.scoped_admission.is_empty() { + my_size += ::protobuf::rt::bytes_size(3, &self.scoped_admission); + } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); my_size @@ -5251,6 +5264,9 @@ impl ::protobuf::Message for CommitBatchRequest { for v in &self.mutations { ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; }; + if !self.scoped_admission.is_empty() { + os.write_bytes(3, &self.scoped_admission)?; + } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) } @@ -5270,6 +5286,7 @@ impl ::protobuf::Message for CommitBatchRequest { fn clear(&mut self) { self.meta.clear(); self.mutations.clear(); + self.scoped_admission.clear(); self.special_fields.clear(); } @@ -5277,6 +5294,7 @@ impl ::protobuf::Message for CommitBatchRequest { static instance: CommitBatchRequest = CommitBatchRequest { meta: ::protobuf::MessageField::none(), mutations: ::std::vec::Vec::new(), + scoped_admission: ::std::vec::Vec::new(), special_fields: ::protobuf::SpecialFields::new(), }; &instance @@ -7406,21 +7424,22 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \x08resource\x12\x1a\n\x08revision\x18\x02\x20\x01(\x04R\x08revision\x12\ 4\n\x05error\x18\x03\x20\x01(\x0b2\x1e.d2b.resource.v3.ResourceErrorR\ \x05error\x12G\n\x0bdisposition\x18\x04\x20\x01(\x0e2%.d2b.resource.v3.R\ - econcileDispositionR\x0bdisposition\"\x7f\n\x12CommitBatchRequest\x120\n\ - \x04meta\x18\x01\x20\x01(\x0b2\x1c.d2b.resource.v3.RequestMetaR\x04meta\ - \x127\n\tmutations\x18\x02\x20\x03(\x0b2\x19.d2b.resource.v3.MutationR\t\ - mutations\"\x8c\x02\n\x13CommitBatchResponse\x12D\n\tresources\x18\x01\ - \x20\x03(\x0b2&.d2b.resource.v3.ResourceEnvelopeBytesR\tresources\x12\ - \x1a\n\x08revision\x18\x02\x20\x01(\x04R\x08revision\x124\n\x05error\x18\ - \x03\x20\x01(\x0b2\x1e.d2b.resource.v3.ResourceErrorR\x05error\x12?\n\ - \x19conflict_mutation_ordinal\x18\x04\x20\x01(\rH\0R\x17conflictMutation\ - Ordinal\x88\x01\x01B\x1c\n\x1a_conflict_mutation_ordinal\"\x80\x01\n\x11\ - ResolveRefRequest\x120\n\x04meta\x18\x01\x20\x01(\x0b2\x1c.d2b.resource.\ - v3.RequestMetaR\x04meta\x129\n\x06target\x18\x02\x20\x01(\x0b2!.d2b.reso\ - urce.v3.ResourceIdentityR\x06target\"\x89\x01\n\x12ResolveRefResponse\ - \x12=\n\x08resource\x18\x01\x20\x01(\x0b2!.d2b.resource.v3.ResourceIdent\ - ityR\x08resource\x124\n\x05error\x18\x02\x20\x01(\x0b2\x1e.d2b.resource.\ - v3.ResourceErrorR\x05error\"m\n\x14InspectSchemaRequest\x120\n\x04meta\ + econcileDispositionR\x0bdisposition\"\xaa\x01\n\x12CommitBatchRequest\ + \x120\n\x04meta\x18\x01\x20\x01(\x0b2\x1c.d2b.resource.v3.RequestMetaR\ + \x04meta\x127\n\tmutations\x18\x02\x20\x03(\x0b2\x19.d2b.resource.v3.Mut\ + ationR\tmutations\x12)\n\x10scoped_admission\x18\x03\x20\x01(\x0cR\x0fsc\ + opedAdmission\"\x8c\x02\n\x13CommitBatchResponse\x12D\n\tresources\x18\ + \x01\x20\x03(\x0b2&.d2b.resource.v3.ResourceEnvelopeBytesR\tresources\ + \x12\x1a\n\x08revision\x18\x02\x20\x01(\x04R\x08revision\x124\n\x05error\ + \x18\x03\x20\x01(\x0b2\x1e.d2b.resource.v3.ResourceErrorR\x05error\x12?\ + \n\x19conflict_mutation_ordinal\x18\x04\x20\x01(\rH\0R\x17conflictMutati\ + onOrdinal\x88\x01\x01B\x1c\n\x1a_conflict_mutation_ordinal\"\x80\x01\n\ + \x11ResolveRefRequest\x120\n\x04meta\x18\x01\x20\x01(\x0b2\x1c.d2b.resou\ + rce.v3.RequestMetaR\x04meta\x129\n\x06target\x18\x02\x20\x01(\x0b2!.d2b.\ + resource.v3.ResourceIdentityR\x06target\"\x89\x01\n\x12ResolveRefRespons\ + e\x12=\n\x08resource\x18\x01\x20\x01(\x0b2!.d2b.resource.v3.ResourceIden\ + tityR\x08resource\x124\n\x05error\x18\x02\x20\x01(\x0b2\x1e.d2b.resource\ + .v3.ResourceErrorR\x05error\"m\n\x14InspectSchemaRequest\x120\n\x04meta\ \x18\x01\x20\x01(\x0b2\x1c.d2b.resource.v3.RequestMetaR\x04meta\x12#\n\r\ resource_type\x18\x02\x20\x01(\tR\x0cresourceType\"\x8d\x01\n\x15Inspect\ SchemaResponse\x12>\n\x06schema\x18\x01\x20\x01(\x0b2&.d2b.resource.v3.R\ diff --git a/packages/d2b-controller-toolkit/benches/support/reaction.rs b/packages/d2b-controller-toolkit/benches/support/reaction.rs index bb1eb9b3a5..f86e0dbbf1 100644 --- a/packages/d2b-controller-toolkit/benches/support/reaction.rs +++ b/packages/d2b-controller-toolkit/benches/support/reaction.rs @@ -119,6 +119,7 @@ impl ProductionStore { remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(payload_digest), @@ -160,6 +161,7 @@ impl ProductionStore { remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, Some(resource.uid.clone()), Some(digest), diff --git a/packages/d2b-controller-toolkit/tests/support/mod.rs b/packages/d2b-controller-toolkit/tests/support/mod.rs index 6f1bd4397c..a2da3c2d0e 100644 --- a/packages/d2b-controller-toolkit/tests/support/mod.rs +++ b/packages/d2b-controller-toolkit/tests/support/mod.rs @@ -129,6 +129,7 @@ pub async fn commit_host( remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(digest), diff --git a/packages/d2b-core-controller/src/api_catalog.rs b/packages/d2b-core-controller/src/api_catalog.rs index 2645ed002f..b843d3ed16 100644 --- a/packages/d2b-core-controller/src/api_catalog.rs +++ b/packages/d2b-core-controller/src/api_catalog.rs @@ -3,22 +3,11 @@ use std::collections::{BTreeMap, BTreeSet}; use d2b_contracts_provider::v3::{ - ArtifactDigest, - ProviderManifest, - ComponentTargetCapability, - ControllerInstanceScope, - ControllerTargetKind, - EffectPortClass, - ResourceApiBinding, - TargetRuntimeArtifacts, - BinaryRef, - ComponentExecution, -}; -use d2b_contracts_resource::v3::{ - ResourceRef, - ResourceTypeName, - SchemaFingerprint, + ArtifactDigest, BinaryRef, ComponentExecution, ComponentTargetCapability, + ControllerInstanceScope, ControllerTargetKind, EffectPortClass, ProviderManifest, + ResourceApiBinding, TargetRuntimeArtifacts, }; +use d2b_contracts_resource::v3::{ResourceRef, ResourceTypeName, SchemaFingerprint}; /// Closed reason a catalog candidate was refused. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -260,28 +249,17 @@ impl ApiCatalogHandler { #[cfg(test)] mod tests { + use d2b_contracts_provider::v3::UpgradePolicy as ProviderUpgradePolicy; use d2b_contracts_provider::v3::{ - UpgradePolicy as ProviderUpgradePolicy, -}; - use d2b_contracts_provider::v3::{ - ArtifactDigest, - ArtifactDigestSet, - CompatibilityRange, - ComponentDescriptor, - ComponentType, - PolicyEvaluation, - RevocationState, - SignatureState, - StandardCapabilityMatrix, - TrustEvidence, - UpgradeDisposition, -}; -use d2b_contracts_resource::v3::{ - ArtifactId, - SchemaVersion, - execution_policy::{BoundedToken, ExecutionDomain}, - resource_schema::PlacementAnchor, -}; + ArtifactDigest, ArtifactDigestSet, CompatibilityRange, ComponentDescriptor, ComponentType, + PolicyEvaluation, RevocationState, SignatureState, StandardCapabilityMatrix, TrustEvidence, + UpgradeDisposition, + }; + use d2b_contracts_resource::v3::{ + ArtifactId, SchemaVersion, + execution_policy::{BoundedToken, ExecutionDomain}, + resource_schema::PlacementAnchor, + }; use super::*; diff --git a/packages/d2b-core-controller/src/audit.rs b/packages/d2b-core-controller/src/audit.rs index 5eb0f033f6..c8029b6f6b 100644 --- a/packages/d2b-core-controller/src/audit.rs +++ b/packages/d2b-core-controller/src/audit.rs @@ -9,15 +9,8 @@ use std::collections::BTreeSet; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceBundleGenerationId, - ResourceName, - ResourceTypeName, - SchemaFingerprint, - Timestamp, - ZoneId, - ZoneRevision, - canonical_digest, + ConfigurationGeneration, ResourceBundleGenerationId, ResourceName, ResourceTypeName, + SchemaFingerprint, Timestamp, ZoneId, ZoneRevision, canonical_digest, }; /// Domain tag for the digest of a resource name in an audit record. diff --git a/packages/d2b-core-controller/src/authority.rs b/packages/d2b-core-controller/src/authority.rs index 070c543f6b..81ecb12b9d 100644 --- a/packages/d2b-core-controller/src/authority.rs +++ b/packages/d2b-core-controller/src/authority.rs @@ -15,14 +15,12 @@ use std::{ }; use d2b_contracts_resource::v3::{ - CanonicalJsonValue, - IfName, - ResourceGeneration, - ResourceRef, - ResourceUid, - UpdateState, + CanonicalJsonValue, IfName, ResourceGeneration, ResourceRef, ResourceUid, UpdateState, is_canonical_digest, - network::{ExternalNicAdmissionError, ExternalNicAuthorityStatus, ExternalNicClaim, MacvtapMode, SharingPolicy, admit_external_nic_claims}, + network::{ + ExternalNicAdmissionError, ExternalNicAuthorityStatus, ExternalNicClaim, MacvtapMode, + SharingPolicy, admit_external_nic_claims, + }, process::PortProtocol, resource_schema::canonical_digest, }; diff --git a/packages/d2b-core-controller/src/authz.rs b/packages/d2b-core-controller/src/authz.rs index d4b664f36f..15f491fcc4 100644 --- a/packages/d2b-core-controller/src/authz.rs +++ b/packages/d2b-core-controller/src/authz.rs @@ -2,21 +2,12 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_resource::v3::{ - ControllerGeneration, - ResourceGeneration, - ResourceName, - ResourceRef, - ResourceTypeName, - ResourceUid, - SchemaFingerprint, -}; use d2b_contracts_resource::v3::identity::{ - AuthenticatedSubjectContext, - EvidenceClass, - Locality, - ServiceName, - SessionPurpose, + AuthenticatedSubjectContext, EvidenceClass, Locality, ServiceName, SessionPurpose, +}; +use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceName, ResourceRef, ResourceTypeName, + ResourceUid, SchemaFingerprint, }; /// Borrowed subject evidence admitted by the authenticated session adapter. @@ -319,15 +310,10 @@ impl AuthorizationHandler { #[cfg(test)] mod tests { use d2b_contracts_resource::v3::SchemaFingerprint; -use d2b_contracts_resource::v3::identity::{ - BindingDigest, - ReconnectGeneration, - ServiceName, - SessionBinding, - SessionPurpose, - TranscriptHash, - TransportBinding, -}; + use d2b_contracts_resource::v3::identity::{ + BindingDigest, ReconnectGeneration, ServiceName, SessionBinding, SessionPurpose, + TranscriptHash, TransportBinding, + }; use super::*; diff --git a/packages/d2b-core-controller/src/cleanup.rs b/packages/d2b-core-controller/src/cleanup.rs index c8b069ed51..159a2aa2bd 100644 --- a/packages/d2b-core-controller/src/cleanup.rs +++ b/packages/d2b-core-controller/src/cleanup.rs @@ -7,11 +7,7 @@ use std::collections::{BTreeMap, BTreeSet}; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceBundleGenerationId, - ResourceTypeName, - Timestamp, - ZoneRevision, + ConfigurationGeneration, ResourceBundleGenerationId, ResourceTypeName, Timestamp, ZoneRevision, execution_policy::DurationMs, }; @@ -949,10 +945,7 @@ fn civil_from_days(days: i128) -> (i128, i128, i128) { #[cfg(test)] mod ephemeral_cleanup_tests { use super::*; - use d2b_contracts_resource::v3::{ - ResourceName, - ResourceTypeName, -}; + use d2b_contracts_resource::v3::{ResourceName, ResourceTypeName}; fn key() -> ResourceKey { ResourceKey::new( diff --git a/packages/d2b-core-controller/src/configuration/bundle_apply.rs b/packages/d2b-core-controller/src/configuration/bundle_apply.rs index 58cbff7fe5..094c899c7b 100644 --- a/packages/d2b-core-controller/src/configuration/bundle_apply.rs +++ b/packages/d2b-core-controller/src/configuration/bundle_apply.rs @@ -7,14 +7,9 @@ use std::collections::{BTreeMap, BTreeSet}; +use d2b_contracts_resource::v3::{ConfigurationGeneration, Timestamp}; use d2b_contracts_zone_session::v3::{ - BundleMetadata, - BundleResource as InputBundleResource, - ZoneBundle, -}; -use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - Timestamp, + BundleMetadata, BundleResource as InputBundleResource, ZoneBundle, }; use super::{ @@ -617,18 +612,11 @@ fn persist_effect( #[cfg(test)] mod tests { use super::*; - use d2b_contracts_zone_session::v3::{ - BundleMetadata, - BundleResource as InputBundleResource, -}; use d2b_contracts_resource::v3::{ - CanonicalJsonObject, - ResourceBundleGenerationId, - ResourceName, - ResourceTypeName, - SchemaFingerprint, - ZoneId, -}; + CanonicalJsonObject, ResourceBundleGenerationId, ResourceName, ResourceTypeName, + SchemaFingerprint, ZoneId, + }; + use d2b_contracts_zone_session::v3::{BundleMetadata, BundleResource as InputBundleResource}; fn digest(byte: char) -> SchemaFingerprint { SchemaFingerprint::parse(format!("sha256:{}", byte.to_string().repeat(64))).unwrap() diff --git a/packages/d2b-core-controller/src/configuration/generation_transition.rs b/packages/d2b-core-controller/src/configuration/generation_transition.rs index d9aef3736d..2cada9c745 100644 --- a/packages/d2b-core-controller/src/configuration/generation_transition.rs +++ b/packages/d2b-core-controller/src/configuration/generation_transition.rs @@ -6,13 +6,10 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_zone_session::v3::ZoneBundle; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceBundleGenerationId, - SchemaFingerprint, - Timestamp, + ConfigurationGeneration, ResourceBundleGenerationId, SchemaFingerprint, Timestamp, }; +use d2b_contracts_zone_session::v3::ZoneBundle; use crate::resource_store::{ ManagedBy, PersistedResourceMetadata, PersistedResourceRecord, ResourceMetadataError, diff --git a/packages/d2b-core-controller/src/configuration/mod.rs b/packages/d2b-core-controller/src/configuration/mod.rs index ba5bad2d4b..c5af36ec5b 100644 --- a/packages/d2b-core-controller/src/configuration/mod.rs +++ b/packages/d2b-core-controller/src/configuration/mod.rs @@ -55,18 +55,11 @@ pub mod generation_transition; use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_zone_session::v3::ZoneBundle; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceBundleGenerationId, - ResourceName, - ResourceTypeName, - ResourceUid, - SchemaFingerprint, - Timestamp, - ZoneId, - ZoneRevision, + ConfigurationGeneration, ResourceBundleGenerationId, ResourceName, ResourceTypeName, + ResourceUid, SchemaFingerprint, Timestamp, ZoneId, ZoneRevision, }; +use d2b_contracts_zone_session::v3::ZoneBundle; use crate::{ audit::{AuditError, AuditEvent, AuditLedger, AuditReason}, diff --git a/packages/d2b-core-controller/src/controller_assignment.rs b/packages/d2b-core-controller/src/controller_assignment.rs new file mode 100644 index 0000000000..46d72cd7b6 --- /dev/null +++ b/packages/d2b-core-controller/src/controller_assignment.rs @@ -0,0 +1,2039 @@ +//! Single-owner Provider controller assignment and fenced ResourceClient leases. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt, + sync::{ + Arc, + atomic::{AtomicBool, AtomicU8, Ordering}, + }, +}; + +use d2b_contracts_provider::v3::{ + ComponentType, ControllerInstanceScope, ControllerTargetKind, ProviderManifest, +}; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; +use d2b_contracts_resource::v3::{ + CanonicalJsonValue, ControllerGeneration, PlacementAnchor, PlacementTarget, + PlacementTargetKind, ResourceEnvelope, ResourceGeneration, ResourceName, ResourceRef, + ResourceTypeName, ResourceUid, ZoneId, ZoneRevision, +}; +use serde_json::{Map, Value, json}; + +/// Maximum encoded assignment evidence carried by one scoped commit. +pub const MAX_SCOPED_COMMIT_TRANSPORT_BYTES: usize = 64 * 1024; + +/// The maximum number of assignments held by one Zone authority. +pub const MAX_ASSIGNMENTS: usize = 16_384; +/// Maximum child ownership entries retained by one assignment. +pub const MAX_ASSIGNED_CHILDREN: usize = 4_096; + +/// A monotonically increasing, nonzero assignment epoch. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AssignmentEpoch(u64); + +impl AssignmentEpoch { + /// Construct a nonzero epoch. + pub fn new(value: u64) -> Result { + if value == 0 { + return Err(AssignmentError::EpochExhausted); + } + Ok(Self(value)) + } + + /// Return the opaque epoch ordinal. + pub const fn get(self) -> u64 { + self.0 + } +} + +impl fmt::Debug for AssignmentEpoch { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("AssignmentEpoch()") + } +} + +/// The exact target selected by a contract-owned placement anchor. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AssignmentTarget { + /// The Zone singleton target. + Zone(ZoneId), + /// One exact Host or Guest execution target. + Execution { + /// The target kind. + kind: PlacementTargetKind, + /// The exact target reference. + reference: ResourceRef, + }, +} + +impl AssignmentTarget { + fn from_placement(target: PlacementTarget) -> Self { + match target { + PlacementTarget::Zone(zone) => Self::Zone(zone), + PlacementTarget::Execution { kind, reference } => Self::Execution { kind, reference }, + } + } + + fn target_kind(&self) -> Option { + match self { + Self::Zone(_) => Some(ControllerTargetKind::Zone), + Self::Execution { + kind: PlacementTargetKind::Host, + .. + } => Some(ControllerTargetKind::Host), + Self::Execution { + kind: PlacementTargetKind::Guest, + .. + } => Some(ControllerTargetKind::Guest), + } + } + + /// Borrow the exact execution target reference, when present. + pub fn execution_ref(&self) -> Option<&ResourceRef> { + match self { + Self::Zone(_) => None, + Self::Execution { reference, .. } => Some(reference), + } + } +} + +impl fmt::Debug for AssignmentTarget { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Zone(_) => formatter.write_str("AssignmentTarget::Zone()"), + Self::Execution { kind, .. } => formatter + .debug_struct("AssignmentTarget::Execution") + .field("kind", kind) + .finish_non_exhaustive(), + } + } +} + +/// One resource-plane operation a controller lease may perform. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AssignmentVerb { + Get, + List, + Watch, + Create, + UpdateSpec, + UpdateStatus, + UpdateMetadata, + UpdateFinalizers, + Delete, + CommitBatch, +} + +impl AssignmentVerb { + /// Whether this operation can mutate durable resource state. + pub const fn is_mutating(self) -> bool { + !matches!(self, Self::Get | Self::List | Self::Watch) + } +} + +/// Lifecycle of one resource's controller assignment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AssignmentPhase { + Pending, + Assigned, + Draining, + Revoked, + Stale, + Quarantined, + Released, +} + +impl AssignmentPhase { + const fn code(self) -> u8 { + match self { + Self::Pending => 0, + Self::Assigned => 1, + Self::Draining => 2, + Self::Revoked => 3, + Self::Stale => 4, + Self::Quarantined => 5, + Self::Released => 6, + } + } + + fn from_code(value: u8) -> Self { + match value { + 1 => Self::Assigned, + 2 => Self::Draining, + 3 => Self::Revoked, + 4 => Self::Stale, + 5 => Self::Quarantined, + 6 => Self::Released, + _ => Self::Pending, + } + } + + const fn admits_watch(self) -> bool { + matches!(self, Self::Assigned) + } + + const fn admits_mutation(self) -> bool { + matches!(self, Self::Assigned) + } +} + +/// Closed assignment or lease failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AssignmentError { + InvalidRole, + ProviderGenerationMismatch, + ControllerGenerationMismatch, + SessionGenerationInvalid, + SessionBindingMismatch, + ResourceTypeUnowned, + PlacementAnchorMissing, + PlacementTargetInvalid, + TargetKindUnsupported, + TargetNotReady, + AssignmentConflict, + AssignmentLimit, + AssignmentMissing, + AssignmentNotDraining, + AssignmentNotReleased, + ChildrenRemain, + ChildLimit, + StaleAssignment, + SessionRevoked, + ResourceRevisionMismatch, + ResourceUidMismatch, + ResourceNotAssigned, + TargetMismatch, + VerbNotAllowed, + QueryWidened, + EpochExhausted, + RoleContractInvalid, +} + +/// Failure while decoding the assignment evidence carried by the existing +/// Resource CommitBatch transport. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AssignmentTransportError { + Malformed, + TooLarge, +} + +impl AssignmentTransportError { + /// Return the stable identity-free reason code. + pub const fn code(self) -> &'static str { + match self { + Self::Malformed => "assignment-transport-malformed", + Self::TooLarge => "assignment-transport-too-large", + } + } +} + +impl fmt::Display for AssignmentTransportError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.code()) + } +} + +impl std::error::Error for AssignmentTransportError {} + +impl AssignmentError { + /// Return the stable identity-free reason code. + pub const fn code(self) -> &'static str { + match self { + Self::InvalidRole => "assignment-role-invalid", + Self::ProviderGenerationMismatch => "assignment-provider-generation-mismatch", + Self::ControllerGenerationMismatch => "assignment-controller-generation-mismatch", + Self::SessionGenerationInvalid => "assignment-session-generation-invalid", + Self::SessionBindingMismatch => "assignment-session-binding-mismatch", + Self::ResourceTypeUnowned => "assignment-resource-type-unowned", + Self::PlacementAnchorMissing => "assignment-placement-anchor-missing", + Self::PlacementTargetInvalid => "assignment-placement-target-invalid", + Self::TargetKindUnsupported => "assignment-target-kind-unsupported", + Self::TargetNotReady => "assignment-target-not-ready", + Self::AssignmentConflict => "assignment-conflict", + Self::AssignmentLimit => "assignment-limit", + Self::AssignmentMissing => "assignment-missing", + Self::AssignmentNotDraining => "assignment-not-draining", + Self::AssignmentNotReleased => "assignment-not-released", + Self::ChildrenRemain => "assignment-children-remain", + Self::ChildLimit => "assignment-child-limit", + Self::StaleAssignment => "assignment-stale", + Self::SessionRevoked => "assignment-session-revoked", + Self::ResourceRevisionMismatch => "assignment-resource-revision-mismatch", + Self::ResourceUidMismatch => "assignment-resource-uid-mismatch", + Self::ResourceNotAssigned => "assignment-resource-not-assigned", + Self::TargetMismatch => "assignment-target-mismatch", + Self::VerbNotAllowed => "assignment-verb-not-allowed", + Self::QueryWidened => "assignment-query-widened", + Self::EpochExhausted => "assignment-epoch-exhausted", + Self::RoleContractInvalid => "assignment-role-contract-invalid", + } + } +} + +impl fmt::Display for AssignmentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.code()) + } +} + +impl std::error::Error for AssignmentError {} + +/// An immutable assignment identity carried by every controller admission. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AssignmentIdentity { + resource_uid: ResourceUid, + resource_revision: ZoneRevision, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + controller_role: ResourceRef, + target: AssignmentTarget, + session_generation: ReconnectGeneration, + epoch: AssignmentEpoch, +} + +impl AssignmentIdentity { + /// Construct one identity from authoritative committed values. + #[allow(clippy::too_many_arguments)] + fn new( + resource_uid: ResourceUid, + resource_revision: ZoneRevision, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + controller_role: ResourceRef, + target: AssignmentTarget, + session_generation: ReconnectGeneration, + epoch: AssignmentEpoch, + ) -> Self { + Self { + resource_uid, + resource_revision, + provider_generation, + controller_generation, + controller_role, + target, + session_generation, + epoch, + } + } +} + +/// Assignment and mutation evidence forwarded through the existing Resource +/// CommitBatch RPC after bus admission. +#[derive(Clone, PartialEq, Eq)] +pub struct ScopedCommitTransport { + assignment: AssignmentIdentity, + mutations: Vec, +} + +impl ScopedCommitTransport { + /// Construct transport evidence from one admitted assignment call. + pub fn new( + assignment: AssignmentIdentity, + mutations: Vec, + ) -> Result { + if mutations.is_empty() + || mutations.len() > 128 + || mutations.iter().any(|mutation| { + mutation.assignment() != &assignment + || !mutation.verb().is_mutating() + || mutation.verb() == AssignmentVerb::CommitBatch + }) + { + return Err(AssignmentTransportError::Malformed); + } + Ok(Self { + assignment, + mutations, + }) + } + + /// Borrow the admitted assignment. + pub const fn assignment(&self) -> &AssignmentIdentity { + &self.assignment + } + + /// Borrow the admitted mutations. + pub fn mutations(&self) -> &[ScopedResourceMutation] { + &self.mutations + } + + /// Encode the evidence as bounded canonical JSON bytes. + pub fn encode(&self) -> Result, AssignmentTransportError> { + let value = json!({ + "version": 1, + "assignment": encode_assignment(&self.assignment), + "mutations": self + .mutations + .iter() + .map(encode_mutation) + .collect::>(), + }); + let bytes = serde_json::to_vec(&value).map_err(|_| AssignmentTransportError::Malformed)?; + let bytes = CanonicalJsonValue::parse(&bytes) + .map_err(|_| AssignmentTransportError::Malformed)? + .to_canonical_bytes(); + if bytes.len() > MAX_SCOPED_COMMIT_TRANSPORT_BYTES { + return Err(AssignmentTransportError::TooLarge); + } + Ok(bytes) + } + + /// Decode bounded evidence produced by [`Self::encode`]. + pub fn decode(bytes: &[u8]) -> Result { + if bytes.is_empty() || bytes.len() > MAX_SCOPED_COMMIT_TRANSPORT_BYTES { + return Err(AssignmentTransportError::TooLarge); + } + CanonicalJsonValue::parse(bytes).map_err(|_| AssignmentTransportError::Malformed)?; + let value = serde_json::from_slice::(bytes) + .map_err(|_| AssignmentTransportError::Malformed)?; + let object = value + .as_object() + .ok_or(AssignmentTransportError::Malformed)?; + require_exact_keys(object, &["version", "assignment", "mutations"])?; + if object.get("version").and_then(Value::as_u64) != Some(1) { + return Err(AssignmentTransportError::Malformed); + } + let assignment = decode_assignment( + object + .get("assignment") + .ok_or(AssignmentTransportError::Malformed)?, + )?; + let mutation_values = object + .get("mutations") + .and_then(Value::as_array) + .ok_or(AssignmentTransportError::Malformed)?; + if mutation_values.is_empty() || mutation_values.len() > 128 { + return Err(AssignmentTransportError::Malformed); + } + let mutations = mutation_values + .iter() + .map(|value| { + let object = value + .as_object() + .ok_or(AssignmentTransportError::Malformed)?; + require_exact_keys(object, &["target", "verb"])?; + let target = ResourceRef::parse( + object + .get("target") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?; + let verb = decode_assignment_verb( + object + .get("verb") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + )?; + Ok(ScopedResourceMutation { + assignment: assignment.clone(), + target, + verb, + }) + }) + .collect::, _>>()?; + Self::new(assignment, mutations) + } +} + +impl fmt::Debug for ScopedCommitTransport { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ScopedCommitTransport") + .field("mutation_count", &self.mutations.len()) + .finish() + } +} + +fn encode_assignment(identity: &AssignmentIdentity) -> Value { + let target = match identity.target() { + AssignmentTarget::Zone(zone) => json!({ + "kind": "zone", + "zone": zone.as_str(), + }), + AssignmentTarget::Execution { kind, reference } => json!({ + "kind": "execution", + "targetKind": match kind { + PlacementTargetKind::Host => "host", + PlacementTargetKind::Guest => "guest", + }, + "reference": reference.to_canonical_string(), + }), + }; + json!({ + "resourceUid": identity.resource_uid().as_str(), + "resourceRevision": identity.resource_revision().get(), + "providerGeneration": identity.provider_generation().get(), + "controllerGeneration": identity.controller_generation().get(), + "controllerRole": identity.controller_role().to_canonical_string(), + "target": target, + "sessionGeneration": identity.session_generation().get(), + "epoch": identity.epoch().get(), + }) +} + +fn encode_mutation(mutation: &ScopedResourceMutation) -> Value { + json!({ + "target": mutation.target().to_canonical_string(), + "verb": encode_assignment_verb(mutation.verb()), + }) +} + +fn encode_assignment_verb(verb: AssignmentVerb) -> &'static str { + match verb { + AssignmentVerb::Get => "Get", + AssignmentVerb::List => "List", + AssignmentVerb::Watch => "Watch", + AssignmentVerb::Create => "Create", + AssignmentVerb::UpdateSpec => "UpdateSpec", + AssignmentVerb::UpdateStatus => "UpdateStatus", + AssignmentVerb::UpdateMetadata => "UpdateMetadata", + AssignmentVerb::UpdateFinalizers => "UpdateFinalizers", + AssignmentVerb::Delete => "Delete", + AssignmentVerb::CommitBatch => "CommitBatch", + } +} + +fn decode_assignment_verb(value: &str) -> Result { + match value { + "Get" => Ok(AssignmentVerb::Get), + "List" => Ok(AssignmentVerb::List), + "Watch" => Ok(AssignmentVerb::Watch), + "Create" => Ok(AssignmentVerb::Create), + "UpdateSpec" => Ok(AssignmentVerb::UpdateSpec), + "UpdateStatus" => Ok(AssignmentVerb::UpdateStatus), + "UpdateMetadata" => Ok(AssignmentVerb::UpdateMetadata), + "UpdateFinalizers" => Ok(AssignmentVerb::UpdateFinalizers), + "Delete" => Ok(AssignmentVerb::Delete), + "CommitBatch" => Ok(AssignmentVerb::CommitBatch), + _ => Err(AssignmentTransportError::Malformed), + } +} + +fn decode_assignment(value: &Value) -> Result { + let object = value + .as_object() + .ok_or(AssignmentTransportError::Malformed)?; + require_exact_keys( + object, + &[ + "resourceUid", + "resourceRevision", + "providerGeneration", + "controllerGeneration", + "controllerRole", + "target", + "sessionGeneration", + "epoch", + ], + )?; + let target = decode_assignment_target( + object + .get("target") + .ok_or(AssignmentTransportError::Malformed)?, + )?; + Ok(AssignmentIdentity::new( + ResourceUid::parse( + object + .get("resourceUid") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + ZoneRevision::new( + object + .get("resourceRevision") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ), + ResourceGeneration::new( + object + .get("providerGeneration") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + ControllerGeneration::new( + object + .get("controllerGeneration") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + ResourceRef::parse( + object + .get("controllerRole") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + target, + ReconnectGeneration::new( + object + .get("sessionGeneration") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + AssignmentEpoch::new( + object + .get("epoch") + .and_then(Value::as_u64) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + )) +} + +fn decode_assignment_target(value: &Value) -> Result { + let object = value + .as_object() + .ok_or(AssignmentTransportError::Malformed)?; + let kind = object + .get("kind") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?; + match kind { + "zone" => { + require_exact_keys(object, &["kind", "zone"])?; + Ok(AssignmentTarget::Zone( + ZoneId::parse( + object + .get("zone") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?, + )) + } + "execution" => { + require_exact_keys(object, &["kind", "targetKind", "reference"])?; + let reference = ResourceRef::parse( + object + .get("reference") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)?, + ) + .map_err(|_| AssignmentTransportError::Malformed)?; + let target_kind = match object + .get("targetKind") + .and_then(Value::as_str) + .ok_or(AssignmentTransportError::Malformed)? + { + "host" => PlacementTargetKind::Host, + "guest" => PlacementTargetKind::Guest, + _ => return Err(AssignmentTransportError::Malformed), + }; + if (target_kind == PlacementTargetKind::Host + && reference.resource_type().as_str() != "Host") + || (target_kind == PlacementTargetKind::Guest + && reference.resource_type().as_str() != "Guest") + { + return Err(AssignmentTransportError::Malformed); + } + Ok(AssignmentTarget::Execution { + kind: target_kind, + reference, + }) + } + _ => Err(AssignmentTransportError::Malformed), + } +} + +fn require_exact_keys( + object: &Map, + expected: &[&str], +) -> Result<(), AssignmentTransportError> { + if object.len() != expected.len() || expected.iter().any(|key| !object.contains_key(*key)) { + return Err(AssignmentTransportError::Malformed); + } + Ok(()) +} + +impl AssignmentIdentity { + /// Borrow the assigned resource UID. + pub const fn resource_uid(&self) -> &ResourceUid { + &self.resource_uid + } + + /// Return the committed resource revision bound by this identity. + pub const fn resource_revision(&self) -> ZoneRevision { + self.resource_revision + } + + /// Return the Provider generation bound by this identity. + pub const fn provider_generation(&self) -> ResourceGeneration { + self.provider_generation + } + + /// Return the Core controller generation bound by this identity. + pub const fn controller_generation(&self) -> ControllerGeneration { + self.controller_generation + } + + /// Borrow the signed controller role. + pub const fn controller_role(&self) -> &ResourceRef { + &self.controller_role + } + + /// Borrow the exact assigned target. + pub const fn target(&self) -> &AssignmentTarget { + &self.target + } + + /// Return the authenticated ComponentSession generation. + pub const fn session_generation(&self) -> ReconnectGeneration { + self.session_generation + } + + /// Return the assignment epoch. + pub const fn epoch(&self) -> AssignmentEpoch { + self.epoch + } +} + +impl fmt::Debug for AssignmentIdentity { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("AssignmentIdentity") + .field("resource_uid", &"") + .field("resource_revision", &self.resource_revision) + .field("provider_generation", &self.provider_generation) + .field("controller_generation", &self.controller_generation) + .field("controller_role", &"") + .field("target", &self.target) + .field("session_generation", &self.session_generation) + .field("epoch", &self.epoch) + .finish() + } +} + +/// The signed role and placement contract used for assignment admission. +#[derive(Clone, PartialEq, Eq)] +pub struct ControllerRoleContract { + provider_ref: ResourceRef, + role_ref: ResourceRef, + scope: ControllerInstanceScope, + supported_target_kinds: BTreeSet, + resource_types: BTreeSet, + placements: BTreeMap, +} + +impl ControllerRoleContract { + /// Derive one role contract from a trusted signed Provider manifest. + pub fn from_signed_manifest( + provider_ref: ResourceRef, + role_ref: ResourceRef, + manifest: &ProviderManifest, + ) -> Result { + if provider_ref.resource_type().as_str() != "Provider" + || role_ref.resource_type().as_str() != "Process" + || manifest.validate_installation_contract().is_err() + { + return Err(AssignmentError::InvalidRole); + } + let component = manifest + .components() + .iter() + .find(|component| { + component.component_type() == ComponentType::Controller + && component.component_id().as_str() == role_ref.name().as_str() + }) + .ok_or(AssignmentError::InvalidRole)?; + let scope = component + .instance_scope() + .ok_or(AssignmentError::RoleContractInvalid)?; + let mut placements = BTreeMap::new(); + for resource_type in component.exported_resource_types() { + let binding = manifest + .binding_for(resource_type) + .ok_or(AssignmentError::PlacementAnchorMissing)?; + let anchor = *binding + .placement_anchor() + .ok_or(AssignmentError::PlacementAnchorMissing)?; + placements.insert(resource_type.clone(), anchor); + } + Ok(Self { + provider_ref, + role_ref, + scope, + supported_target_kinds: component.supported_target_kinds().clone(), + resource_types: component.exported_resource_types().clone(), + placements, + }) + } + + /// Borrow the Provider resource selected by this signed role. + pub const fn provider_ref(&self) -> &ResourceRef { + &self.provider_ref + } + + /// Borrow the controller role reference. + pub const fn role_ref(&self) -> &ResourceRef { + &self.role_ref + } + + /// Return the closed instance scope. + pub const fn scope(&self) -> ControllerInstanceScope { + self.scope + } + + /// Borrow the exclusively owned ResourceTypes. + pub const fn resource_types(&self) -> &BTreeSet { + &self.resource_types + } + + fn placement_for(&self, resource_type: &ResourceTypeName) -> Option { + self.placements.get(resource_type).copied() + } + + fn supports_target(&self, target: &AssignmentTarget) -> bool { + target + .target_kind() + .is_some_and(|kind| self.supported_target_kinds.contains(&kind)) + } +} + +impl fmt::Debug for ControllerRoleContract { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ControllerRoleContract") + .field("scope", &self.scope) + .field("resource_type_count", &self.resource_types.len()) + .field("target_kind_count", &self.supported_target_kinds.len()) + .finish() + } +} + +/// Trusted inputs for one assignment admission. +pub struct AssignmentRequest<'a> { + resource: &'a ResourceEnvelope, + role: &'a ControllerRoleContract, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + session_generation: ReconnectGeneration, + target_ready: bool, +} + +impl<'a> AssignmentRequest<'a> { + /// Bind a committed resource to a signed role and authenticated session. + pub fn new( + resource: &'a ResourceEnvelope, + role: &'a ControllerRoleContract, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + session_generation: ReconnectGeneration, + target_ready: bool, + ) -> Self { + Self { + resource, + role, + provider_generation, + controller_generation, + session_generation, + target_ready, + } + } +} + +/// A controller's non-widenable query scope. +#[derive(Clone, PartialEq, Eq)] +pub struct ScopedResourceQuery { + assignment: AssignmentIdentity, + resource_types: Vec, + resource_names: Vec, + filters: Vec, +} + +impl ScopedResourceQuery { + /// Borrow the immutable assignment evidence. + pub const fn assignment(&self) -> &AssignmentIdentity { + &self.assignment + } + + /// Borrow the exact ResourceType selector. + pub fn resource_types(&self) -> &[ResourceTypeName] { + &self.resource_types + } + + /// Borrow the exact resource-name selector. + pub fn resource_names(&self) -> &[ResourceName] { + &self.resource_names + } + + /// Borrow the assignment-bound filters. + pub fn filters(&self) -> &[ScopedResourceFilter] { + &self.filters + } + + /// Consume the query into transport-neutral parts. + pub fn into_parts( + self, + ) -> ( + AssignmentIdentity, + Vec, + Vec, + Vec, + ) { + ( + self.assignment, + self.resource_types, + self.resource_names, + self.filters, + ) + } +} + +impl fmt::Debug for ScopedResourceQuery { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ScopedResourceQuery") + .field("resource_type_count", &self.resource_types.len()) + .field("resource_name_count", &self.resource_names.len()) + .field("filter_count", &self.filters.len()) + .finish() + } +} + +/// One exact filter minted by a controller assignment. +#[derive(Clone, PartialEq, Eq)] +pub struct ScopedResourceFilter { + field: String, + values: Vec, + assignment_bound: bool, +} + +impl ScopedResourceFilter { + /// Construct a caller-supplied narrowing filter. It cannot name the + /// assignment field; Core appends that filter itself. + pub fn narrow(field: impl Into, values: Vec) -> Result { + let field = field.into(); + if field.is_empty() + || field.len() > 64 + || !field + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_')) + || values.is_empty() + || values.len() > 64 + || values.iter().any(|value| { + value.is_empty() + || value.len() > 128 + || !value + .bytes() + .all(|byte| byte.is_ascii_graphic() || byte == b' ') + }) + || field == "assignment.resourceUid" + { + return Err(AssignmentError::QueryWidened); + } + Ok(Self { + field, + values, + assignment_bound: false, + }) + } + + /// Borrow the indexed field. + pub fn field(&self) -> &str { + &self.field + } + + /// Borrow the accepted values. + pub fn values(&self) -> &[String] { + &self.values + } + + /// Whether this filter was minted by the assignment authority. + pub const fn assignment_bound(&self) -> bool { + self.assignment_bound + } +} + +impl fmt::Debug for ScopedResourceFilter { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ScopedResourceFilter") + .field("field", &self.field) + .field("value_count", &self.values.len()) + .field("assignment_bound", &self.assignment_bound) + .finish() + } +} + +/// A single-resource mutation admitted by a controller lease. +#[derive(Clone, PartialEq, Eq)] +pub struct ScopedResourceMutation { + assignment: AssignmentIdentity, + target: ResourceRef, + verb: AssignmentVerb, +} + +impl ScopedResourceMutation { + /// Borrow the assignment evidence. + pub const fn assignment(&self) -> &AssignmentIdentity { + &self.assignment + } + + /// Borrow the exact target. + pub const fn target(&self) -> &ResourceRef { + &self.target + } + + /// Return the admitted verb. + pub const fn verb(&self) -> AssignmentVerb { + self.verb + } +} + +impl fmt::Debug for ScopedResourceMutation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ScopedResourceMutation") + .field("target", &"") + .field("verb", &self.verb) + .finish() + } +} + +/// A non-clonable ResourceClient capability minted for one assignment. +pub struct ResourceClientLease { + identity: AssignmentIdentity, + resource_ref: ResourceRef, + resource_types: BTreeSet, + state: Arc, + allowed_verbs: BTreeSet, +} + +impl ResourceClientLease { + /// Borrow the complete immutable assignment identity. + pub const fn identity(&self) -> &AssignmentIdentity { + &self.identity + } + + /// Borrow the exact assigned resource target. + pub const fn resource_ref(&self) -> &ResourceRef { + &self.resource_ref + } + + /// Borrow the exact assigned placement target. + pub const fn target(&self) -> &AssignmentTarget { + self.identity.target() + } + + /// Return the current lease phase. + pub fn phase(&self) -> AssignmentPhase { + AssignmentPhase::from_code(self.state.phase.load(Ordering::Acquire)) + } + + /// Mint a query whose assignment filter cannot be removed or widened. + pub fn query( + &self, + resource_types: Vec, + resource_names: Vec, + filters: Vec, + ) -> Result { + let phase = self.phase(); + if !phase.admits_watch() { + return Err(match phase { + AssignmentPhase::Revoked => AssignmentError::SessionRevoked, + AssignmentPhase::Stale + | AssignmentPhase::Draining + | AssignmentPhase::Released + | AssignmentPhase::Quarantined => AssignmentError::StaleAssignment, + AssignmentPhase::Pending => AssignmentError::AssignmentMissing, + AssignmentPhase::Assigned => AssignmentError::StaleAssignment, + }); + } + if resource_types + .iter() + .any(|resource_type| !self.resource_types.contains(resource_type)) + { + return Err(AssignmentError::QueryWidened); + } + if filters + .iter() + .any(|filter| filter.field() == "assignment.resourceUid") + { + return Err(AssignmentError::QueryWidened); + } + let mut filters = filters; + filters.push(ScopedResourceFilter { + field: "assignment.resourceUid".to_owned(), + values: vec![self.identity.resource_uid().as_str().to_owned()], + assignment_bound: true, + }); + Ok(ScopedResourceQuery { + assignment: self.identity.clone(), + resource_types, + resource_names, + filters, + }) + } + + /// Admit a mutation against the resource owned by this lease. + pub fn mutation( + &self, + target: ResourceRef, + verb: AssignmentVerb, + ) -> Result { + let phase = self.phase(); + if !phase.admits_mutation() { + return Err(match phase { + AssignmentPhase::Revoked => AssignmentError::SessionRevoked, + _ => AssignmentError::StaleAssignment, + }); + } + if verb == AssignmentVerb::CommitBatch || !self.allowed_verbs.contains(&verb) { + return Err(AssignmentError::VerbNotAllowed); + } + if target != self.resource_ref { + return Err(AssignmentError::ResourceNotAssigned); + } + Ok(ScopedResourceMutation { + assignment: self.identity.clone(), + target, + verb, + }) + } + + /// Verify that a placement target remains exactly the admitted target. + pub fn target_for(&self, target: PlacementTarget) -> Result<(), AssignmentError> { + if self.target() == &AssignmentTarget::from_placement(target) { + Ok(()) + } else { + Err(AssignmentError::TargetMismatch) + } + } +} + +impl fmt::Debug for ResourceClientLease { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ResourceClientLease") + .field("resource_ref", &"") + .field("phase", &self.phase()) + .field("resource_type_count", &self.resource_types.len()) + .field("allowed_verb_count", &self.allowed_verbs.len()) + .finish() + } +} + +struct AssignmentRecord { + identity: AssignmentIdentity, + allowed_verbs: BTreeSet, + state: Arc, + children: BTreeSet, +} + +struct AssignmentLeaseState { + phase: AtomicU8, + stale_observation: AtomicBool, +} + +impl AssignmentLeaseState { + fn new(phase: AssignmentPhase) -> Self { + Self { + phase: AtomicU8::new(phase.code()), + stale_observation: AtomicBool::new(false), + } + } + + fn phase(&self) -> AssignmentPhase { + AssignmentPhase::from_code(self.phase.load(Ordering::Acquire)) + } + + fn set_phase(&self, phase: AssignmentPhase) { + self.phase.store(phase.code(), Ordering::Release); + } + + fn mark_stale(&self) { + self.stale_observation.store(true, Ordering::Release); + } +} + +/// Core's single-owner assignment registry. +#[derive(Default)] +pub struct ControllerAssignmentRegistry { + records: BTreeMap, + active_targets: BTreeMap<(ResourceTypeName, AssignmentTarget), BTreeSet>, + next_epoch: u64, +} + +impl fmt::Debug for ControllerAssignmentRegistry { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ControllerAssignmentRegistry") + .field("assignment_count", &self.records.len()) + .field("active_target_count", &self.active_targets.len()) + .finish() + } +} + +impl ControllerAssignmentRegistry { + /// Admit one resource from the committed store snapshot. + pub fn admit( + &mut self, + request: AssignmentRequest<'_>, + ) -> Result { + let resource_type = request.resource.resource_type().clone(); + if !request.role.resource_types.contains(&resource_type) { + return Err(AssignmentError::ResourceTypeUnowned); + } + if request.resource.spec().provider_ref() != Some(request.role.provider_ref()) { + return Err(AssignmentError::InvalidRole); + } + let placement_anchor = request + .role + .placement_for(&resource_type) + .ok_or(AssignmentError::PlacementAnchorMissing)?; + let target = AssignmentTarget::from_placement( + placement_anchor + .resolve(request.resource) + .map_err(|_| AssignmentError::PlacementTargetInvalid)?, + ); + if !request.role.supports_target(&target) { + return Err(AssignmentError::TargetKindUnsupported); + } + if matches!( + request.role.scope(), + ControllerInstanceScope::ZoneSingleton + if !matches!(target, AssignmentTarget::Zone(_)) + ) || matches!( + request.role.scope(), + ControllerInstanceScope::FixedExecutionTarget + if !matches!(target, AssignmentTarget::Execution { .. }) + ) { + return Err(AssignmentError::RoleContractInvalid); + } + if !request.target_ready { + return Err(AssignmentError::TargetNotReady); + } + if let Some(existing) = self.records.get(request.resource.metadata().uid()) { + if matches!( + existing.state.phase(), + AssignmentPhase::Released | AssignmentPhase::Quarantined + ) { + self.records.remove(request.resource.metadata().uid()); + } else { + return Err(AssignmentError::AssignmentConflict); + } + } + if self.records.len() >= MAX_ASSIGNMENTS { + return Err(AssignmentError::AssignmentLimit); + } + let target_key = (resource_type.clone(), target.clone()); + if self + .active_targets + .get(&target_key) + .is_some_and(|uids| !uids.is_empty()) + // A per-resource target role intentionally allows multiple + // resources at one target. The key is therefore only used for + // fixed/Zone singleton roles. + && matches!( + request.role.scope(), + ControllerInstanceScope::ZoneSingleton + | ControllerInstanceScope::FixedExecutionTarget + ) + { + return Err(AssignmentError::AssignmentConflict); + } + let epoch_value = self + .next_epoch + .checked_add(1) + .ok_or(AssignmentError::EpochExhausted)?; + self.next_epoch = epoch_value; + let epoch = AssignmentEpoch::new(epoch_value)?; + let identity = AssignmentIdentity::new( + request.resource.metadata().uid().clone(), + request.resource.metadata().revision(), + request.provider_generation, + request.controller_generation, + request.role.role_ref().clone(), + target.clone(), + request.session_generation, + epoch, + ); + let allowed_verbs = BTreeSet::from([ + AssignmentVerb::Get, + AssignmentVerb::List, + AssignmentVerb::Watch, + AssignmentVerb::Create, + AssignmentVerb::UpdateStatus, + AssignmentVerb::UpdateFinalizers, + AssignmentVerb::CommitBatch, + ]); + let state = Arc::new(AssignmentLeaseState::new(AssignmentPhase::Assigned)); + self.records.insert( + identity.resource_uid().clone(), + AssignmentRecord { + identity: identity.clone(), + allowed_verbs: allowed_verbs.clone(), + state: Arc::clone(&state), + children: BTreeSet::new(), + }, + ); + self.active_targets + .entry(target_key) + .or_default() + .insert(identity.resource_uid().clone()); + Ok(ResourceClientLease { + identity, + resource_ref: ResourceRef::new( + resource_type.clone(), + request.resource.metadata().name().clone(), + ), + resource_types: request.role.resource_types.clone(), + state, + allowed_verbs, + }) + } + + /// Rebind one live lease to the resource revision produced by its last + /// successful write without changing its assignment epoch. + pub fn rebind_revision( + &mut self, + lease: &mut ResourceClientLease, + revision: ZoneRevision, + ) -> Result<(), AssignmentError> { + let record = self + .records + .get_mut(lease.identity.resource_uid()) + .ok_or(AssignmentError::AssignmentMissing)?; + if record.identity != lease.identity { + return Err(AssignmentError::StaleAssignment); + } + if record.state.phase() == AssignmentPhase::Revoked { + return Err(AssignmentError::SessionRevoked); + } + if record.state.phase() != AssignmentPhase::Assigned { + return Err(AssignmentError::StaleAssignment); + } + if revision < lease.identity.resource_revision() { + return Err(AssignmentError::ResourceRevisionMismatch); + } + if revision == lease.identity.resource_revision() { + return Ok(()); + } + let mut identity = lease.identity.clone(); + identity.resource_revision = revision; + record.identity = identity.clone(); + lease.identity = identity; + Ok(()) + } + + /// Return the current phase for an assignment identity. + pub fn phase(&self, identity: &AssignmentIdentity) -> Option { + self.records + .get(identity.resource_uid()) + .filter(|record| record.identity == *identity) + .map(|record| record.state.phase()) + } + + /// Mark an assignment as draining before target or generation handoff. + pub fn begin_drain(&mut self, identity: &AssignmentIdentity) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + if record.state.phase() != AssignmentPhase::Assigned { + return Err(AssignmentError::AssignmentNotDraining); + } + record.state.set_phase(AssignmentPhase::Draining); + record.state.mark_stale(); + Ok(()) + } + + /// Release a drained or revoked assignment and its target index. + pub fn release(&mut self, identity: &AssignmentIdentity) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + if !matches!( + record.state.phase(), + AssignmentPhase::Draining | AssignmentPhase::Revoked | AssignmentPhase::Quarantined + ) { + return Err(AssignmentError::AssignmentNotReleased); + } + if !record.children.is_empty() { + return Err(AssignmentError::ChildrenRemain); + } + record.state.set_phase(AssignmentPhase::Released); + self.remove_active_target(identity); + Ok(()) + } + + /// Quarantine an assignment whose target or child ownership is ambiguous. + pub fn quarantine(&mut self, identity: &AssignmentIdentity) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + record.state.set_phase(AssignmentPhase::Quarantined); + record.state.mark_stale(); + self.remove_active_target(identity); + Ok(()) + } + + /// Record one child resource in the assignment's narrow owner index. + pub fn record_child( + &mut self, + identity: &AssignmentIdentity, + child_uid: ResourceUid, + ) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + if record.children.len() >= MAX_ASSIGNED_CHILDREN { + return Err(AssignmentError::ChildLimit); + } + record.children.insert(child_uid); + Ok(()) + } + + /// Remove one child after its terminal deletion is committed. + pub fn remove_child( + &mut self, + identity: &AssignmentIdentity, + child_uid: &ResourceUid, + ) -> Result<(), AssignmentError> { + let record = self.record_mut(identity)?; + if !record.children.remove(child_uid) { + return Err(AssignmentError::AssignmentMissing); + } + Ok(()) + } + + /// Return the currently indexed child UIDs. + pub fn child_uids(&self, identity: &AssignmentIdentity) -> Option<&BTreeSet> { + self.records + .get(identity.resource_uid()) + .filter(|record| record.identity == *identity) + .map(|record| &record.children) + } + + /// Revoke all assignments bound to a disconnected session generation. + pub fn revoke_session(&mut self, generation: ReconnectGeneration) { + for record in self.records.values_mut() { + if record.identity.session_generation() == generation + && matches!( + record.state.phase(), + AssignmentPhase::Assigned | AssignmentPhase::Draining + ) + { + record.state.set_phase(AssignmentPhase::Revoked); + record.state.mark_stale(); + } + } + } + + /// Validate a writer against every assignment fence. + pub fn validate_writer( + &self, + identity: &AssignmentIdentity, + uid: &ResourceUid, + revision: ZoneRevision, + verb: AssignmentVerb, + ) -> Result<(), AssignmentError> { + let record = self + .records + .get(identity.resource_uid()) + .ok_or(AssignmentError::AssignmentMissing)?; + if record.identity != *identity { + return Err(AssignmentError::StaleAssignment); + } + if record.state.phase() == AssignmentPhase::Revoked { + return Err(AssignmentError::SessionRevoked); + } + if !record.state.phase().admits_mutation() { + return Err(AssignmentError::StaleAssignment); + } + if record.identity.resource_uid() != uid { + return Err(AssignmentError::ResourceUidMismatch); + } + if record.identity.resource_revision() != revision { + return Err(AssignmentError::ResourceRevisionMismatch); + } + if !record.allowed_verbs.contains(&verb) { + return Err(AssignmentError::VerbNotAllowed); + } + Ok(()) + } + + /// Validate a read or mutation lease without a new resource snapshot. + pub fn validate_scope( + &self, + identity: &AssignmentIdentity, + verb: AssignmentVerb, + ) -> Result<(), AssignmentError> { + let record = self + .records + .get(identity.resource_uid()) + .ok_or(AssignmentError::AssignmentMissing)?; + if record.identity != *identity { + return Err(AssignmentError::StaleAssignment); + } + if record.state.phase() == AssignmentPhase::Revoked { + return Err(AssignmentError::SessionRevoked); + } + if !record.state.phase().admits_watch() + && matches!( + verb, + AssignmentVerb::Get | AssignmentVerb::List | AssignmentVerb::Watch + ) + { + return Err(AssignmentError::StaleAssignment); + } + if !record.state.phase().admits_mutation() && verb.is_mutating() { + return Err(AssignmentError::StaleAssignment); + } + if !record.allowed_verbs.contains(&verb) { + return Err(AssignmentError::VerbNotAllowed); + } + Ok(()) + } + + /// Whether the last committed observation must be retained as stale. + pub fn observation_is_stale(&self, identity: &AssignmentIdentity) -> bool { + self.records + .get(identity.resource_uid()) + .filter(|record| record.identity == *identity) + .is_some_and(|record| record.state.stale_observation.load(Ordering::Acquire)) + } + + fn record_mut( + &mut self, + identity: &AssignmentIdentity, + ) -> Result<&mut AssignmentRecord, AssignmentError> { + let record = self + .records + .get_mut(identity.resource_uid()) + .ok_or(AssignmentError::AssignmentMissing)?; + if record.identity != *identity { + return Err(AssignmentError::StaleAssignment); + } + Ok(record) + } + + fn remove_active_target(&mut self, identity: &AssignmentIdentity) { + self.active_targets.retain(|_, uids| { + uids.remove(identity.resource_uid()); + !uids.is_empty() + }); + } +} + +#[cfg(test)] +mod tests { + use d2b_contracts_provider::v3::{ + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, ProviderManifest, + ResourceApiBinding, RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, + UpgradeDisposition, UpgradePolicy, + }; + use d2b_contracts_resource::v3::execution_policy::BoundedToken; + use d2b_contracts_resource::v3::identity::ReconnectGeneration; + use d2b_contracts_resource::v3::{ + ControllerGeneration, PlacementTarget, ResourceEnvelope, ResourceGeneration, ResourceRef, + ResourceTypeName, ResourceUid, SchemaFingerprint, SchemaVersion, ZoneRevision, + }; + + use super::{ + AssignmentError, AssignmentPhase, AssignmentRequest, AssignmentTarget, AssignmentVerb, + ControllerAssignmentRegistry, ControllerRoleContract, ScopedCommitTransport, + ScopedResourceFilter, + }; + + const DIGEST: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + + fn digest() -> ArtifactDigest { + ArtifactDigest::parse(DIGEST).unwrap() + } + + fn fingerprint() -> SchemaFingerprint { + SchemaFingerprint::parse(DIGEST).unwrap() + } + + fn manifest() -> ProviderManifest { + let process = ResourceTypeName::parse("Process").unwrap(); + let component = ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [process.clone()], + [], + [d2b_contracts_resource::v3::ExecutionDomain::System], + 8, + digest(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + fingerprint(), + SchemaVersion::new(1, 0).unwrap(), + fingerprint(), + Default::default(), + None, + None, + d2b_contracts_resource::v3::PlacementAnchor::ExecutionRef, + ) + .unwrap(); + let trust = TrustEvidence { + publisher: BoundedToken::parse("trusted").unwrap(), + root_epoch: 1, + publisher_trusted: true, + signature: SignatureState::Valid, + revocation: RevocationState::Clear, + emergency_deny: false, + provenance: PolicyEvaluation::Accepted, + sbom: PolicyEvaluation::Accepted, + license: PolicyEvaluation::Accepted, + vulnerability: PolicyEvaluation::Accepted, + conformance: PolicyEvaluation::Accepted, + support_channel: BoundedToken::parse("stable").unwrap(), + }; + ProviderManifest::new( + d2b_contracts_resource::v3::ArtifactId::parse("provider-runtime").unwrap(), + ArtifactDigestSet { + package: digest(), + executable: digest(), + manifest: digest(), + config: digest(), + schema: digest(), + service: digest(), + }, + trust, + CompatibilityRange { + api_major: 3, + api_minor: 0, + descriptor_fingerprint: fingerprint(), + state_schema_version: SchemaVersion::new(1, 0).unwrap(), + }, + [component], + [binding], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new(ControllerTargetKind::Host, digest(), digest()).unwrap(), + TargetRuntimeArtifacts::new(ControllerTargetKind::Guest, digest(), digest()).unwrap(), + ]) + .unwrap() + } + + fn process(name: &str, execution_ref: &str, revision: u64) -> ResourceEnvelope { + let uid = if name.contains("guest") { + if name.contains("second") { + "323e4567-e89b-42d3-a456-426614174002" + } else { + "223e4567-e89b-42d3-a456-426614174001" + } + } else { + "123e4567-e89b-42d3-a456-426614174000" + }; + let value = serde_json::json!({ + "apiVersion": "resources.d2bus.org/v3", + "type": "Process", + "metadata": { + "name": name, + "zone": "dev", + "uid": uid, + "generation": 1, + "revision": revision, + "ownerRef": null, + "finalizers": [], + "deletionRequestedAt": null, + "createdAt": "2026-07-22T00:00:00.000Z", + "updatedAt": "2026-07-22T00:00:00.000Z", + "managedBy": "api", + "configurationGeneration": null, + "controllerGeneration": null, + "providerGeneration": null + }, + "spec": { + "providerRef": "Provider/provider-runtime", + "executionRef": execution_ref, + "argv": ["true"] + }, + "status": { + "completedAt": null, + "conditions": [], + "lastReconciledAt": null, + "observedGeneration": 0, + "outcome": null, + "phase": "Pending", + "resource": {}, + "startedAt": null, + "update": { + "dependencies": {"count": 0, "refs": []}, + "disruption": "None", + "lastAssessedAt": null, + "observedGeneration": 0, + "operationId": null, + "owned": {"count": 0, "refs": []}, + "preserveState": true, + "reasons": [], + "state": "Unknown", + "targetGeneration": 1 + } + } + }); + ResourceEnvelope::from_json(&serde_json::to_vec(&value).unwrap()).unwrap() + } + + fn role() -> ControllerRoleContract { + ControllerRoleContract::from_signed_manifest( + ResourceRef::parse("Provider/provider-runtime").unwrap(), + ResourceRef::parse("Process/process-controller").unwrap(), + &manifest(), + ) + .unwrap() + } + + fn request<'a>( + resource: &'a ResourceEnvelope, + role: &'a ControllerRoleContract, + provider_generation: u64, + controller_generation: u64, + session_generation: u64, + ) -> AssignmentRequest<'a> { + AssignmentRequest::new( + resource, + role, + ResourceGeneration::new(provider_generation).unwrap(), + ControllerGeneration::new(controller_generation).unwrap(), + ReconnectGeneration::new(session_generation).unwrap(), + true, + ) + } + + #[test] + fn host_and_guest_resources_have_one_disjoint_target_assignment() { + let host = process("host-process", "Host/host-system", 11); + let guest = process("guest-process", "Guest/dev-vm", 12); + let guest_second = process("guest-process-second", "Guest/dev-vm", 13); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let host_lease = registry.admit(request(&host, &role, 2, 3, 4)).unwrap(); + let guest_lease = registry.admit(request(&guest, &role, 2, 3, 5)).unwrap(); + let guest_second_lease = registry + .admit(request(&guest_second, &role, 2, 3, 6)) + .unwrap(); + + assert_ne!( + host_lease.identity().target(), + guest_lease.identity().target() + ); + assert_eq!( + guest_lease.identity().target(), + guest_second_lease.identity().target() + ); + assert_ne!( + host_lease.identity().epoch(), + guest_lease.identity().epoch() + ); + assert_eq!(host_lease.phase(), AssignmentPhase::Assigned); + assert_eq!(guest_lease.phase(), AssignmentPhase::Assigned); + assert_eq!( + host_lease.target(), + &AssignmentTarget::Execution { + kind: d2b_contracts_resource::v3::PlacementTargetKind::Host, + reference: ResourceRef::parse("Host/host-system").unwrap(), + } + ); + } + + #[test] + fn stale_assignment_epoch_rejects_status_and_finalizer_writers() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let old = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + registry.begin_drain(old.identity()).unwrap(); + registry.release(old.identity()).unwrap(); + let new = registry.admit(request(&resource, &role, 1, 1, 2)).unwrap(); + + assert_eq!( + registry.validate_writer( + old.identity(), + &resource.metadata().uid().clone(), + resource.metadata().revision(), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::StaleAssignment) + ); + assert!( + registry + .validate_writer( + new.identity(), + &resource.metadata().uid().clone(), + resource.metadata().revision(), + AssignmentVerb::UpdateFinalizers, + ) + .is_ok() + ); + } + + #[test] + fn scoped_commit_transport_round_trips_assignment_and_mutations() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + let target = ResourceRef::new( + resource.resource_type().clone(), + resource.metadata().name().clone(), + ); + let mutation = lease + .mutation(target, AssignmentVerb::UpdateStatus) + .unwrap(); + let transport = + ScopedCommitTransport::new(lease.identity().clone(), vec![mutation]).unwrap(); + let decoded = ScopedCommitTransport::decode(&transport.encode().unwrap()).unwrap(); + + assert_eq!(decoded.assignment(), lease.identity()); + assert_eq!(decoded.mutations(), transport.mutations()); + } + + #[test] + fn same_epoch_rebind_updates_the_active_writer_revision() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let mut lease = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + let stale = lease.identity().clone(); + + registry + .rebind_revision(&mut lease, ZoneRevision::new(8)) + .unwrap(); + + assert_eq!(lease.identity().resource_revision(), ZoneRevision::new(8)); + assert!( + registry + .validate_writer( + lease.identity(), + resource.metadata().uid(), + ZoneRevision::new(8), + AssignmentVerb::UpdateStatus, + ) + .is_ok() + ); + assert_eq!( + registry.validate_writer( + &stale, + resource.metadata().uid(), + ZoneRevision::new(7), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::StaleAssignment) + ); + } + + #[test] + fn released_assignment_allows_successor_at_the_current_revision() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let mut old = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + registry + .rebind_revision(&mut old, ZoneRevision::new(8)) + .unwrap(); + registry.begin_drain(old.identity()).unwrap(); + registry.release(old.identity()).unwrap(); + + let current = process("process", "Guest/dev-vm", 8); + let successor = registry.admit(request(¤t, &role, 2, 2, 2)).unwrap(); + assert_eq!( + successor.identity().resource_revision(), + ZoneRevision::new(8) + ); + assert!( + registry + .validate_writer( + successor.identity(), + current.metadata().uid(), + ZoneRevision::new(8), + AssignmentVerb::UpdateFinalizers, + ) + .is_ok() + ); + } + + #[test] + fn disconnected_session_revokes_mutation_but_keeps_stale_observation() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry.admit(request(&resource, &role, 1, 1, 9)).unwrap(); + registry.revoke_session(ReconnectGeneration::new(9).unwrap()); + + assert_eq!( + registry.phase(lease.identity()), + Some(AssignmentPhase::Revoked) + ); + assert_eq!(lease.phase(), AssignmentPhase::Revoked); + assert_eq!( + lease.mutation( + ResourceRef::parse("Process/process").unwrap(), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::SessionRevoked) + ); + assert_eq!( + registry.validate_writer( + lease.identity(), + resource.metadata().uid(), + resource.metadata().revision(), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::SessionRevoked) + ); + assert!(registry.observation_is_stale(lease.identity())); + } + + #[test] + fn guest_lease_cannot_widen_to_host_or_foreign_resource() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + let query = lease + .query( + vec![ResourceTypeName::parse("Process").unwrap()], + vec![], + vec![ + ScopedResourceFilter::narrow("metadata.name", vec!["process".to_owned()]) + .unwrap(), + ], + ) + .unwrap(); + assert!( + query + .filters() + .iter() + .any(|filter| filter.field() == "metadata.name") + ); + assert!( + query + .filters() + .iter() + .any(|filter| filter.field() == "assignment.resourceUid" + && filter.assignment_bound()) + ); + assert_eq!( + lease.query( + vec![ResourceTypeName::parse("Host").unwrap()], + vec![], + vec![], + ), + Err(AssignmentError::QueryWidened) + ); + assert_eq!( + lease.mutation( + ResourceRef::parse("Process/other").unwrap(), + AssignmentVerb::UpdateStatus, + ), + Err(AssignmentError::ResourceNotAssigned) + ); + assert_eq!( + lease.target_for(PlacementTarget::Execution { + kind: d2b_contracts_resource::v3::PlacementTargetKind::Host, + reference: ResourceRef::parse("Host/host-system").unwrap(), + }), + Err(AssignmentError::TargetMismatch) + ); + } + + #[test] + fn target_handoff_requires_drain_and_release_before_reassignment() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let old = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + assert_eq!( + registry + .admit(request(&resource, &role, 2, 2, 2)) + .unwrap_err(), + AssignmentError::AssignmentConflict + ); + registry.begin_drain(old.identity()).unwrap(); + assert_eq!( + registry + .admit(request(&resource, &role, 2, 2, 2)) + .unwrap_err(), + AssignmentError::AssignmentConflict + ); + registry.release(old.identity()).unwrap(); + let replacement = registry.admit(request(&resource, &role, 2, 2, 2)).unwrap(); + assert_eq!(replacement.identity().provider_generation().get(), 2); + assert_eq!(replacement.identity().controller_generation().get(), 2); + } + + #[test] + fn child_index_must_drain_before_parent_release() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + let lease = registry.admit(request(&resource, &role, 1, 1, 1)).unwrap(); + let child = ResourceUid::parse("423e4567-e89b-42d3-a456-426614174003").unwrap(); + registry + .record_child(lease.identity(), child.clone()) + .unwrap(); + registry.begin_drain(lease.identity()).unwrap(); + assert_eq!( + registry.release(lease.identity()), + Err(AssignmentError::ChildrenRemain) + ); + assert_eq!(registry.child_uids(lease.identity()).unwrap().len(), 1); + registry.remove_child(lease.identity(), &child).unwrap(); + registry.release(lease.identity()).unwrap(); + } + + #[test] + fn ambiguous_or_unready_targets_fail_closed_without_fallback() { + let resource = process("process", "Guest/dev-vm", 7); + let role = role(); + let mut registry = ControllerAssignmentRegistry::default(); + assert_eq!( + registry + .admit(AssignmentRequest::new( + &resource, + &role, + ResourceGeneration::new(1).unwrap(), + ControllerGeneration::new(1).unwrap(), + ReconnectGeneration::new(1).unwrap(), + false, + )) + .unwrap_err(), + AssignmentError::TargetNotReady + ); + let invalid = process("process", "User/not-a-target", 7); + assert_eq!( + registry + .admit(request(&invalid, &role, 1, 1, 1)) + .unwrap_err(), + AssignmentError::PlacementTargetInvalid + ); + } +} diff --git a/packages/d2b-core-controller/src/controllers.rs b/packages/d2b-core-controller/src/controllers.rs index 36a35b6fce..d1ff81412b 100644 --- a/packages/d2b-core-controller/src/controllers.rs +++ b/packages/d2b-core-controller/src/controllers.rs @@ -3,11 +3,7 @@ use std::collections::BTreeMap; use d2b_contracts_resource::v3::resource_status::MAX_STATUS_COLLECTION_ENTRIES; -use d2b_contracts_resource::v3::{ - ResourceCurrencySet, - ResourceRef, - UpdateState, -}; +use d2b_contracts_resource::v3::{ResourceCurrencySet, ResourceRef, UpdateState}; /// Closed fixed core handler set. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/packages/d2b-core-controller/src/coordinator.rs b/packages/d2b-core-controller/src/coordinator.rs index bcdaab3415..914b5e1389 100644 --- a/packages/d2b-core-controller/src/coordinator.rs +++ b/packages/d2b-core-controller/src/coordinator.rs @@ -7,10 +7,7 @@ use std::collections::{BTreeMap, btree_map::Entry}; -use d2b_contracts_resource::v3::{ - ResourceBundleGenerationId, - ZoneId, -}; +use d2b_contracts_resource::v3::{ResourceBundleGenerationId, ZoneId}; /// Closed failure from the per-Zone coordinator. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/packages/d2b-core-controller/src/dependencies.rs b/packages/d2b-core-controller/src/dependencies.rs index 975976dfc5..fa126b0847 100644 --- a/packages/d2b-core-controller/src/dependencies.rs +++ b/packages/d2b-core-controller/src/dependencies.rs @@ -2,10 +2,7 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; -use d2b_contracts_resource::v3::{ - ResourceRef, - ZoneRevision, -}; +use d2b_contracts_resource::v3::{ResourceRef, ZoneRevision}; use crate::hints::{ControllerLeaseKey, CoreTriggerReason, HintTarget}; @@ -325,10 +322,7 @@ impl std::error::Error for DependencyError {} #[cfg(test)] mod tests { - use d2b_contracts_resource::v3::{ - ResourceUid, - ZoneId, -}; + use d2b_contracts_resource::v3::{ResourceUid, ZoneId}; use super::*; diff --git a/packages/d2b-core-controller/src/emergency_policy.rs b/packages/d2b-core-controller/src/emergency_policy.rs index fa4af7f03a..1cfeff0b33 100644 --- a/packages/d2b-core-controller/src/emergency_policy.rs +++ b/packages/d2b-core-controller/src/emergency_policy.rs @@ -6,15 +6,9 @@ use std::collections::BTreeMap; +use d2b_contracts_resource::v3::{ResourceUid, Timestamp}; use d2b_contracts_zone_session::v3::{ - EmergencyPolicySpec, - EmergencyPolicyStatusResource, - EmergencyScope, - effective_scope, -}; -use d2b_contracts_resource::v3::{ - ResourceUid, - Timestamp, + EmergencyPolicySpec, EmergencyPolicyStatusResource, EmergencyScope, effective_scope, }; use super::{ @@ -354,8 +348,8 @@ pub type EmergencyPolicyHandler = EmergencyPolicyAuthority; #[cfg(test)] mod tests { use super::*; + use d2b_contracts_resource::v3::ResourceGeneration; use d2b_contracts_zone_session::v3::EmergencyScope; -use d2b_contracts_resource::v3::ResourceGeneration; fn uid(value: &str) -> ResourceUid { ResourceUid::parse(value).unwrap() diff --git a/packages/d2b-core-controller/src/export_import.rs b/packages/d2b-core-controller/src/export_import.rs index 6d63e438ef..eed1321e45 100644 --- a/packages/d2b-core-controller/src/export_import.rs +++ b/packages/d2b-core-controller/src/export_import.rs @@ -8,23 +8,13 @@ //! implemented here. use d2b_contracts_provider::v3::{ - BindingTargetType, - Exportability, - ProjectionFactory, - ProviderContractError, - SEMANTIC_PROJECTION_PROTOCOL_VERSION, - SemanticProjectionProtocolVersion, -}; -use d2b_contracts_zone_session::v3::{ - ResourceExportSpec, - ResourceImportSpec, + BindingTargetType, Exportability, ProjectionFactory, ProviderContractError, + SEMANTIC_PROJECTION_PROTOCOL_VERSION, SemanticProjectionProtocolVersion, }; use d2b_contracts_resource::v3::{ - ResourceEnvelope, - ResourceRef, - ResourceTypeName, - SchemaFingerprint, + ResourceEnvelope, ResourceRef, ResourceTypeName, SchemaFingerprint, }; +use d2b_contracts_zone_session::v3::{ResourceExportSpec, ResourceImportSpec}; /// Why Core refused an export or import admission. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -429,22 +419,13 @@ pub fn projection_identity( #[cfg(test)] mod tests { - use d2b_contracts_provider::v3::{ - BindingTargetType, - Exportability, -}; + use d2b_contracts_provider::v3::{BindingTargetType, Exportability}; + use d2b_contracts_resource::v3::{ + ResourceName, ResourceTypeName, SchemaFingerprint, execution_policy::BoundedToken, + }; use d2b_contracts_zone_session::v3::{ - ConsumerZonePolicy, - ExportArbitration, - ResourceExportSpec, - ResourceImportSpec, -}; -use d2b_contracts_resource::v3::{ - ResourceName, - ResourceTypeName, - SchemaFingerprint, - execution_policy::BoundedToken, -}; + ConsumerZonePolicy, ExportArbitration, ResourceExportSpec, ResourceImportSpec, + }; use super::*; diff --git a/packages/d2b-core-controller/src/export_import_projection.rs b/packages/d2b-core-controller/src/export_import_projection.rs index 86c708931b..7c42eb2b8c 100644 --- a/packages/d2b-core-controller/src/export_import_projection.rs +++ b/packages/d2b-core-controller/src/export_import_projection.rs @@ -9,15 +9,9 @@ use std::collections::BTreeSet; use d2b_contracts_provider::v3::SemanticProjectionProtocolVersion; +use d2b_contracts_resource::v3::{FinalizerId, ResourceRef, ResourceTypeName, SchemaFingerprint}; use d2b_contracts_zone_session::v3::{ - RESOURCE_IMPORT_DRAIN_FINALIZER, - ResourceImportConditionType, -}; -use d2b_contracts_resource::v3::{ - FinalizerId, - ResourceRef, - ResourceTypeName, - SchemaFingerprint, + RESOURCE_IMPORT_DRAIN_FINALIZER, ResourceImportConditionType, }; use crate::export_import::{AdmittedImport, ProjectionServiceIdentity}; @@ -912,22 +906,14 @@ impl core::fmt::Debug for ProjectionController { #[cfg(test)] mod tests { use d2b_contracts_provider::v3::{ - BindingTargetType, - Exportability, - SemanticProjectionProtocolVersion, -}; + BindingTargetType, Exportability, SemanticProjectionProtocolVersion, + }; + use d2b_contracts_resource::v3::{ + ResourceName, ResourceTypeName, SchemaFingerprint, execution_policy::BoundedToken, + }; use d2b_contracts_zone_session::v3::{ - ConsumerZonePolicy, - ExportArbitration, - ResourceExportSpec, - ResourceImportSpec, -}; -use d2b_contracts_resource::v3::{ - ResourceName, - ResourceTypeName, - SchemaFingerprint, - execution_policy::BoundedToken, -}; + ConsumerZonePolicy, ExportArbitration, ResourceExportSpec, ResourceImportSpec, + }; use super::*; use crate::export_import::admit_import; diff --git a/packages/d2b-core-controller/src/hints.rs b/packages/d2b-core-controller/src/hints.rs index 28e63977ee..4ebe489019 100644 --- a/packages/d2b-core-controller/src/hints.rs +++ b/packages/d2b-core-controller/src/hints.rs @@ -3,13 +3,8 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; use d2b_contracts_resource::v3::{ - ControllerGeneration, - ObservedGeneration, - ResourceGeneration, - ResourceRef, - ResourceTypeName, - ZoneId, - ZoneRevision, + ControllerGeneration, ObservedGeneration, ResourceGeneration, ResourceRef, ResourceTypeName, + ZoneId, ZoneRevision, }; pub use d2b_controller_toolkit::{ diff --git a/packages/d2b-core-controller/src/lib.rs b/packages/d2b-core-controller/src/lib.rs index fd10b74c84..3b4bb4f3d0 100644 --- a/packages/d2b-core-controller/src/lib.rs +++ b/packages/d2b-core-controller/src/lib.rs @@ -15,6 +15,7 @@ pub mod authz_audit; pub mod budgets; pub mod cleanup; pub mod configuration; +pub mod controller_assignment; pub mod controllers; pub mod coordinator; pub mod dependencies; @@ -40,6 +41,12 @@ pub mod zone_links; pub mod zone_status; pub mod zonelink; +pub use controller_assignment::{ + AssignmentEpoch, AssignmentError, AssignmentIdentity, AssignmentPhase, AssignmentRequest, + AssignmentTarget, AssignmentTransportError, AssignmentVerb, ControllerAssignmentRegistry, + ControllerRoleContract, MAX_SCOPED_COMMIT_TRANSPORT_BYTES, ResourceClientLease, + ScopedCommitTransport, ScopedResourceFilter, ScopedResourceMutation, ScopedResourceQuery, +}; pub use controllers::{ AggregateHealth, CoreHandlerKind, CoreHandlerRegistry, CurrencyAggregation, CurrencyAggregationError, HandlerOutcome, HandlerPhase, HandlerStatus, diff --git a/packages/d2b-core-controller/src/optional_state_admission.rs b/packages/d2b-core-controller/src/optional_state_admission.rs index 2f4403cec0..051fe513b7 100644 --- a/packages/d2b-core-controller/src/optional_state_admission.rs +++ b/packages/d2b-core-controller/src/optional_state_admission.rs @@ -5,11 +5,7 @@ //! or an effect. A later dispatcher must consume this result before it creates //! a declared Volume or launches the component. -use d2b_contracts_provider::v3::{ - ComponentDescriptor, - ComponentStateNamespace, - StorageNeed, -}; +use d2b_contracts_provider::v3::{ComponentDescriptor, ComponentStateNamespace, StorageNeed}; /// Whether one declared payload genuinely requires a state Volume. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -169,17 +165,11 @@ mod tests { use std::collections::BTreeMap; use d2b_contracts_provider::v3::{ - ArtifactDigest, - ComponentStateKind, - ComponentStateNamespace, - ComponentStateView, - ComponentType, - StatePlacementMode, + ArtifactDigest, ComponentStateKind, ComponentStateNamespace, ComponentStateView, + ComponentType, StatePlacementMode, }; use d2b_contracts_resource::v3::{ - ResourceTypeName, - SchemaFingerprint, - SchemaVersion, + ResourceTypeName, SchemaFingerprint, SchemaVersion, execution_policy::{BoundedToken, ExecutionDomain}, volume::ViewRight, volume_state::{MigrationPolicy, PersistenceClass, SensitivityClass, VolumeStateSchemaId}, diff --git a/packages/d2b-core-controller/src/owner_reconcile.rs b/packages/d2b-core-controller/src/owner_reconcile.rs index deb30ec665..75a12f1553 100644 --- a/packages/d2b-core-controller/src/owner_reconcile.rs +++ b/packages/d2b-core-controller/src/owner_reconcile.rs @@ -2,11 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_resource::v3::{ - ResourceRef, - ResourceUid, - ZoneRevision, -}; +use d2b_contracts_resource::v3::{ResourceRef, ResourceUid, ZoneRevision}; use crate::hints::HintTarget; @@ -513,10 +509,7 @@ impl std::error::Error for OwnerGraphError {} #[cfg(test)] mod tests { - use d2b_contracts_resource::v3::{ - ResourceUid, - ZoneId, -}; + use d2b_contracts_resource::v3::{ResourceUid, ZoneId}; use super::*; diff --git a/packages/d2b-core-controller/src/ownership.rs b/packages/d2b-core-controller/src/ownership.rs index 2739b04039..689c3043fc 100644 --- a/packages/d2b-core-controller/src/ownership.rs +++ b/packages/d2b-core-controller/src/ownership.rs @@ -2,10 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_resource::v3::{ - FinalizerId, - ResourceRef, -}; +use d2b_contracts_resource::v3::{FinalizerId, ResourceRef}; use d2b_controller_toolkit::ResourceKey; use crate::audit::{AuditEvent, resource_name_digest}; @@ -384,10 +381,7 @@ pub fn commit_atomic_deletion( #[cfg(test)] mod tests { - use d2b_contracts_resource::v3::{ - ResourceUid, - ZoneId, -}; + use d2b_contracts_resource::v3::{ResourceUid, ZoneId}; use super::*; @@ -531,11 +525,8 @@ mod tests { timestamp.clone(), ); assert_eq!( - commit_atomic_deletion( - blocked, - d2b_contracts_resource::v3::ZoneRevision::new(4) - ) - .unwrap_err(), + commit_atomic_deletion(blocked, d2b_contracts_resource::v3::ZoneRevision::new(4)) + .unwrap_err(), AtomicDeletionError::FinalizersRemain ); let committed = commit_atomic_deletion( diff --git a/packages/d2b-core-controller/src/providers.rs b/packages/d2b-core-controller/src/providers.rs index b5d4deece5..caca608ab9 100644 --- a/packages/d2b-core-controller/src/providers.rs +++ b/packages/d2b-core-controller/src/providers.rs @@ -1,19 +1,10 @@ //! Provider lifecycle validation and child-resource planning. use d2b_contracts_provider::v3::{ - BinaryRef, - ComponentExecution, - ComponentTargetCapability, - ControllerTargetKind, - EffectPortClass, - ComponentType, - ProviderManifest, - TargetRuntimeArtifacts, -}; -use d2b_contracts_resource::v3::{ - ResourceRef, - SchemaFingerprint, + BinaryRef, ComponentExecution, ComponentTargetCapability, ComponentType, ControllerTargetKind, + EffectPortClass, ProviderManifest, TargetRuntimeArtifacts, }; +use d2b_contracts_resource::v3::{ResourceRef, SchemaFingerprint}; /// Provider lifecycle phase derived from exact child observations. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -219,24 +210,15 @@ impl ProviderHandler { #[cfg(test)] mod tests { + use d2b_contracts_provider::v3::UpgradePolicy as ProviderUpgradePolicy; use d2b_contracts_provider::v3::{ - UpgradePolicy as ProviderUpgradePolicy, -}; - use d2b_contracts_provider::v3::{ - ArtifactDigest, - ArtifactDigestSet, - CompatibilityRange, - ComponentDescriptor, - PolicyEvaluation, - RevocationState, - SignatureState, - TrustEvidence, - UpgradeDisposition, -}; -use d2b_contracts_resource::v3::{ - ArtifactId, - execution_policy::{BoundedToken, ExecutionDomain}, -}; + ArtifactDigest, ArtifactDigestSet, CompatibilityRange, ComponentDescriptor, + PolicyEvaluation, RevocationState, SignatureState, TrustEvidence, UpgradeDisposition, + }; + use d2b_contracts_resource::v3::{ + ArtifactId, + execution_policy::{BoundedToken, ExecutionDomain}, + }; use super::*; @@ -276,8 +258,7 @@ use d2b_contracts_resource::v3::{ api_major: 3, api_minor: 0, descriptor_fingerprint: fingerprint(), - state_schema_version: d2b_contracts_resource::v3::SchemaVersion::new(1, 0) - .unwrap(), + state_schema_version: d2b_contracts_resource::v3::SchemaVersion::new(1, 0).unwrap(), }, [ComponentDescriptor::new( BoundedToken::parse("service").unwrap(), diff --git a/packages/d2b-core-controller/src/quota.rs b/packages/d2b-core-controller/src/quota.rs index 5cc38b8819..f070560f08 100644 --- a/packages/d2b-core-controller/src/quota.rs +++ b/packages/d2b-core-controller/src/quota.rs @@ -8,13 +8,8 @@ use std::collections::BTreeMap; use d2b_contracts_resource::v3::{ - QuotaEnforcementPolicy, - QuotaSpec, - QuotaStatusResource, - ResourceRef, - ResourceTypeName, - ResourceUid, - Timestamp, + QuotaEnforcementPolicy, QuotaSpec, QuotaStatusResource, ResourceRef, ResourceTypeName, + ResourceUid, Timestamp, }; use super::{ @@ -494,11 +489,8 @@ mod tests { use super::*; use d2b_contracts_resource::v3::{ - QuotaCeilings, - QuotaScope, - QuotaTypeCeiling, - ResourceGeneration, -}; + QuotaCeilings, QuotaScope, QuotaTypeCeiling, ResourceGeneration, + }; fn uid(value: &str) -> ResourceUid { ResourceUid::parse(value).unwrap() diff --git a/packages/d2b-core-controller/src/rbac.rs b/packages/d2b-core-controller/src/rbac.rs index eb3624009f..0704b64b45 100644 --- a/packages/d2b-core-controller/src/rbac.rs +++ b/packages/d2b-core-controller/src/rbac.rs @@ -5,12 +5,7 @@ use std::{ sync::{Mutex, MutexGuard}, }; -use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceRef, - ResourceUid, - ZoneRevision, -}; +use d2b_contracts_resource::v3::{ConfigurationGeneration, ResourceRef, ResourceUid, ZoneRevision}; /// Policy revisions that make one positive decision valid. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] diff --git a/packages/d2b-core-controller/src/runtime.rs b/packages/d2b-core-controller/src/runtime.rs index 9617ffce6e..0b22e32b02 100644 --- a/packages/d2b-core-controller/src/runtime.rs +++ b/packages/d2b-core-controller/src/runtime.rs @@ -546,16 +546,9 @@ mod tests { use std::collections::{BTreeMap, BTreeSet, VecDeque}; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ControllerGeneration, - ObservedGeneration, - ResourceGeneration, - ResourcePhase, - ResourceRef, - ResourceTypeName, - ResourceUid, - ZoneId, -}; + ConfigurationGeneration, ControllerGeneration, ObservedGeneration, ResourceGeneration, + ResourcePhase, ResourceRef, ResourceTypeName, ResourceUid, ZoneId, + }; use d2b_controller_toolkit::{ ControllerExecutionPolicy, ControllerIdentity, ControllerSelector, ControllerVerb, ProjectionDisposition, ResourceRegistration, ResyncPolicy, Runner, RunnerConfig, diff --git a/packages/d2b-core-controller/src/zone_links.rs b/packages/d2b-core-controller/src/zone_links.rs index abcafe3d0d..7c455f1155 100644 --- a/packages/d2b-core-controller/src/zone_links.rs +++ b/packages/d2b-core-controller/src/zone_links.rs @@ -30,10 +30,10 @@ //! the session state purely from the durable [`ZoneLinkRecord`]. Replaying an //! already-committed event after restart is a no-op that plans no effect. -use d2b_contracts_zone_session::v3::{ - zone_routing::{ZoneLinkControllerGeneration, ZoneRouteFailClosedReason, ZoneSigningKeyFingerprint}, -}; use d2b_contracts_resource::v3::ResourceUid; +use d2b_contracts_zone_session::v3::zone_routing::{ + ZoneLinkControllerGeneration, ZoneRouteFailClosedReason, ZoneSigningKeyFingerprint, +}; /// Default absolute lifetime of one allocator-issued bootstrap PSK. pub const BOOTSTRAP_PSK_TTL_MS_DEFAULT: u64 = 300_000; diff --git a/packages/d2b-core-controller/src/zone_status.rs b/packages/d2b-core-controller/src/zone_status.rs index 5e10c405e7..8726536eb2 100644 --- a/packages/d2b-core-controller/src/zone_status.rs +++ b/packages/d2b-core-controller/src/zone_status.rs @@ -1,15 +1,8 @@ //! Production Zone status projection with the mandatory system-core pair. +use d2b_contracts_resource::v3::{ResourcePhase, Timestamp}; use d2b_contracts_zone_session::v3::ZoneHandlerName; -use d2b_contracts_zone_session::v3::{ - ZoneHandlerPhase, - ZoneHandlerStatus, - ZoneStatusResource, -}; -use d2b_contracts_resource::v3::{ - ResourcePhase, - Timestamp, -}; +use d2b_contracts_zone_session::v3::{ZoneHandlerPhase, ZoneHandlerStatus, ZoneStatusResource}; fn emit_handler_status( host_phase: ZoneHandlerPhase, diff --git a/packages/d2b-resource-api/src/adapter.rs b/packages/d2b-resource-api/src/adapter.rs index fff9ec17b6..f74dc5511f 100644 --- a/packages/d2b-resource-api/src/adapter.rs +++ b/packages/d2b-resource-api/src/adapter.rs @@ -7,6 +7,13 @@ use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; use d2b_contracts_resource::resource_proto as wire; +use d2b_core_controller::controller_assignment::{ + AssignmentTransportError, ScopedCommitTransport, ScopedResourceMutation, +}; +use protobuf::Message; +use ttrpc::proto::{ + MESSAGE_HEADER_LENGTH, MESSAGE_TYPE_REQUEST, MessageHeader, Request as TtrpcRequest, +}; use crate::{ ResourceStoreBackend, @@ -17,6 +24,115 @@ use crate::{ service::{ResourceService, TrustedRequest, UpgradeDispatcher}, }; +/// Failure while attaching assignment evidence to a Resource CommitBatch +/// frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScopedCommitFrameError { + InvalidFrame, + InvalidRequest, + Assignment(AssignmentTransportError), +} + +impl core::fmt::Display for ScopedCommitFrameError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(match self { + Self::InvalidFrame => "scoped-commit-frame-invalid", + Self::InvalidRequest => "scoped-commit-request-invalid", + Self::Assignment(error) => error.code(), + }) + } +} + +impl std::error::Error for ScopedCommitFrameError {} + +/// Attach bus-admitted assignment evidence to the existing ttrpc CommitBatch +/// request without creating another transport. +pub fn attach_scoped_commit_frame( + frame: &[u8], + transport: &ScopedCommitTransport, +) -> Result, ScopedCommitFrameError> { + let header_bytes: [u8; MESSAGE_HEADER_LENGTH] = frame + .get(..MESSAGE_HEADER_LENGTH) + .ok_or(ScopedCommitFrameError::InvalidFrame)? + .try_into() + .map_err(|_| ScopedCommitFrameError::InvalidFrame)?; + let header = MessageHeader::from(header_bytes); + let body_len = + usize::try_from(header.length).map_err(|_| ScopedCommitFrameError::InvalidFrame)?; + if header.type_ != MESSAGE_TYPE_REQUEST + || frame.len() != MESSAGE_HEADER_LENGTH.saturating_add(body_len) + { + return Err(ScopedCommitFrameError::InvalidFrame); + } + let mut rpc = TtrpcRequest::parse_from_bytes(&frame[MESSAGE_HEADER_LENGTH..]) + .map_err(|_| ScopedCommitFrameError::InvalidRequest)?; + if rpc.service != "d2b.resource.v3.ResourceService" || rpc.method != "CommitBatch" { + return Err(ScopedCommitFrameError::InvalidRequest); + } + let mut request = wire::CommitBatchRequest::parse_from_bytes(&rpc.payload) + .map_err(|_| ScopedCommitFrameError::InvalidRequest)?; + if !request.scoped_admission.is_empty() { + return Err(ScopedCommitFrameError::InvalidRequest); + } + request.scoped_admission = transport + .encode() + .map_err(ScopedCommitFrameError::Assignment)?; + rpc.payload = request + .write_to_bytes() + .map_err(|_| ScopedCommitFrameError::InvalidRequest)?; + let body = rpc + .write_to_bytes() + .map_err(|_| ScopedCommitFrameError::InvalidRequest)?; + let mut header = header; + header.length = u32::try_from(body.len()).map_err(|_| ScopedCommitFrameError::InvalidFrame)?; + let mut result = Vec::with_capacity(MESSAGE_HEADER_LENGTH + body.len()); + result.extend_from_slice(&Vec::from(header)); + result.extend_from_slice(&body); + Ok(result) +} + +/// Decode the optional assignment evidence carried by a CommitBatch request. +pub fn decode_scoped_commit_request( + request: &wire::CommitBatchRequest, +) -> Result, AssignmentTransportError> { + if request.scoped_admission.is_empty() { + return Ok(None); + } + ScopedCommitTransport::decode(&request.scoped_admission).map(Some) +} + +/// Reject assignment evidence supplied by an ordinary CommitBatch caller. +/// +/// Scoped evidence is bus-owned. A plain ResourceCall must never be able to +/// smuggle the field through the same RPC and receive a storage fence. +pub fn reject_scoped_commit_frame(frame: &[u8]) -> Result<(), ScopedCommitFrameError> { + let header_bytes: [u8; MESSAGE_HEADER_LENGTH] = frame + .get(..MESSAGE_HEADER_LENGTH) + .ok_or(ScopedCommitFrameError::InvalidFrame)? + .try_into() + .map_err(|_| ScopedCommitFrameError::InvalidFrame)?; + let header = MessageHeader::from(header_bytes); + let body_len = + usize::try_from(header.length).map_err(|_| ScopedCommitFrameError::InvalidFrame)?; + if header.type_ != MESSAGE_TYPE_REQUEST + || frame.len() != MESSAGE_HEADER_LENGTH.saturating_add(body_len) + { + return Err(ScopedCommitFrameError::InvalidFrame); + } + let rpc = TtrpcRequest::parse_from_bytes(&frame[MESSAGE_HEADER_LENGTH..]) + .map_err(|_| ScopedCommitFrameError::InvalidRequest)?; + if rpc.service != "d2b.resource.v3.ResourceService" || rpc.method != "CommitBatch" { + return Err(ScopedCommitFrameError::InvalidRequest); + } + let request = wire::CommitBatchRequest::parse_from_bytes(&rpc.payload) + .map_err(|_| ScopedCommitFrameError::InvalidRequest)?; + if request.scoped_admission.is_empty() { + Ok(()) + } else { + Err(ScopedCommitFrameError::InvalidRequest) + } +} + /// Failure to bind an authenticated ComponentSession route. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AdapterBindingError; @@ -73,6 +189,17 @@ where ) } + /// Dispatch one scoped commit through the existing authenticated adapter. + pub async fn scoped_commit_batch( + &self, + request: wire::CommitBatchRequest, + scoped_mutations: Vec, + ) -> wire::CommitBatchResponse { + self.client() + .scoped_commit_batch(request, scoped_mutations) + .await + } + pub(crate) fn service(&self) -> &ResourceService { &self.service } @@ -183,7 +310,22 @@ where _ctx: &ttrpc::r#async::TtrpcContext, request: wire::CommitBatchRequest, ) -> ttrpc::Result { - Ok(self.service().commit_batch(self.trusted(request)).await) + let scoped = match decode_scoped_commit_request(&request) { + Ok(scoped) => scoped, + Err(_) => { + return Ok(ResourceService::::invalid_commit_batch( + "scoped commit transport is invalid", + )); + } + }; + Ok(match scoped { + Some(transport) => { + self.client() + .scoped_commit_batch(request, transport.mutations().to_vec()) + .await + } + None => self.service().commit_batch(self.trusted(request)).await, + }) } async fn resolve_ref( @@ -216,34 +358,18 @@ mod tests { use super::*; use std::{collections::BTreeSet, sync::Mutex}; + use d2b_contracts_resource::v3::identity::{ + AuthenticatedSubjectContext as SessionClaims, BindingDigest, EvidenceClass, Locality, + ReconnectGeneration, ServiceName, SessionBinding, SessionPurpose, TranscriptHash, + TransportBinding, + }; use d2b_contracts_resource::v3::{ - CanonicalJsonValue, - ConfigurationGeneration, - ControllerGeneration, - RESOURCE_ENVELOPE_DOMAIN_TAG, - ResourceEnvelope, - ResourceGeneration, - ResourceName, - ResourceRef, - ResourceTypeName, - ResourceUid, - SchemaFingerprint, - ZoneId, - ZoneRevision, - canonical_digest, -}; -use d2b_contracts_resource::v3::identity::{ - AuthenticatedSubjectContext as SessionClaims, - BindingDigest, - EvidenceClass, - Locality, - ReconnectGeneration, - ServiceName, - SessionBinding, - SessionPurpose, - TranscriptHash, - TransportBinding, -}; + CanonicalJsonValue, ConfigurationGeneration, ControllerGeneration, + RESOURCE_ENVELOPE_DOMAIN_TAG, ResourceEnvelope, ResourceGeneration, ResourceName, + ResourceRef, ResourceTypeName, ResourceUid, SchemaFingerprint, ZoneId, ZoneRevision, + canonical_digest, + }; + use d2b_core_controller::controller_assignment::ScopedCommitTransport; use d2b_resource_store::mutation_seal::MutationSealAcceptor; use d2b_resource_store::{ AdmittedVerb, PolicySnapshot, ResourceMutationKind, StoreCommitResult, StoreError, @@ -251,7 +377,8 @@ use d2b_contracts_resource::v3::identity::{ StoreResolveRequest, StoreResolvedIdentity, StoreSealIdentity, StoreSlot, StoreWatchReceipt, StoreWatchRequest, StoredResource, StoredSchema, }; - use protobuf::{EnumOrUnknown, MessageField}; + use protobuf::{EnumOrUnknown, Message, MessageField}; + use ttrpc::proto::{MessageHeader, Request as TtrpcRequest}; use crate::ResourceStoreBackend; use crate::authz::{ @@ -323,6 +450,40 @@ use d2b_contracts_resource::v3::identity::{ ) } + #[test] + fn scoped_commit_frame_preserves_assignment_evidence_on_existing_rpc() { + let transport = ScopedCommitTransport::decode( + br#"{"version":1,"assignment":{"resourceUid":"123e4567-e89b-42d3-a456-426614174000","resourceRevision":7,"providerGeneration":2,"controllerGeneration":3,"controllerRole":"Process/process-controller","target":{"kind":"zone","zone":"dev"},"sessionGeneration":1,"epoch":9},"mutations":[{"target":"Process/work","verb":"UpdateStatus"},{"target":"Process/work","verb":"UpdateFinalizers"}]}"#, + ) + .unwrap(); + let request = d2b_contracts_resource::resource_proto::CommitBatchRequest::new(); + let rpc = TtrpcRequest { + service: "d2b.resource.v3.ResourceService".to_owned(), + method: "CommitBatch".to_owned(), + payload: request.write_to_bytes().unwrap(), + ..TtrpcRequest::default() + }; + let body = rpc.write_to_bytes().unwrap(); + let mut frame = Vec::with_capacity(ttrpc::proto::MESSAGE_HEADER_LENGTH + body.len()); + let mut header = MessageHeader::new_request(1, body.len() as u32); + header.length = body.len() as u32; + frame.extend_from_slice(&Vec::from(header)); + frame.extend_from_slice(&body); + + let attached = attach_scoped_commit_frame(&frame, &transport).unwrap(); + let attached_body = &attached[ttrpc::proto::MESSAGE_HEADER_LENGTH..]; + let attached_rpc = TtrpcRequest::parse_from_bytes(attached_body).unwrap(); + let attached_request = + d2b_contracts_resource::resource_proto::CommitBatchRequest::parse_from_bytes( + &attached_rpc.payload, + ) + .unwrap(); + let decoded = decode_scoped_commit_request(&attached_request) + .unwrap() + .unwrap(); + assert_eq!(decoded, transport); + } + #[derive(Debug, Clone, PartialEq, Eq)] struct ObservedTarget { resource_type: String, diff --git a/packages/d2b-resource-api/src/admission.rs b/packages/d2b-resource-api/src/admission.rs index a216ce719d..a3758779ea 100644 --- a/packages/d2b-resource-api/src/admission.rs +++ b/packages/d2b-resource-api/src/admission.rs @@ -1,10 +1,7 @@ //! Instance-bound admission witnesses owned by the native evaluator. use d2b_contracts_resource::v3::{ - CanonicalJsonValue, - RESOURCE_ENVELOPE_DOMAIN_TAG, - ResourceEnvelope, - RetryClass, + CanonicalJsonValue, RESOURCE_ENVELOPE_DOMAIN_TAG, ResourceEnvelope, RetryClass, canonical_digest, }; use d2b_resource_store::mutation_seal::MutationSealIssuer; @@ -408,14 +405,9 @@ fn preparation_error(reason_code: &'static str) -> StoreError { mod tests { use super::*; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceName, - ResourceRef, - ResourceTypeName, - ResourceUid, - ZoneId, - ZoneRevision, -}; + ConfigurationGeneration, ResourceName, ResourceRef, ResourceTypeName, ResourceUid, ZoneId, + ZoneRevision, + }; fn authorization(zone: &str) -> AdmittedAuthorization { AdmittedAuthorization { @@ -464,6 +456,7 @@ mod tests { remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, } } diff --git a/packages/d2b-resource-api/src/authz.rs b/packages/d2b-resource-api/src/authz.rs index ed9d5e9c83..d72d730184 100644 --- a/packages/d2b-resource-api/src/authz.rs +++ b/packages/d2b-resource-api/src/authz.rs @@ -6,38 +6,23 @@ use std::{ }; use d2b_contracts_resource::v3::identity::STANDARD_RESOURCE_TYPES; +use d2b_contracts_resource::v3::identity::{AuthenticatedSubjectContext, EvidenceClass, Locality}; use d2b_contracts_resource::v3::{ - ControllerGeneration, - MAX_ROLE_BINDING_SUBJECTS, - MAX_ROLE_RULE_EXECUTION_REFS, - MAX_ROLE_RULE_RESOURCE_NAMES, - MAX_ROLE_RULE_RESOURCE_TYPES, - MAX_ROLE_RULE_VERBS, - MAX_ROLE_RULES, - ResourceErrorKind, - ResourceName, - ResourceRef, - ResourceTypeName, - ResourceUid, - ZoneId, - ZoneRevision, -}; -use d2b_contracts_resource::v3::identity::{ - AuthenticatedSubjectContext, - EvidenceClass, - Locality, + ControllerGeneration, MAX_ROLE_BINDING_SUBJECTS, MAX_ROLE_RULE_EXECUTION_REFS, + MAX_ROLE_RULE_RESOURCE_NAMES, MAX_ROLE_RULE_RESOURCE_TYPES, MAX_ROLE_RULE_VERBS, + MAX_ROLE_RULES, ResourceErrorKind, ResourceName, ResourceRef, ResourceTypeName, ResourceUid, + ZoneId, ZoneRevision, }; use d2b_contracts_zone_session::v3::{ - RoleBindingSpec, - RoleResourceVerb, - RoleRule, - RoleSessionVerb, - RoleSpec, + RoleBindingSpec, RoleResourceVerb, RoleRule, RoleSessionVerb, RoleSpec, +}; +use d2b_core_controller::controller_assignment::{ + AssignmentError, AssignmentIdentity, AssignmentTarget, ScopedResourceMutation, }; use d2b_core_controller::rbac::{AuthorizationCacheKey, PolicyRevisionSet, PositiveDecisionCache}; use d2b_resource_store::{ AdmittedAuthorization, AdmittedAuthorizationTarget, AdmittedVerb, PolicySnapshot, - StoreMutation, StoreOperationContext, StoreSealIdentity, StoreSlot, + ResourceAssignmentFence, StoreMutation, StoreOperationContext, StoreSealIdentity, StoreSlot, }; use sha2::{Digest, Sha256}; @@ -946,6 +931,34 @@ pub struct AuthorizationGrant { permit: AdmissionPermit, } +/// Convert Core's assignment identity to the storage-neutral mutation fence. +pub fn assignment_fence( + identity: &AssignmentIdentity, +) -> Result { + let target = match identity.target() { + AssignmentTarget::Zone(zone) => ResourceRef::parse(&format!("Zone/{}", zone.as_str())) + .map_err(|_| AssignmentError::TargetMismatch)?, + AssignmentTarget::Execution { reference, .. } => reference.clone(), + }; + Ok(ResourceAssignmentFence { + resource_uid: identity.resource_uid().clone(), + resource_revision: identity.resource_revision(), + provider_generation: identity.provider_generation(), + controller_generation: identity.controller_generation(), + controller_role: identity.controller_role().clone(), + target, + session_generation: identity.session_generation(), + epoch: identity.epoch().get(), + }) +} + +/// Build a mutation fence from a controller-scoped mutation. +pub fn assignment_fence_for_mutation( + mutation: &ScopedResourceMutation, +) -> Result { + assignment_fence(mutation.assignment()) +} + impl core::fmt::Debug for AuthorizationGrant { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str("AuthorizationGrant()") @@ -1801,20 +1814,13 @@ impl std::error::Error for AuthorizationPolicyError {} #[cfg(test)] mod tests { use super::*; + use d2b_contracts_resource::v3::identity::{ + BindingDigest, ReconnectGeneration, ServiceName, SessionBinding, SessionPurpose, + TranscriptHash, TransportBinding, + }; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceGeneration, - SchemaFingerprint, -}; -use d2b_contracts_resource::v3::identity::{ - BindingDigest, - ReconnectGeneration, - ServiceName, - SessionBinding, - SessionPurpose, - TranscriptHash, - TransportBinding, -}; + ConfigurationGeneration, ResourceGeneration, SchemaFingerprint, + }; fn test_issuer() -> AdmissionIssuer { crate::admission::admission_pair().0 diff --git a/packages/d2b-resource-api/src/client.rs b/packages/d2b-resource-api/src/client.rs index d0d8d1c23b..9baad1efb1 100644 --- a/packages/d2b-resource-api/src/client.rs +++ b/packages/d2b-resource-api/src/client.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use d2b_contracts_resource::resource_proto as wire; use d2b_contracts_resource::v3::identity::AuthenticatedSubjectContext; +use d2b_core_controller::controller_assignment::ScopedResourceMutation; use crate::{ ResourceStoreBackend, @@ -93,6 +94,18 @@ where self.service.commit_batch(self.trusted(request)).await } + /// Commit a batch carrying the bus-admitted assignment fence into every + /// store mutation without opening a second transport. + pub async fn scoped_commit_batch( + &self, + request: wire::CommitBatchRequest, + scoped_mutations: Vec, + ) -> wire::CommitBatchResponse { + self.service + .commit_scoped_batch(self.trusted(request), scoped_mutations) + .await + } + pub async fn resolve_ref(&self, request: wire::ResolveRefRequest) -> wire::ResolveRefResponse { self.service.resolve_ref(self.trusted(request)).await } diff --git a/packages/d2b-resource-api/src/emergency_gate.rs b/packages/d2b-resource-api/src/emergency_gate.rs index 1aae9fdd7f..dde44b628d 100644 --- a/packages/d2b-resource-api/src/emergency_gate.rs +++ b/packages/d2b-resource-api/src/emergency_gate.rs @@ -1,10 +1,6 @@ //! Pure EmergencyPolicy admission and effect signals. -use d2b_contracts_zone_session::v3::{ - EmergencyPolicySpec, - EmergencyScope, - effective_scope, -}; +use d2b_contracts_zone_session::v3::{EmergencyPolicySpec, EmergencyScope, effective_scope}; /// Effective Zone-wide emergency gate. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] diff --git a/packages/d2b-resource-api/src/error.rs b/packages/d2b-resource-api/src/error.rs index a8e48e51cb..a4d025cd6a 100644 --- a/packages/d2b-resource-api/src/error.rs +++ b/packages/d2b-resource-api/src/error.rs @@ -2,10 +2,7 @@ use d2b_contracts_resource::resource_proto as wire; use d2b_contracts_resource::v3::{ - ResourceError, - ResourceErrorKind, - ResourceErrorReason, - RetryClass, + ResourceError, ResourceErrorKind, ResourceErrorReason, RetryClass, }; use d2b_resource_store::{StoreError, StoreErrorKind}; use protobuf::EnumOrUnknown; diff --git a/packages/d2b-resource-api/src/identity.rs b/packages/d2b-resource-api/src/identity.rs index 6e2884eb78..6a2522b62b 100644 --- a/packages/d2b-resource-api/src/identity.rs +++ b/packages/d2b-resource-api/src/identity.rs @@ -2,9 +2,7 @@ use std::sync::Arc; -use d2b_contracts_resource::v3::identity::{ - AuthenticatedSubjectContext as SessionClaims, -}; +use d2b_contracts_resource::v3::identity::AuthenticatedSubjectContext as SessionClaims; use crate::authz::AuthorizationState; @@ -74,24 +72,13 @@ pub(crate) fn issue_test_subject( mod tests { use super::*; use crate::authz::{AuthorizationState, BootstrapPhase}; + use d2b_contracts_resource::v3::identity::{ + BindingDigest, EvidenceClass, Locality, ReconnectGeneration, ServiceName, SessionBinding, + SessionPurpose, TranscriptHash, TransportBinding, + }; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ResourceRef, - ResourceUid, - SchemaFingerprint, - ZoneRevision, -}; -use d2b_contracts_resource::v3::identity::{ - BindingDigest, - EvidenceClass, - Locality, - ReconnectGeneration, - ServiceName, - SessionBinding, - SessionPurpose, - TranscriptHash, - TransportBinding, -}; + ConfigurationGeneration, ResourceRef, ResourceUid, SchemaFingerprint, ZoneRevision, + }; use d2b_resource_store::PolicySnapshot; #[test] diff --git a/packages/d2b-resource-api/src/lib.rs b/packages/d2b-resource-api/src/lib.rs index 019904de2f..5e9eafcd20 100644 --- a/packages/d2b-resource-api/src/lib.rs +++ b/packages/d2b-resource-api/src/lib.rs @@ -20,6 +20,8 @@ pub mod zone_service; pub use adapter::{ AdapterBindingError, RESOURCE_API_REACHABILITY, ResourceApiReachability, ResourceBusAdapter, + ScopedCommitFrameError, attach_scoped_commit_frame, decode_scoped_commit_request, + reject_scoped_commit_frame, }; pub use admission::{AdmissionError, AdmittedMutation}; pub use authz::StoreSealHandoffError; diff --git a/packages/d2b-resource-api/src/quota_gate.rs b/packages/d2b-resource-api/src/quota_gate.rs index 5bba987792..84df96ec25 100644 --- a/packages/d2b-resource-api/src/quota_gate.rs +++ b/packages/d2b-resource-api/src/quota_gate.rs @@ -5,8 +5,7 @@ //! deleted by an authorized caller before a Quota can drain. use d2b_contracts_resource::v3::{ - QuotaSpec, - ResourceTypeName, + QuotaSpec, ResourceTypeName, quota::{QuotaContractError, QuotaEnforcementPolicy}, }; @@ -141,12 +140,8 @@ impl From for QuotaGateError { mod tests { use super::*; use d2b_contracts_resource::v3::{ - QuotaCeilings, - QuotaEnforcementPolicy, - QuotaScope, - QuotaSpec, - QuotaTypeCeiling, -}; + QuotaCeilings, QuotaEnforcementPolicy, QuotaScope, QuotaSpec, QuotaTypeCeiling, + }; use std::collections::BTreeMap; fn quota(policy: QuotaEnforcementPolicy) -> QuotaSpec { diff --git a/packages/d2b-resource-api/src/service.rs b/packages/d2b-resource-api/src/service.rs index 909b9e1280..3eba8b58fb 100644 --- a/packages/d2b-resource-api/src/service.rs +++ b/packages/d2b-resource-api/src/service.rs @@ -35,6 +35,7 @@ use d2b_contracts_resource::v3::{ canonical_digest, }; use d2b_contracts_resource::v3::identity::AuthenticatedSubjectContext; +use d2b_core_controller::controller_assignment::{AssignmentVerb, ScopedResourceMutation}; use d2b_resource_store::{ ExpectedRevision, ResourceMutationKind, StoreCommitResult, StoreFilter, StoreGetRequest, StoreInspectSchemaRequest, StoreListRequest, StoreListResult, StoreMutation, @@ -46,7 +47,7 @@ use crate::{ ResourceStoreBackend, StoreBindingError, authz::{ ApiMethod, AuthorizationRequest, AuthorizationState, AuthorizationTarget, NativeAuthorizer, - ResourceVerb, + ResourceVerb, assignment_fence_for_mutation, }, error::{map_store_error, map_store_error_with_revision_visibility, to_wire_error}, store::CheckedResourceStore, @@ -637,6 +638,29 @@ where pub async fn commit_batch( &self, trusted: TrustedRequest, + ) -> wire::CommitBatchResponse { + self.commit_batch_with_scope(trusted, None).await + } + + pub(crate) fn invalid_commit_batch(reason: &'static str) -> wire::CommitBatchResponse { + batch_error(schema_error(reason)) + } + + /// Commit one bus-authorized assignment batch while carrying the same + /// lease evidence into every eligible store mutation. + pub async fn commit_scoped_batch( + &self, + trusted: TrustedRequest, + scoped_mutations: Vec, + ) -> wire::CommitBatchResponse { + self.commit_batch_with_scope(trusted, Some(scoped_mutations)) + .await + } + + async fn commit_batch_with_scope( + &self, + trusted: TrustedRequest, + scoped_mutations: Option>, ) -> wire::CommitBatchResponse { if trusted.request.mutations.is_empty() { return batch_error(schema_error("batch mutation count exceeds its bound")); @@ -682,7 +706,7 @@ where if trusted.request.mutations.len() > MAX_BATCH_MUTATIONS { return batch_error(schema_error("batch mutation count exceeds its bound")); } - let parsed = match trusted + let mut parsed = match trusted .request .mutations .iter() @@ -693,6 +717,11 @@ where Ok(parsed) => parsed, Err(error) => return batch_error(error), }; + if let Some(scoped_mutations) = scoped_mutations.as_deref() { + if let Err(error) = attach_scoped_fences(&mut parsed, scoped_mutations, &routes) { + return batch_error(error); + } + } let operation = match operation_context( trusted.request.meta.as_ref(), true, @@ -1191,6 +1220,66 @@ struct ParsedMutationRoute { authorizations: Vec, } +fn attach_scoped_fences( + parsed: &mut [ParsedMutation], + scoped: &[ScopedResourceMutation], + routes: &[ParsedMutationRoute], +) -> Result<(), ResourceError> { + if scoped.is_empty() || scoped.len() != parsed.len() || scoped.len() != routes.len() { + return Err(schema_error("scoped batch mutation count does not match")); + } + let assignment = scoped + .first() + .map(ScopedResourceMutation::assignment) + .ok_or_else(|| schema_error("scoped batch assignment is missing"))?; + for (ordinal, ((parsed, route), scoped)) in + parsed.iter_mut().zip(routes).zip(scoped).enumerate() + { + if scoped.assignment() != assignment + || scoped.target() != &route.identity.resource_ref + || resource_mutation_kind_for_assignment(scoped.verb()) != Some(route.kind) + { + return Err(ResourceError::terminal( + ResourceErrorKind::AuthorizationDenied, + "scoped batch mutation is outside its assignment", + )); + } + let mut fence = assignment_fence_for_mutation(scoped).map_err(|_| { + ResourceError::terminal( + ResourceErrorKind::InternalIntegrityFailure, + "assignment-fence-invalid", + ) + })?; + // Keep the first fence at the admitted snapshot for stale-batch and + // single-write fencing; later mutations follow the staged revision. + if ordinal > 0 { + let ExpectedRevision::Exact(revision) = parsed.store.expected else { + return Err(schema_error("scoped batch assignment requires an exact revision")); + }; + fence.resource_revision = revision; + } + parsed.store.assignment = Some(fence); + } + Ok(()) +} + +const fn resource_mutation_kind_for_assignment( + verb: AssignmentVerb, +) -> Option { + match verb { + AssignmentVerb::Create => Some(ResourceMutationKind::Create), + AssignmentVerb::UpdateSpec => Some(ResourceMutationKind::UpdateSpec), + AssignmentVerb::UpdateStatus => Some(ResourceMutationKind::UpdateStatus), + AssignmentVerb::UpdateMetadata => Some(ResourceMutationKind::UpdateMetadata), + AssignmentVerb::UpdateFinalizers => Some(ResourceMutationKind::UpdateFinalizers), + AssignmentVerb::Delete => Some(ResourceMutationKind::Delete), + AssignmentVerb::Get + | AssignmentVerb::List + | AssignmentVerb::Watch + | AssignmentVerb::CommitBatch => None, + } +} + impl core::fmt::Debug for ParsedIdentity { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("ParsedIdentity") @@ -1561,6 +1650,7 @@ fn parse_mutation( reconcile_deadline_ms: mutation .wait_for_reconcile .then_some(mutation.reconcile_deadline_ms), + assignment: None, }, }) } @@ -1897,25 +1987,14 @@ mod tests { }, }; + use d2b_contracts_resource::v3::identity::{ + BindingDigest, EvidenceClass, Locality, ReconnectGeneration, ServiceName, SessionBinding, + SessionPurpose, TranscriptHash, TransportBinding, + }; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ControllerGeneration, - ResourceGeneration, - ResourceUid, - SchemaFingerprint, - ZoneId, -}; -use d2b_contracts_resource::v3::identity::{ - BindingDigest, - EvidenceClass, - Locality, - ReconnectGeneration, - ServiceName, - SessionBinding, - SessionPurpose, - TranscriptHash, - TransportBinding, -}; + ConfigurationGeneration, ControllerGeneration, ResourceGeneration, ResourceUid, + SchemaFingerprint, ZoneId, + }; use d2b_resource_store::mutation_seal::MutationSealAcceptor; use d2b_resource_store::{ MutationOrdinal, StoreError, StoreErrorKind, StoreListResult, StoreResolvedIdentity, @@ -2843,6 +2922,7 @@ use d2b_contracts_resource::v3::identity::{ remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }; let parsed_mutation = ParsedMutation { store: protected_mutation, diff --git a/packages/d2b-resource-api/src/watch.rs b/packages/d2b-resource-api/src/watch.rs index c4367c1f8d..4d78808e2a 100644 --- a/packages/d2b-resource-api/src/watch.rs +++ b/packages/d2b-resource-api/src/watch.rs @@ -11,11 +11,7 @@ use std::sync::{ }; use d2b_contracts_resource::v3::{ - ResourceName, - ResourceRef, - ResourceTypeName, - ResourceUid, - ZoneRevision, + ResourceName, ResourceRef, ResourceTypeName, ResourceUid, ZoneRevision, }; use d2b_resource_store::{StoreError, StoreErrorKind, StoreWatchReceipt, StoreWatchRequest}; use d2b_resource_store_redb::{ @@ -425,16 +421,9 @@ mod tests { use std::time::Duration; use d2b_contracts_resource::v3::{ - CanonicalJsonValue, - ConfigurationGeneration, - RESOURCE_ENVELOPE_DOMAIN_TAG, - ResourceRef, - ResourceTypeName, - ResourceUid, - Timestamp, - ZoneId, - canonical_digest, -}; + CanonicalJsonValue, ConfigurationGeneration, RESOURCE_ENVELOPE_DOMAIN_TAG, ResourceRef, + ResourceTypeName, ResourceUid, Timestamp, ZoneId, canonical_digest, + }; use d2b_resource_store::mutation_seal::{MutationSealBody, mutation_seal_pair}; use d2b_resource_store::{ AdmittedAuthorization, AdmittedAuthorizationTarget, AdmittedVerb, ExpectedRevision, @@ -622,6 +611,7 @@ mod tests { remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(digest), diff --git a/packages/d2b-resource-api/src/zone_service.rs b/packages/d2b-resource-api/src/zone_service.rs index c60d2c5509..48c72b7aaf 100644 --- a/packages/d2b-resource-api/src/zone_service.rs +++ b/packages/d2b-resource-api/src/zone_service.rs @@ -7,10 +7,7 @@ use std::{fmt, future::Future, str::FromStr}; use d2b_contracts_resource::v3::{ - CanonicalJsonObject, - MAX_REQUEST_CANONICAL_BYTES, - MAX_RESPONSE_CANONICAL_BYTES, - ResourceRef, + CanonicalJsonObject, MAX_REQUEST_CANONICAL_BYTES, MAX_RESPONSE_CANONICAL_BYTES, ResourceRef, ZoneId, }; diff --git a/packages/d2b-resource-client/BUILD.bazel b/packages/d2b-resource-client/BUILD.bazel index b78d84d1cd..b52caba9f3 100644 --- a/packages/d2b-resource-client/BUILD.bazel +++ b/packages/d2b-resource-client/BUILD.bazel @@ -15,6 +15,7 @@ rust_library( deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", + "//packages/d2b-core-controller:d2b_core_controller", "//packages/d2b-contracts:d2b_contracts", ] + all_crate_deps(normal = True, cargo_only = True), ) diff --git a/packages/d2b-resource-client/Cargo.toml b/packages/d2b-resource-client/Cargo.toml index b37e6299c3..a88428c7a6 100644 --- a/packages/d2b-resource-client/Cargo.toml +++ b/packages/d2b-resource-client/Cargo.toml @@ -12,6 +12,7 @@ workspace = true d2b-contracts-zone-session = { path = "../d2b-contracts-zone-session", version = "0.0.0-bootstrap" } d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } +d2b-core-controller = { path = "../d2b-core-controller", version = "0.0.0-bootstrap" } tokio = { workspace = true, features = ["sync", "time"] } [dev-dependencies] diff --git a/packages/d2b-resource-client/src/dispatch.rs b/packages/d2b-resource-client/src/dispatch.rs index 3480034d30..109377e9da 100644 --- a/packages/d2b-resource-client/src/dispatch.rs +++ b/packages/d2b-resource-client/src/dispatch.rs @@ -11,10 +11,7 @@ use core::time::Duration; use std::{sync::Arc, time::Instant}; -use d2b_contracts_resource::v3::{ - ResourceError, - RetryClass, -}; +use d2b_contracts_resource::v3::{ResourceError, RetryClass}; use crate::{ CallOptions, CancellationToken, ClientError, MAX_REQUEST_LIFETIME_MS, ResolvedTarget, @@ -338,10 +335,7 @@ mod tests { MetadataInput, REQUEST_ID_BYTES, RetryPolicy, RouteRecord, RouteTable, ServiceOwner, TargetInput, TargetResolver, TransportKind, TransportSelection, target::fixtures::zone, }; - use d2b_contracts_resource::v3::{ - ResourceErrorKind, - ResourceErrorReason, -}; + use d2b_contracts_resource::v3::{ResourceErrorKind, ResourceErrorReason}; const ISSUED: u64 = 1_000; diff --git a/packages/d2b-resource-client/src/error.rs b/packages/d2b-resource-client/src/error.rs index 16be47511a..adffe89474 100644 --- a/packages/d2b-resource-client/src/error.rs +++ b/packages/d2b-resource-client/src/error.rs @@ -7,10 +7,7 @@ use core::{error::Error, fmt}; -use d2b_contracts_resource::v3::{ - ResourceErrorKind, - RetryClass, -}; +use d2b_contracts_resource::v3::{ResourceErrorKind, RetryClass}; /// The closed set of client-local refusals. /// diff --git a/packages/d2b-resource-client/src/lib.rs b/packages/d2b-resource-client/src/lib.rs index cb88fc6d4f..77a25578b9 100644 --- a/packages/d2b-resource-client/src/lib.rs +++ b/packages/d2b-resource-client/src/lib.rs @@ -65,8 +65,8 @@ pub use target::{ TransportKind, TransportSelection, ZoneServiceKind, }; pub use zone_client::{ - ComponentSessionConnector, ConnectedZoneClient, ConnectedZoneSession, LocalZoneSession, - ResourceCallOptions, ResourceVerb, ResourceWatch, ResourceWatchTransport, ZoneClient, - ZonePeerIdentity, ZoneServiceClient, ZoneSessionConnector, ZoneSessionPin, ZoneSocketConnector, - resource_verb_is_mutating, + AssignmentIdentity, ComponentSessionConnector, ConnectedZoneClient, ConnectedZoneSession, + LocalZoneSession, ResourceCallOptions, ResourceVerb, ResourceWatch, ResourceWatchTransport, + ScopedResourceMutation, ZoneClient, ZonePeerIdentity, ZoneServiceClient, ZoneSessionConnector, + ZoneSessionPin, ZoneSocketConnector, resource_verb_is_mutating, }; diff --git a/packages/d2b-resource-client/src/process_attach.rs b/packages/d2b-resource-client/src/process_attach.rs index 82aa384920..6b8b1c0c85 100644 --- a/packages/d2b-resource-client/src/process_attach.rs +++ b/packages/d2b-resource-client/src/process_attach.rs @@ -13,8 +13,8 @@ use std::{ time::Duration, }; -use d2b_contracts_zone_session::v3::zone_routing::ZonePath; use d2b_contracts_resource::v3::ResourceRef; +use d2b_contracts_zone_session::v3::zone_routing::ZonePath; use crate::{ AttemptDisposition, CallDriver, CallOptions, CancellationToken, ClientError, MethodProfile, @@ -756,12 +756,8 @@ mod tests { }, }; + use d2b_contracts_resource::v3::{CanonicalJsonObject, ResourceErrorKind, RetryClass}; use d2b_contracts_zone_session::v3::zone_routing::ZoneLabelId; -use d2b_contracts_resource::v3::{ - CanonicalJsonObject, - ResourceErrorKind, - RetryClass, -}; use super::*; use crate::{ diff --git a/packages/d2b-resource-client/src/target.rs b/packages/d2b-resource-client/src/target.rs index dd66f82f86..fe0f20a9d9 100644 --- a/packages/d2b-resource-client/src/target.rs +++ b/packages/d2b-resource-client/src/target.rs @@ -9,12 +9,9 @@ use core::fmt; +use d2b_contracts_resource::v3::{ResourceName, ResourceRef, ResourceTypeName}; use d2b_contracts_zone_session::v3::zone_routing::ZonePath; -use d2b_contracts_resource::v3::{ - ResourceName, - ResourceRef, - ResourceTypeName, -}; +use d2b_core_controller::controller_assignment::AssignmentTarget; use crate::ClientError; @@ -405,6 +402,24 @@ impl ResolvedTarget { pub fn resource_ref(&self) -> Option { self.owner.resource_ref() } + + /// Check that this resolved route is exactly the assignment target. + pub fn matches_assignment(&self, assignment: &AssignmentTarget) -> bool { + match assignment { + AssignmentTarget::Zone(zone) => { + self.owner.zone().depth() == 1 + && self + .owner + .zone() + .labels() + .first() + .is_some_and(|label| label.as_str() == zone.as_str()) + } + AssignmentTarget::Execution { reference, .. } => { + self.resource_ref().as_ref() == Some(reference) + } + } + } } impl fmt::Debug for ResolvedTarget { diff --git a/packages/d2b-resource-client/src/zone_client.rs b/packages/d2b-resource-client/src/zone_client.rs index fe17168fcd..5dd31a2873 100644 --- a/packages/d2b-resource-client/src/zone_client.rs +++ b/packages/d2b-resource-client/src/zone_client.rs @@ -17,9 +17,12 @@ use std::{ }; use d2b_contracts_resource::v3::{ - CanonicalJsonObject, - ResourceRef, + CanonicalJsonObject, ResourceName, ResourceRef, ResourceTypeName, }; +use d2b_core_controller::controller_assignment::{ + AssignmentError, AssignmentVerb, ResourceClientLease, ScopedResourceFilter, ScopedResourceQuery, +}; +pub use d2b_core_controller::controller_assignment::{AssignmentIdentity, ScopedResourceMutation}; use crate::{ AttemptDisposition, CallDriver, CallOptions, ClientError, MethodProfile, ResolvedTarget, @@ -32,9 +35,7 @@ use crate::{ /// This is an alias of the contract-owned catalogue rather than a second /// client-side enum, so adding or removing a method changes the canonical /// service descriptor and this API together. -pub use d2b_contracts_zone_session::v3::{ - ResourceMethod as ResourceVerb, -}; +pub use d2b_contracts_zone_session::v3::ResourceMethod as ResourceVerb; /// Whether a ResourceService method can change durable Resource state. pub const fn resource_verb_is_mutating(verb: ResourceVerb) -> bool { @@ -244,6 +245,21 @@ pub trait ConnectedZoneSession: Send + Sync { self.call(verb, target, payload) } + /// Issue one scoped commit through the same authenticated transport. + /// + /// The assignment and mutations are the transport-neutral bus admission + /// descriptor. The runtime adapter converts them to its existing + /// `ResourceCall::ScopedCommitBatch` route and attaches the storage fence. + fn call_scoped_commit_batch( + &self, + _assignment: AssignmentIdentity, + _mutations: Vec, + _payload: CanonicalJsonObject, + _relative_timeout_nanos: u64, + ) -> impl Future> + Send { + core::future::ready(Err(ClientError::ContractViolation)) + } + /// Forward cancellation for the request currently in flight. fn cancel( &self, @@ -473,6 +489,19 @@ where R: TargetResolver, W: WallClock, { + /// Mint the controller-scoped collection query used by the existing + /// Resource API route. The lease supplies the non-widenable assignment + /// filter; callers can only narrow its ResourceType/name selectors. + pub fn scoped_query( + &self, + lease: &ResourceClientLease, + resource_types: Vec, + resource_names: Vec, + filters: Vec, + ) -> Result { + lease.query(resource_types, resource_names, filters) + } + /// Resolve a target and prepare one bounded Resource call. pub fn prepare_resource_call( &self, @@ -534,7 +563,16 @@ where { let (resolved, _driver) = self.prepare_resource_call(target, verb, options, selection, request.has_attachments)?; - execute_resource_call(&self.resource, session, &resolved, verb, _driver, request).await + execute_resource_call( + &self.resource, + session, + &resolved, + verb, + _driver, + request, + None, + ) + .await } /// Execute a typed call over a handle whose authenticated route pin was @@ -569,6 +607,68 @@ where verb, driver, request, + None, + ) + .await + } + + /// Execute a controller-scoped atomic commit batch on one authenticated + /// Zone session. + /// + /// Each target and verb is re-admitted by the non-clonable lease. The + /// resulting transport descriptor is derived from that same admission + /// before the existing Resource transport is used. + pub async fn scoped_commit_batch( + &self, + connection: &ConnectedZoneClient, + lease: &ResourceClientLease, + mutations: Vec<(ResourceRef, AssignmentVerb)>, + options: CallOptions, + request: ResourceCallOptions<'_>, + ) -> Result + where + C: ZoneSessionConnector, + { + if mutations.is_empty() || mutations.len() > 128 { + return Err(ClientError::ContractViolation); + } + if !connection + .session_pin() + .matches_target(connection.target(), connection.target().service()) + { + return Err(ClientError::TransportPolicyMismatch); + } + let scoped_mutations = mutations + .into_iter() + .map(|(target, verb)| { + if !verb.is_mutating() || verb == AssignmentVerb::CommitBatch { + return Err(ClientError::ContractViolation); + } + lease + .mutation(target, verb) + .map_err(|_| ClientError::ContractViolation) + }) + .collect::, _>>()?; + let assignment = lease.identity().clone(); + let profile = method_profile_for_service( + connection.target().service(), + ResourceVerb::CommitBatch, + &options, + )?; + let driver = self.resource.prepare_call( + connection.target(), + profile, + options, + request.has_attachments, + )?; + execute_resource_call( + &self.resource, + connection.session(), + connection.target(), + ResourceVerb::CommitBatch, + driver, + request, + Some((assignment, scoped_mutations)), ) .await } @@ -600,6 +700,7 @@ async fn execute_resource_call( verb: ResourceVerb, mut driver: CallDriver, request: ResourceCallOptions<'_>, + scoped_call: Option<(AssignmentIdentity, Vec)>, ) -> Result where S: ConnectedZoneSession, @@ -615,13 +716,30 @@ where loop { let attempt = driver.begin_attempt(cancellation)?; - let call = session.call_with_timeout( - verb, - resource_target.clone(), - payload.clone(), - attempt.relative_timeout_nanos(), - ); - let result = match await_with_cancellation(call, cancellation).await { + let result = if let Some((assignment, mutations)) = scoped_call.as_ref() { + await_with_cancellation( + session.call_scoped_commit_batch( + assignment.clone(), + mutations.clone(), + payload.clone(), + attempt.relative_timeout_nanos(), + ), + cancellation, + ) + .await + } else { + await_with_cancellation( + session.call_with_timeout( + verb, + resource_target.clone(), + payload.clone(), + attempt.relative_timeout_nanos(), + ), + cancellation, + ) + .await + }; + let result = match result { Ok(result) => result, Err(ClientError::Cancelled) => { let _ = session.cancel(request_id).await; diff --git a/packages/d2b-resource-store-redb/src/actor.rs b/packages/d2b-resource-store-redb/src/actor.rs index 635149a348..3a1e8e1deb 100644 --- a/packages/d2b-resource-store-redb/src/actor.rs +++ b/packages/d2b-resource-store-redb/src/actor.rs @@ -7,7 +7,9 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use d2b_audit::OperationIdentity; use d2b_audit::{DurabilityEvidence, DurabilityOutcome, Reconciliation, ZoneOperationKey}; -use d2b_contracts_resource::v3::{ResourceRef, ResourceTypeName, ZoneId, ZoneRevision}; +use d2b_contracts_resource::v3::{ + ResourceRef, ResourceTypeName, ResourceUid, ZoneId, ZoneRevision, +}; use d2b_resource_store::{ ExpectedRevision, ResourceMutationKind, StoreError, StoreFilter, StoreGetRequest, StoreInspectSchemaRequest, StoreListRequest, StoreListResult, StoreProjection, @@ -2105,11 +2107,11 @@ fn read_list( { continue; } - if !filters_match(&request.filters, resource_type, name) { - continue; - } let record: ResourceRecord = decode(ValueKind::ResourceRecord, value.value())?; let mut resource = stored_resource(&request.zone, &resource_ref, &record)?; + if !filters_match(&request.filters, resource_type, name, &resource.uid) { + continue; + } project_resource(&mut resource, request.projection)?; resources.push((key.value().to_vec(), resource)); if resources.len() > page_size { @@ -2234,10 +2236,16 @@ fn hex_decode(value: &str) -> Result, StoreError> { .collect() } -fn filters_match(filters: &[StoreFilter], resource_type: &str, name: &str) -> bool { +fn filters_match( + filters: &[StoreFilter], + resource_type: &str, + name: &str, + uid: &ResourceUid, +) -> bool { filters.iter().all(|filter| match filter.field.as_str() { "metadata.name" => filter.values.iter().any(|value| value == name), "type" => filter.values.iter().any(|value| value == resource_type), + "assignment.resourceUid" => filter.values.iter().any(|value| value == uid.as_str()), _ => false, }) } @@ -2373,6 +2381,29 @@ mod tests { .unwrap() } + #[test] + fn assignment_filter_is_exact_and_does_not_widen_a_watch() { + let uid = ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(); + assert!(filters_match( + &[StoreFilter { + field: "assignment.resourceUid".to_owned(), + values: vec![uid.as_str().to_owned()], + }], + "Process", + "worker", + &uid, + )); + assert!(!filters_match( + &[StoreFilter { + field: "assignment.resourceUid".to_owned(), + values: vec!["223e4567-e89b-42d3-a456-426614174001".to_owned(),], + }], + "Process", + "worker", + &uid, + )); + } + #[test] fn range_seek_never_scans_or_decodes_a_corrupt_older_envelope() { let (_directory, database) = database("range-seek-corrupt-old"); diff --git a/packages/d2b-resource-store-redb/src/revision_log.rs b/packages/d2b-resource-store-redb/src/revision_log.rs index 1d653cac86..24ac2cb6d9 100644 --- a/packages/d2b-resource-store-redb/src/revision_log.rs +++ b/packages/d2b-resource-store-redb/src/revision_log.rs @@ -93,6 +93,10 @@ impl WatchSelector { .values .iter() .any(|value| value == entry.resource_type().as_str()), + "assignment.resourceUid" => filter + .values + .iter() + .any(|value| value == entry.resource_uid().as_str()), _ => false, }) } @@ -674,6 +678,36 @@ mod tests { .unwrap() } + #[test] + fn replay_selector_keeps_assignment_uid_fence() { + let selector = WatchSelector::new( + [ResourceTypeName::parse("Process").unwrap()], + [], + [StoreFilter { + field: "assignment.resourceUid".to_owned(), + values: vec![ + "123e4567-e89b-42d3-a456-426614174000".to_owned(), + ], + }], + ); + let entry = crate::transaction::ChangeEntry::new( + 0, + ResourceTypeName::parse("Process").unwrap(), + ResourceName::parse("worker").unwrap(), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + crate::transaction::ChangeEvent::StatusUpdated, + Some(ResourceGeneration::new(1).unwrap()), + Some(ResourceGeneration::new(1).unwrap()), + None, + "sha256:0000000000000000000000000000000000000000000000000000000000000000".to_owned(), + None, + "operation".to_owned(), + "correlation".to_owned(), + ) + .unwrap(); + assert!(selector.matches(&entry)); + } + #[test] fn budget_eviction_releases_entries_and_retains_ack_cursor() { let mut coordinator = WatchCoordinator::default(); @@ -788,7 +822,16 @@ mod tests { ); let mut coordinator = WatchCoordinator::default(); - let selector = WatchSelector::new([ResourceTypeName::parse("Process").unwrap()], [], []); + let selector = WatchSelector::new( + [ResourceTypeName::parse("Process").unwrap()], + [], + [StoreFilter { + field: "assignment.resourceUid".to_owned(), + values: vec![ + "123e4567-e89b-42d3-a456-426614174000".to_owned(), + ], + }], + ); let expired = coordinator .register_and_replay( &database, diff --git a/packages/d2b-resource-store-redb/src/tests.rs b/packages/d2b-resource-store-redb/src/tests.rs index 75edf8db52..4b3dc38e7a 100644 --- a/packages/d2b-resource-store-redb/src/tests.rs +++ b/packages/d2b-resource-store-redb/src/tests.rs @@ -226,6 +226,7 @@ fn create_seal_body_for_type( remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, }, None, Some(payload_digest), @@ -434,6 +435,7 @@ fn seed_host(directory: &tempfile::TempDir, name: &str) { owner_uid: None, controller_binding_id: "Provider/system-core".to_owned(), payload_digest: envelope.digest().unwrap(), + assignment: None, }; let value = encode(ValueKind::ResourceRecord, &record).unwrap(); let type_value = encode( @@ -523,6 +525,7 @@ fn seed_two_hosts(directory: &tempfile::TempDir) { owner_uid: None, controller_binding_id: "Provider/system-core".to_owned(), payload_digest: envelope.digest().unwrap(), + assignment: None, }; let write = database.begin_write().unwrap(); let value = crate::transaction::encode(ValueKind::ResourceRecord, &record).unwrap(); @@ -2376,6 +2379,7 @@ fn prepare_production_rss_fixture() -> tempfile::TempDir { owner_uid: None, controller_binding_id: "Provider/system-core".to_owned(), payload_digest, + assignment: None, }; let value = crate::transaction::encode(ValueKind::ResourceRecord, &record) .expect("production RSS fixture resource encoding"); diff --git a/packages/d2b-resource-store-redb/src/transaction.rs b/packages/d2b-resource-store-redb/src/transaction.rs index 9800b50413..e1a1dcbe35 100644 --- a/packages/d2b-resource-store-redb/src/transaction.rs +++ b/packages/d2b-resource-store-redb/src/transaction.rs @@ -8,9 +8,9 @@ use d2b_contracts_resource::v3::{ RetryClass, Timestamp, ZoneId, ZoneRevision, canonical_digest, }; use d2b_resource_store::{ - AdmittedAuthorization, ExpectedRevision, MutationOrdinal, PolicySnapshot, ResourceMutationKind, - StoreCommitResult, StoreError, StoreErrorKind, StoreMutation, StoreOperationContext, - StoredResource, + AdmittedAuthorization, ExpectedRevision, MutationOrdinal, PolicySnapshot, + ResourceAssignmentFence, ResourceMutationKind, StoreCommitResult, StoreError, StoreErrorKind, + StoreMutation, StoreOperationContext, StoredResource, }; use redb::{ Database, Durability, ReadableDatabase, ReadableTable, ReadableTableMetadata, TableDefinition, @@ -203,6 +203,22 @@ pub(crate) struct ResourceRecord { pub owner_uid: Option, pub controller_binding_id: String, pub payload_digest: String, + #[serde(default)] + pub assignment: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub(crate) struct AssignmentRecord { + pub resource_uid: String, + pub resource_revision: u64, + pub provider_generation: u64, + pub controller_generation: u64, + pub controller_role: String, + pub target: String, + pub session_generation: u64, + pub epoch: u64, + pub phase: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -460,6 +476,10 @@ impl ChangeEntry { &self.resource_name } + pub const fn resource_uid(&self) -> &ResourceUid { + &self.resource_uid + } + pub const fn event(&self) -> ChangeEvent { self.event } @@ -740,6 +760,8 @@ struct FinalizedMutation { payload_digest: String, } +type StagedResourceState = Option<(ResourceRecord, ResourceEnvelope)>; + #[cfg(test)] pub(crate) fn empty_write_request_for_test( sequence: u64, @@ -1219,10 +1241,29 @@ pub(crate) fn validate_consistency(database: &Database) -> Result<(), StoreError .transpose()? .map(|uid| uid.as_str().to_owned()); if record.owner_uid != expected_owner_uid - || record.controller_binding_id != controller_binding_id(&envelope) + || record.controller_binding_id + != controller_binding_id(&envelope, record.assignment.as_ref()) { return Err(integrity("stored-resource-derived-fields-invalid")); } + if let Some(assignment) = &record.assignment { + if assignment.resource_uid != uid.as_str() + || assignment.resource_revision != envelope.metadata().revision().get() + || assignment.provider_generation == 0 + || assignment.controller_generation == 0 + || assignment.session_generation == 0 + || assignment.epoch == 0 + || !matches!( + assignment.phase.as_str(), + "assigned" | "draining" | "revoked" | "stale" | "quarantined" | "released" + ) + || ResourceRef::parse(&assignment.controller_role) + .map_or(true, |role| role.resource_type().as_str() != "Process") + || ResourceRef::parse(&assignment.target).is_err() + { + return Err(integrity("stored-assignment-invalid")); + } + } let type_value = types .get(type_index_key(&resource_ref)?.as_slice()) .map_err(integrity)? @@ -2812,6 +2853,9 @@ fn apply_prepared( owner_uid: old_record.owner_uid.clone(), controller_binding_id: old_record.controller_binding_id.clone(), payload_digest: payload_digest.clone(), + assignment: old_record.assignment.as_ref().map(|assignment| { + assignment_rebound_to_revision(assignment, revision) + }), }; write .open_table(RESOURCES) @@ -2929,11 +2973,30 @@ fn apply_prepared( if payload_digest != finalized.payload_digest { return Err(integrity("finalized-payload-digest-mismatch")); } + let assignment = mutation + .assignment + .as_ref() + .map(|fence| { + assignment_record( + fence, + &uid, + revision, + previous_resource.as_ref().map(|resource| resource.revision), + ) + }) + .transpose()? + .or_else(|| { + previous + .as_ref() + .and_then(|record| record.assignment.clone()) + }) + .map(|assignment| assignment_rebound_to_revision(&assignment, revision)); let record = ResourceRecord { canonical_json: canonical_json.clone(), owner_uid: owner_uid.clone(), - controller_binding_id: controller_binding_id(&envelope), + controller_binding_id: controller_binding_id(&envelope, assignment.as_ref()), payload_digest: payload_digest.clone(), + assignment, }; let producer = endpoint_producer(&envelope)?; insert_resource_and_indexes( @@ -2975,6 +3038,49 @@ fn apply_prepared( )) } +fn staged_state_after_mutation( + prepared: &VerifiedPreparedMutation, + finalized: &FinalizedMutation, + previous: Option<&(ResourceRecord, ResourceEnvelope)>, + revision: u64, + uid: &ResourceUid, +) -> Result<(ResourceRecord, ResourceEnvelope), StoreError> { + let envelope = ResourceEnvelope::from_json(&finalized.canonical_json) + .map_err(|_| integrity("stored-resource-envelope-invalid"))?; + let assignment = prepared + .mutation() + .assignment + .as_ref() + .map(|fence| { + assignment_record( + fence, + uid, + revision, + previous.map(|(_, envelope)| envelope.metadata().revision()), + ) + }) + .transpose()? + .or_else(|| { + previous + .and_then(|(record, _)| record.assignment.clone()) + }) + .map(|assignment| assignment_rebound_to_revision(&assignment, revision)); + let mut record = previous + .map(|(record, _)| record.clone()) + .unwrap_or_else(|| ResourceRecord { + canonical_json: Vec::new(), + owner_uid: None, + controller_binding_id: String::new(), + payload_digest: String::new(), + assignment: None, + }); + record.canonical_json = finalized.canonical_json.clone(); + record.payload_digest = finalized.payload_digest.clone(); + record.assignment = assignment; + record.controller_binding_id = controller_binding_id(&envelope, record.assignment.as_ref()); + Ok((record, envelope)) +} + fn validate_verified_write( write: &redb::WriteTransaction, verified: &VerifiedWrite, @@ -2985,7 +3091,7 @@ fn validate_verified_write( if verified.authorization.zone.as_str() != meta.zone_name { return Err(integrity("mutation-zone-mismatch")); } - let mut staged = std::collections::BTreeMap::>::new(); + let mut staged = std::collections::BTreeMap::::new(); let mut finalized = Vec::with_capacity(verified.mutations.len()); for (ordinal, prepared) in verified.mutations.iter().enumerate() { let mutation = prepared.mutation(); @@ -3000,14 +3106,60 @@ fn validate_verified_write( if mutation.zone != verified.authorization.zone { return Err(integrity("mutation-zone-mismatch")); } - let current = if let Some(uid) = staged.get(&mutation.target) { - uid.clone().map(|uid| (uid, revision)) - } else { - current_identity_in_write(write, &mutation.target)? - }; + // A single verified operation may touch one target more than once. + // Later mutations must see the earlier staged envelope and assignment, + // not the transaction's original snapshot. + let current_state = staged_or_current_record_in_write(write, &staged, &mutation.target)?; + let current = current_state.as_ref().map(|(_, envelope)| { + ( + envelope.metadata().uid().clone(), + envelope.metadata().revision().get(), + ) + }); if !authorization_matches(&verified.authorization, mutation) { return Err(authorization_denied(meta.current_revision)); } + if let Some(fence) = &mutation.assignment { + let Some((record, envelope)) = current_state.as_ref() else { + return Err(conflict( + meta.current_revision, + ordinal, + "assignment-resource-missing", + )); + }; + if fence.resource_uid != *envelope.metadata().uid() + || fence.resource_revision != envelope.metadata().revision() + { + return Err(conflict( + envelope.metadata().revision().get(), + ordinal, + "stale-assignment", + )); + } + if record.assignment.as_ref().is_some_and(|current| { + !assignment_matches(current, fence) + && !assignment_replacement_allowed(current, fence) + }) + { + return Err(conflict( + envelope.metadata().revision().get(), + ordinal, + "stale-assignment", + )); + } + } else if matches!( + mutation.kind, + ResourceMutationKind::UpdateStatus | ResourceMutationKind::UpdateFinalizers + ) && current_state + .as_ref() + .is_some_and(|(record, _)| record.assignment.is_some()) + { + return Err(conflict( + current.as_ref().map_or(0, |(_, revision)| *revision), + ordinal, + "assignment-required", + )); + } match mutation.expected { ExpectedRevision::CreateAbsent if current.is_some() => { return Err(error( @@ -3067,7 +3219,9 @@ fn validate_verified_write( "resource-not-found", )); } - let (record, envelope) = current_record_in_write(write, &mutation.target)? + let (record, envelope) = current_state + .as_ref() + .cloned() .ok_or_else(|| integrity("mutation-current-resource-missing"))?; if deletion_requested(&record.canonical_json)? { if has_finalizers(&record.canonical_json)? { @@ -3092,10 +3246,7 @@ fn validate_verified_write( )); } } - staged.insert( - mutation.target.clone(), - current.as_ref().map(|(uid, _)| uid.clone()), - ); + staged.insert(mutation.target.clone(), None); finalized.push(None); continue; } @@ -3125,15 +3276,24 @@ fn validate_verified_write( "resource-uid-changed", )); } - let previous = - current_record_in_write(write, &mutation.target)?.map(|(record, _)| record); - finalized.push(Some(finalize_authorized_mutation( + let previous = current_state + .as_ref() + .map(|(record, _)| record.clone()); + let finalized_mutation = finalize_authorized_mutation( prepared, previous.as_ref(), revision, &uid, - )?)); - staged.insert(mutation.target.clone(), Some(uid)); + )?; + let staged_state = staged_state_after_mutation( + prepared, + &finalized_mutation, + current_state.as_ref(), + revision, + &uid, + )?; + finalized.push(Some(finalized_mutation)); + staged.insert(mutation.target.clone(), Some(staged_state)); continue; } @@ -3169,7 +3329,8 @@ fn validate_verified_write( "resource-uid-changed", )); } - let current_owner = current_record_in_write(write, &mutation.target)? + let current_owner = current_state + .as_ref() .and_then(|(_, envelope)| envelope.metadata().owner_ref().cloned()); let owner = if mutation.kind == ResourceMutationKind::Create || mutation.kind == ResourceMutationKind::UpdateMetadata @@ -3180,7 +3341,9 @@ fn validate_verified_write( }; if let Some(owner_ref) = owner { let owner_uid = if let Some(owner) = staged.get(owner_ref) { - owner.clone() + owner + .as_ref() + .map(|(_, envelope)| envelope.metadata().uid().clone()) } else { current_identity_in_write(write, owner_ref)?.map(|(uid, _)| uid) }; @@ -3204,15 +3367,25 @@ fn validate_verified_write( let previous = if mutation.kind == ResourceMutationKind::Create { None } else { - current_record_in_write(write, &mutation.target)?.map(|(record, _)| record) + current_state + .as_ref() + .map(|(record, _)| record.clone()) }; - finalized.push(Some(finalize_authorized_mutation( + let finalized_mutation = finalize_authorized_mutation( prepared, previous.as_ref(), revision, &uid, - )?)); - staged.insert(mutation.target.clone(), Some(uid)); + )?; + let staged_state = staged_state_after_mutation( + prepared, + &finalized_mutation, + current_state.as_ref(), + revision, + &uid, + )?; + finalized.push(Some(finalized_mutation)); + staged.insert(mutation.target.clone(), Some(staged_state)); } Ok(finalized) } @@ -3273,7 +3446,7 @@ fn authorization_matches(authorization: &AdmittedAuthorization, mutation: &Store fn owner_chain_reaches( write: &redb::WriteTransaction, - staged: &std::collections::BTreeMap>, + staged: &std::collections::BTreeMap, owner_ref: &ResourceRef, child_uid: &ResourceUid, ) -> Result { @@ -3289,8 +3462,10 @@ fn owner_chain_reaches( "resource-owner-depth", )); } - let uid = if let Some(staged_uid) = staged.get(&resource_ref) { - staged_uid.clone() + let uid = if let Some(staged_state) = staged.get(&resource_ref) { + staged_state + .as_ref() + .map(|(_, envelope)| envelope.metadata().uid().clone()) } else { current_identity_in_write(write, &resource_ref)?.map(|(uid, _)| uid) }; @@ -3300,11 +3475,28 @@ fn owner_chain_reaches( if &uid == child_uid || !visited.insert(uid) { return Ok(true); } - current = current_owner_ref_in_write(write, &resource_ref)?; + current = if let Some(staged_state) = staged.get(&resource_ref) { + staged_state + .as_ref() + .and_then(|(_, envelope)| envelope.metadata().owner_ref().cloned()) + } else { + current_owner_ref_in_write(write, &resource_ref)? + }; } Ok(false) } +fn staged_or_current_record_in_write( + write: &redb::WriteTransaction, + staged: &std::collections::BTreeMap, + resource_ref: &ResourceRef, +) -> Result { + if let Some(state) = staged.get(resource_ref) { + return Ok(state.clone()); + } + current_record_in_write(write, resource_ref) +} + fn current_owner_ref_in_write( write: &redb::WriteTransaction, resource_ref: &ResourceRef, @@ -4002,13 +4194,82 @@ pub(crate) fn stored_resource( }) } -fn controller_binding_id(envelope: &ResourceEnvelope) -> String { +fn controller_binding_id( + envelope: &ResourceEnvelope, + assignment: Option<&AssignmentRecord>, +) -> String { + if let Some(assignment) = assignment { + let mut bytes = Vec::new(); + bytes.extend_from_slice(assignment.controller_role.as_bytes()); + bytes.push(0); + bytes.extend_from_slice(assignment.target.as_bytes()); + bytes.push(0); + bytes.extend_from_slice(&assignment.epoch.to_be_bytes()); + return canonical_digest("d2b:v3:assignment-binding", &bytes); + } envelope.spec().provider_ref().cloned().map_or_else( || envelope.resource_type().as_str().to_owned(), |provider| provider.to_canonical_string(), ) } +fn assignment_record( + fence: &ResourceAssignmentFence, + uid: &ResourceUid, + new_revision: u64, + current_resource_revision: Option, +) -> Result { + if fence.epoch == 0 + || fence.resource_uid != *uid + || current_resource_revision != Some(fence.resource_revision) + || fence.controller_role.resource_type().as_str() != "Process" + || !matches!( + fence.target.resource_type().as_str(), + "Zone" | "Host" | "Guest" + ) + { + return Err(integrity("assignment-fence-invalid")); + } + Ok(AssignmentRecord { + resource_uid: fence.resource_uid.as_str().to_owned(), + resource_revision: new_revision, + provider_generation: fence.provider_generation.get(), + controller_generation: fence.controller_generation.get(), + controller_role: fence.controller_role.to_canonical_string(), + target: fence.target.to_canonical_string(), + session_generation: fence.session_generation.get(), + epoch: fence.epoch, + phase: "assigned".to_owned(), + }) +} + +fn assignment_matches(record: &AssignmentRecord, fence: &ResourceAssignmentFence) -> bool { + record.resource_uid == fence.resource_uid.as_str() + && record.provider_generation == fence.provider_generation.get() + && record.controller_generation == fence.controller_generation.get() + && record.controller_role == fence.controller_role.to_canonical_string() + && record.target == fence.target.to_canonical_string() + && record.session_generation == fence.session_generation.get() + && record.epoch == fence.epoch + && record.phase == "assigned" +} + +fn assignment_rebound_to_revision(assignment: &AssignmentRecord, revision: u64) -> AssignmentRecord { + let mut rebound = assignment.clone(); + rebound.resource_revision = revision; + rebound +} + +fn assignment_replacement_allowed( + record: &AssignmentRecord, + fence: &ResourceAssignmentFence, +) -> bool { + // Core's registry makes drain/release the authority for a newer epoch. + // The store serializes that successor with the resource write, replacing + // the old assignment before either writer can observe a later revision. + record.resource_uid == fence.resource_uid.as_str() && fence.epoch > record.epoch +} + fn operation_digests(verified: &VerifiedWrite) -> Result<[String; 2], StoreError> { Ok([ operation_digest(verified)?, @@ -4066,6 +4327,22 @@ fn operation_digest(verified: &VerifiedWrite) -> Result { .as_ref() .map(|uid| uid.as_str().as_bytes()), )?; + if let Some(fence) = &prepared.assignment { + digest.update([1]); + digest_field(&mut digest, fence.resource_uid.as_str().as_bytes())?; + digest.update(fence.resource_revision.get().to_be_bytes()); + digest.update(fence.provider_generation.get().to_be_bytes()); + digest.update(fence.controller_generation.get().to_be_bytes()); + digest_field( + &mut digest, + fence.controller_role.to_canonical_string().as_bytes(), + )?; + digest_field(&mut digest, fence.target.to_canonical_string().as_bytes())?; + digest.update(fence.session_generation.get().to_be_bytes()); + digest.update(fence.epoch.to_be_bytes()); + } else { + digest.update([0]); + } digest_optional_field( &mut digest, prepared @@ -4454,9 +4731,13 @@ fn error( #[cfg(test)] mod tests { use super::*; - use d2b_contracts_resource::v3::{ConfigurationGeneration, ResourceTypeName, Timestamp}; + use d2b_contracts_resource::v3::identity::ReconnectGeneration; + use d2b_contracts_resource::v3::{ + ConfigurationGeneration, ResourcePhase, ResourceTypeName, Timestamp, + }; use d2b_resource_store::{ - AdmittedAuthorizationTarget, AdmittedVerb, ResourceMutationKind, StoreSlot, + AdmittedAuthorizationTarget, AdmittedVerb, ResourceAssignmentFence, ResourceMutationKind, + StoreSlot, }; use redb::ReadableTableMetadata; use std::fs::OpenOptions; @@ -4552,6 +4833,7 @@ mod tests { remove_finalizers: Vec::new(), wait_for_reconcile: false, reconcile_deadline_ms: None, + assignment: None, } } @@ -5004,6 +5286,347 @@ mod tests { assert_eq!(created.results[0].as_ref().unwrap().revision.get(), 1); } + #[test] + fn assignment_fence_rebinds_sequential_writes_and_rejects_stale_successors() { + let (_directory, database, _identity) = fixture(); + let target = ResourceRef::parse("Host/host-system").unwrap(); + let uid = ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(); + let seeded = apply_group( + &database, + vec![verified( + "assignment-seed", + create_mutation(target.clone()), + uid.clone(), + )], + ) + .unwrap(); + let uid = seeded.results[0].as_ref().unwrap().resources[0].uid.clone(); + + let fence = ResourceAssignmentFence { + resource_uid: uid.clone(), + resource_revision: ZoneRevision::new(1), + provider_generation: ResourceGeneration::new(2).unwrap(), + controller_generation: ControllerGeneration::new(3).unwrap(), + controller_role: ResourceRef::parse("Process/process-controller").unwrap(), + target: target.clone(), + session_generation: ReconnectGeneration::new(4).unwrap(), + epoch: 1, + }; + let mut first = create_mutation(target.clone()); + first.kind = ResourceMutationKind::UpdateFinalizers; + first.expected = ExpectedRevision::Exact(ZoneRevision::new(1)); + first.expected_uid = Some(uid.clone()); + first.canonical_resource = None; + first.add_finalizers = vec![FinalizerId::parse("core.controller-test").unwrap()]; + first.assignment = Some(fence.clone()); + let committed = apply_group( + &database, + vec![verified("assignment-first", first, uid.clone())], + ) + .unwrap(); + assert_eq!( + committed.results[0].as_ref().unwrap().revision, + ZoneRevision::new(2) + ); + + let unrelated_target = ResourceRef::parse("Host/unrelated").unwrap(); + let unrelated_uid = + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174001").unwrap(); + apply_group( + &database, + vec![verified( + "assignment-gap", + create_mutation_with_uid(unrelated_target, &unrelated_uid), + unrelated_uid, + )], + ) + .unwrap(); + + let mut status_body = CanonicalJsonValue::parse(RESOURCE).unwrap(); + let CanonicalJsonValue::Object(root) = &mut status_body else { + unreachable!() + }; + let CanonicalJsonValue::Object(metadata) = root.get_mut("metadata").unwrap() else { + unreachable!() + }; + metadata.insert( + "uid".to_owned(), + CanonicalJsonValue::String(uid.as_str().to_owned()), + ); + let CanonicalJsonValue::Object(status) = root.get_mut("status").unwrap() else { + unreachable!() + }; + status.insert( + "phase".to_owned(), + CanonicalJsonValue::String("Ready".to_owned()), + ); + let mut sequential = create_mutation(target.clone()); + sequential.kind = ResourceMutationKind::UpdateStatus; + sequential.expected = ExpectedRevision::Exact(ZoneRevision::new(2)); + sequential.expected_uid = Some(uid.clone()); + sequential.canonical_resource = Some(status_body.to_canonical_bytes()); + sequential.assignment = Some(ResourceAssignmentFence { + resource_revision: ZoneRevision::new(2), + ..fence.clone() + }); + let sequential_commit = apply_group( + &database, + vec![verified( + "assignment-sequential", + sequential, + uid.clone(), + )], + ) + .unwrap(); + assert_eq!( + sequential_commit.results[0].as_ref().unwrap().revision, + ZoneRevision::new(4) + ); + + let mut stale = create_mutation(target.clone()); + stale.kind = ResourceMutationKind::UpdateFinalizers; + stale.expected = ExpectedRevision::Exact(ZoneRevision::new(4)); + stale.expected_uid = Some(uid.clone()); + stale.canonical_resource = None; + stale.add_finalizers = vec![FinalizerId::parse("core.controller-stale").unwrap()]; + stale.assignment = Some(fence.clone()); + let rejected = apply_group( + &database, + vec![verified("assignment-stale", stale, uid.clone())], + ) + .unwrap(); + assert_eq!( + rejected.results[0].as_ref().unwrap_err().reason_code(), + "stale-assignment" + ); + let mut successor = create_mutation(target); + successor.kind = ResourceMutationKind::UpdateFinalizers; + successor.expected = ExpectedRevision::Exact(ZoneRevision::new(4)); + successor.expected_uid = Some(uid.clone()); + successor.canonical_resource = None; + successor.add_finalizers = + vec![FinalizerId::parse("core.controller-successor").unwrap()]; + successor.assignment = Some(ResourceAssignmentFence { + resource_revision: ZoneRevision::new(4), + epoch: 2, + ..fence + }); + let successor_commit = apply_group( + &database, + vec![verified("assignment-successor", successor, uid)], + ) + .unwrap(); + assert_eq!( + successor_commit.results[0].as_ref().unwrap().revision, + ZoneRevision::new(5) + ); + assert_eq!(current_meta(&database).unwrap().current_revision, 5); + assert_eq!( + database + .begin_read() + .unwrap() + .open_table(REVISION_LOG) + .unwrap() + .len() + .unwrap(), + 5 + ); + validate_consistency(&database).unwrap(); + } + + #[test] + fn scoped_commit_batch_persists_fenced_status_finalizer_index_and_revision_atomically() { + let (_directory, database, _identity) = fixture(); + let target = ResourceRef::parse("Host/host-system").unwrap(); + let uid = ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(); + let seeded = apply_group( + &database, + vec![verified( + "multi-assignment-seed", + create_mutation(target.clone()), + uid.clone(), + )], + ) + .unwrap(); + let uid = seeded.results[0].as_ref().unwrap().resources[0].uid.clone(); + + let fence = ResourceAssignmentFence { + resource_uid: uid.clone(), + resource_revision: ZoneRevision::new(1), + provider_generation: ResourceGeneration::new(2).unwrap(), + controller_generation: ControllerGeneration::new(3).unwrap(), + controller_role: ResourceRef::parse("Process/process-controller").unwrap(), + target: target.clone(), + session_generation: ReconnectGeneration::new(4).unwrap(), + epoch: 1, + }; + let mut assignment_seed = create_mutation(target.clone()); + assignment_seed.kind = ResourceMutationKind::UpdateFinalizers; + assignment_seed.expected = ExpectedRevision::Exact(ZoneRevision::new(1)); + assignment_seed.expected_uid = Some(uid.clone()); + assignment_seed.canonical_resource = None; + assignment_seed.add_finalizers = + vec![FinalizerId::parse("core.controller-seed").unwrap()]; + assignment_seed.assignment = Some(fence.clone()); + apply_group( + &database, + vec![verified("multi-assignment-bind", assignment_seed, uid.clone())], + ) + .unwrap(); + + let mut status_body = CanonicalJsonValue::parse(RESOURCE).unwrap(); + let CanonicalJsonValue::Object(root) = &mut status_body else { + unreachable!() + }; + let CanonicalJsonValue::Object(metadata) = root.get_mut("metadata").unwrap() else { + unreachable!() + }; + metadata.insert( + "uid".to_owned(), + CanonicalJsonValue::String(uid.as_str().to_owned()), + ); + let CanonicalJsonValue::Object(status) = root.get_mut("status").unwrap() else { + unreachable!() + }; + status.insert( + "phase".to_owned(), + CanonicalJsonValue::String("Ready".to_owned()), + ); + + let mut status = create_mutation(target.clone()); + status.kind = ResourceMutationKind::UpdateStatus; + status.expected = ExpectedRevision::Exact(ZoneRevision::new(2)); + status.expected_uid = Some(uid.clone()); + status.canonical_resource = Some(status_body.to_canonical_bytes()); + status.assignment = Some(ResourceAssignmentFence { + resource_revision: ZoneRevision::new(2), + ..fence.clone() + }); + let mut finalizers = create_mutation(target.clone()); + finalizers.kind = ResourceMutationKind::UpdateFinalizers; + finalizers.expected = ExpectedRevision::Exact(ZoneRevision::new(3)); + finalizers.expected_uid = Some(uid.clone()); + finalizers.canonical_resource = None; + finalizers.add_finalizers = + vec![FinalizerId::parse("core.controller-batch").unwrap()]; + finalizers.assignment = Some(ResourceAssignmentFence { + resource_revision: ZoneRevision::new(3), + ..fence + }); + + let mut batch = verified("multi-assignment-batch", status, uid.clone()); + let finalizer_write = verified("multi-assignment-batch", finalizers, uid.clone()); + batch + .authorization + .targets + .extend(finalizer_write.authorization.targets); + batch.mutations.extend(finalizer_write.mutations); + let committed = apply_group(&database, vec![batch]).unwrap(); + + assert_eq!( + committed.results[0].as_ref().unwrap().revision, + ZoneRevision::new(3) + ); + let envelope = stored_envelope(&database, &target); + assert_eq!(envelope.status().phase(), ResourcePhase::Ready); + let stored = serde_json::to_value(&envelope).unwrap(); + assert!( + stored["metadata"]["finalizers"] + .as_array() + .unwrap() + .iter() + .any(|finalizer| finalizer == "core.controller-batch") + ); + assert_eq!(envelope.metadata().revision(), ZoneRevision::new(3)); + let read = database.begin_read().unwrap(); + assert_eq!(read.open_table(TYPE_INDEX).unwrap().len().unwrap(), 1); + assert_eq!( + read.open_table(CONTROLLER_INDEX).unwrap().len().unwrap(), + 1 + ); + assert_eq!(read.open_table(REVISION_LOG).unwrap().len().unwrap(), 3); + validate_consistency(&database).unwrap(); + } + + #[test] + fn assignment_fences_reject_lower_epoch_inside_multi_mutation_batch() { + let (_directory, database, _identity) = fixture(); + let target = ResourceRef::parse("Host/host-system").unwrap(); + let uid = ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(); + let seeded = apply_group( + &database, + vec![verified( + "multi-epoch-seed", + create_mutation(target.clone()), + uid.clone(), + )], + ) + .unwrap(); + let uid = seeded.results[0].as_ref().unwrap().resources[0].uid.clone(); + + let fence = ResourceAssignmentFence { + resource_uid: uid.clone(), + resource_revision: ZoneRevision::new(1), + provider_generation: ResourceGeneration::new(2).unwrap(), + controller_generation: ControllerGeneration::new(3).unwrap(), + controller_role: ResourceRef::parse("Process/process-controller").unwrap(), + target: target.clone(), + session_generation: ReconnectGeneration::new(4).unwrap(), + epoch: 1, + }; + let mut assignment_seed = create_mutation(target.clone()); + assignment_seed.kind = ResourceMutationKind::UpdateFinalizers; + assignment_seed.expected = ExpectedRevision::Exact(ZoneRevision::new(1)); + assignment_seed.expected_uid = Some(uid.clone()); + assignment_seed.canonical_resource = None; + assignment_seed.add_finalizers = + vec![FinalizerId::parse("core.controller-epoch-seed").unwrap()]; + assignment_seed.assignment = Some(fence.clone()); + apply_group( + &database, + vec![verified("multi-epoch-bind", assignment_seed, uid.clone())], + ) + .unwrap(); + + let mut successor = create_mutation(target.clone()); + successor.kind = ResourceMutationKind::UpdateFinalizers; + successor.expected = ExpectedRevision::Exact(ZoneRevision::new(2)); + successor.expected_uid = Some(uid.clone()); + successor.canonical_resource = None; + successor.add_finalizers = + vec![FinalizerId::parse("core.controller-epoch-successor").unwrap()]; + successor.assignment = Some(ResourceAssignmentFence { + resource_revision: ZoneRevision::new(2), + epoch: 2, + ..fence.clone() + }); + + let mut stale = create_mutation(target); + stale.kind = ResourceMutationKind::UpdateFinalizers; + stale.expected = ExpectedRevision::Exact(ZoneRevision::new(3)); + stale.expected_uid = Some(uid.clone()); + stale.canonical_resource = None; + stale.add_finalizers = + vec![FinalizerId::parse("core.controller-epoch-stale").unwrap()]; + stale.assignment = Some(fence); + + let mut batch = verified("multi-epoch-batch", successor, uid.clone()); + let stale_write = verified("multi-epoch-batch", stale, uid); + batch + .authorization + .targets + .extend(stale_write.authorization.targets); + batch.mutations.extend(stale_write.mutations); + let outcome = apply_group(&database, vec![batch]).unwrap(); + + assert_eq!( + outcome.results[0].as_ref().unwrap_err().reason_code(), + "stale-assignment" + ); + assert_eq!(current_meta(&database).unwrap().current_revision, 2); + validate_consistency(&database).unwrap(); + } + #[test] fn prepared_uid_mismatch_cannot_replace_an_existing_resource() { let (_directory, database, _identity) = fixture(); diff --git a/packages/d2b-resource-store/src/lib.rs b/packages/d2b-resource-store/src/lib.rs index ca696a46a8..df753b25cd 100644 --- a/packages/d2b-resource-store/src/lib.rs +++ b/packages/d2b-resource-store/src/lib.rs @@ -9,6 +9,7 @@ use d2b_contracts_resource::v3::{ ConfigurationGeneration, ControllerGeneration, FinalizerId, ResourceGeneration, ResourceName, ResourceRef, ResourceTypeName, ResourceUid, ZoneId, ZoneRevision, }; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; pub use error::{ MAX_STORE_SLOTS, MutationOrdinal, MutationOrdinalError, SealIdentityMismatch, StoreError, @@ -262,6 +263,8 @@ pub struct StoreMutation { pub remove_finalizers: Vec, pub wait_for_reconcile: bool, pub reconcile_deadline_ms: Option, + /// Optional Core-issued assignment fence for controller-owned writes. + pub assignment: Option, } impl core::fmt::Debug for StoreMutation { @@ -279,6 +282,36 @@ impl core::fmt::Debug for StoreMutation { "has_reconcile_deadline", &self.reconcile_deadline_ms.is_some(), ) + .field("has_assignment_fence", &self.assignment.is_some()) + .finish() + } +} + +/// Storage-neutral assignment evidence attached to one controller mutation. +#[derive(Clone, PartialEq, Eq)] +pub struct ResourceAssignmentFence { + pub resource_uid: ResourceUid, + pub resource_revision: ZoneRevision, + pub provider_generation: ResourceGeneration, + pub controller_generation: ControllerGeneration, + pub controller_role: ResourceRef, + pub target: ResourceRef, + pub session_generation: ReconnectGeneration, + pub epoch: u64, +} + +impl core::fmt::Debug for ResourceAssignmentFence { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("ResourceAssignmentFence") + .field("resource_uid", &"") + .field("resource_revision", &self.resource_revision) + .field("provider_generation", &self.provider_generation) + .field("controller_generation", &self.controller_generation) + .field("controller_role", &"") + .field("target", &"") + .field("session_generation", &self.session_generation) + .field("epoch", &"") .finish() } } @@ -544,6 +577,7 @@ mod tests { remove_finalizers: Vec::new(), wait_for_reconcile: true, reconcile_deadline_ms: Some(12), + assignment: None, }; let admitted_target = AdmittedAuthorizationTarget { resource_type, diff --git a/packages/d2b/src/context.rs b/packages/d2b/src/context.rs index c498a507a2..6ab440ee0c 100644 --- a/packages/d2b/src/context.rs +++ b/packages/d2b/src/context.rs @@ -23,15 +23,41 @@ use d2b_contracts_resource::v3::{ RetryClass, ZoneId, }; -use d2b_contracts_resource::v3::identity::STANDARD_RESOURCE_TYPES; use d2b_resource_client::{ - CallOptions, CancellationToken, ClientError, ConnectedSession, ConnectedZoneSession, - MetadataInput, NamedStreamTransport, ProcessAttachClient, ProcessAttachOpenRequest, - ProcessAttachOptions, ProcessAttachTarget, ResourceCallOptions, ResourceVerb, RetryPolicy, - RouteRecord, RouteTable, ServiceOwner, SystemClock, TargetInput, TerminalSize, TransportKind, - TransportSelection, WallClock, ZoneClient, ZonePeerIdentity, ZoneServiceKind, - ZoneSessionConnector, ZoneSessionPin, ZoneSocketConnector, resource_verb_is_mutating, + AssignmentIdentity, + CallOptions, + CancellationToken, + ClientError, + ConnectedSession, + ConnectedZoneSession, + MetadataInput, + NamedStreamTransport, + ProcessAttachClient, + ProcessAttachOpenRequest, + ProcessAttachOptions, + ProcessAttachTarget, + ResourceCallOptions, + ResourceVerb, + RetryPolicy, + RouteRecord, + RouteTable, + ScopedResourceMutation, + ServiceOwner, + SystemClock, + TargetInput, + TerminalSize, + TransportKind, + TransportSelection, + WallClock, + ZoneClient, + ZonePeerIdentity, + ZoneServiceKind, + ZoneSessionConnector, + ZoneSessionPin, + ZoneSocketConnector, + resource_verb_is_mutating, }; +use d2b_contracts_resource::v3::identity::STANDARD_RESOURCE_TYPES; use serde_json::{Value, json}; use sha2::{Digest, Sha256}; @@ -1428,6 +1454,18 @@ impl ConnectedZoneSession for CliConnectedSession { let result = self.invoke(target, payload, relative_timeout_nanos); ready(result) } + + fn call_scoped_commit_batch( + &self, + _assignment: AssignmentIdentity, + _mutations: Vec, + _payload: CanonicalJsonObject, + _relative_timeout_nanos: u64, + ) -> impl Future> + Send { + // The CLI public socket is an operator route, not a controller + // ComponentSession. Never downgrade a scoped write to plain CommitBatch. + ready(Err(ClientError::ContractViolation)) + } } impl ConnectedSession for CliConnectedSession { diff --git a/packages/d2bd-runtime/src/resource_runtime_support.rs b/packages/d2bd-runtime/src/resource_runtime_support.rs index 3ebfd36e07..47f1c86be9 100644 --- a/packages/d2bd-runtime/src/resource_runtime_support.rs +++ b/packages/d2bd-runtime/src/resource_runtime_support.rs @@ -6,7 +6,7 @@ use std::{ io::{self, Read}, os::unix::fs::FileTypeExt, path::Path, - sync::Arc, + sync::{Arc, Mutex}, time::{SystemTime, UNIX_EPOCH}, }; @@ -53,6 +53,7 @@ use d2b_contracts_resource::v3::identity::{ TransportBinding, }; use d2b_core_controller::{ + controller_assignment::ControllerAssignmentRegistry, authority::HostGlobalAuthorityIndex, controllers::{CoreHandlerKind, HandlerOutcome, HandlerPhase, HandlerStatus}, main::{ @@ -61,6 +62,15 @@ use d2b_core_controller::{ }, zone_status::ZoneRuntimeMetadata, }; + +/// Provider-neutral Core assignment registry shared by Resource API and bus +/// admission for one Zone runtime. +pub type AssignmentRegistry = Arc>; + +/// Construct one empty Zone assignment registry. +pub fn new_assignment_registry() -> AssignmentRegistry { + Arc::new(Mutex::new(ControllerAssignmentRegistry::default())) +} use d2b_resource_api::{ RedbBackend, ResourceApiClient, ResourceBusAdapter, ResourceService, authz::{ diff --git a/packages/d2bd/BUILD.bazel b/packages/d2bd/BUILD.bazel index 58c778f092..50a7a2dae7 100644 --- a/packages/d2bd/BUILD.bazel +++ b/packages/d2bd/BUILD.bazel @@ -448,6 +448,7 @@ rust_test( "//packages/d2b-contracts-provider:d2b_contracts_provider", "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", + "//packages/d2b-core-controller:d2b_core_controller_test_support", "//packages/d2b-provider-device-tpm:d2b_provider_device_tpm", "//packages/d2b-provider-network-local:d2b_provider_network_local", "//packages/d2b-provider-runtime-cloud-hypervisor:d2b_provider_runtime_cloud_hypervisor", diff --git a/packages/d2bd/src/resource_runtime.rs b/packages/d2bd/src/resource_runtime.rs index 08e407bd98..411b864275 100644 --- a/packages/d2bd/src/resource_runtime.rs +++ b/packages/d2bd/src/resource_runtime.rs @@ -31,10 +31,6 @@ use crate::process_resource_runtime::{ ProcessResourceRuntime, ProcessResourceRuntimeError, list_process_snapshot, process_watch_request, run_process_watch, }; -use d2bd_runtime::resource_operator_activation::{ - Wave6AcceptanceReport, Wave6Dependencies, Wave6ProviderBoundary, Wave6ReconcileResult, - select_wave6_resources, -}; use d2b_audit::{AuditSink, DurabilityEvidence}; use d2b_bus::{BusAuthorizer, BusConfig, BusIngress, ZoneBus, ZoneRegistrar}; #[cfg(test)] @@ -43,34 +39,24 @@ use d2b_contracts_broker::broker_wire::ZoneStoreDisposition; use d2b_contracts_provider::v3::provider::ProviderSpec; use d2b_contracts_resource::resource_proto as wire; #[cfg(test)] +use d2b_contracts_resource::v3::{ConfigurationGeneration, ControllerGeneration}; use d2b_contracts_resource::v3::{ - ConfigurationGeneration, - ControllerGeneration, -}; -use d2b_contracts_zone_session::v3::{ - ZoneStatusResource, - resource_bundle::ResourceBundle, -}; -use d2b_contracts_resource::v3::{ - ResourceEnvelope, - ResourceGeneration, - ResourceName, - ResourcePhase, - ResourceRef, - ResourceTypeName, - ResourceUid, - ZoneId, - ZoneRevision, + ResourceEnvelope, ResourceGeneration, ResourceName, ResourcePhase, ResourceRef, + ResourceTypeName, ResourceUid, ZoneId, ZoneRevision, }; use d2b_contracts_resource::v3::{ host::{HOST_PROVIDER_REF, HostSpec}, user::UserSpec, }; +use d2b_contracts_zone_session::v3::{ZoneStatusResource, resource_bundle::ResourceBundle}; use d2b_core_controller::authority::{ AuthorityRequest, AuthorityReservation, ExternalNicClaimRequest, ExternalNicRecoveryInventory, ExternalNicReservation, HostGlobalAuthorityIndex, TrustedExternalNicInventory, }; use d2b_core_controller::authority_persistence::AuthorityRecoveryCoordinator; +use d2b_core_controller::controller_assignment::{ + AssignmentError, AssignmentIdentity, AssignmentRequest, ResourceClientLease, +}; use d2b_core_controller::controllers::HandlerPhase; use d2b_core_controller::main::{ CoreProcess, RecoverySnapshot, RuntimeReadiness as CoreRuntimeReadiness, StartupStage, @@ -104,13 +90,18 @@ use d2b_session::SessionServerError; use d2bd_runtime::authority_persistence::RedbAuthorityPersistence; pub use d2bd_runtime::resource_api::ResourceRuntimeError; use d2bd_runtime::resource_api::{parse_list_request, route_service_matches}; +use d2bd_runtime::resource_operator_activation::{ + Wave6AcceptanceReport, Wave6Dependencies, Wave6ProviderBoundary, Wave6ReconcileResult, + select_wave6_resources, +}; use d2bd_runtime::resource_runtime_support::{ - SystemCoreReconcileResult, compatibility_error_envelope, configuration_cleanup_pending, - current_status_timestamp, encode_public_get_response, encode_public_list_response, - ensure_bootstrap_host_resource, handler_phase_to_zone_phase, host_phase_for_resource_count, - initial_policy_snapshot, map_startup_error, materialize_zone_resource_bundle, - public_list_request, public_operation_id, public_request_meta, register_system_core_session, - runtime_authorizer, runtime_policy, store_identity, watch_needs_restart, zone_runtime_metadata, + AssignmentRegistry, SystemCoreReconcileResult, compatibility_error_envelope, + configuration_cleanup_pending, current_status_timestamp, encode_public_get_response, + encode_public_list_response, ensure_bootstrap_host_resource, handler_phase_to_zone_phase, + host_phase_for_resource_count, initial_policy_snapshot, map_startup_error, + materialize_zone_resource_bundle, new_assignment_registry, public_list_request, + public_operation_id, public_request_meta, register_system_core_session, runtime_authorizer, + runtime_policy, store_identity, watch_needs_restart, zone_runtime_metadata, }; pub use d2bd_runtime::resource_runtime_support::{ ZoneRuntimeReadiness, persist_resource_status, persist_resource_status_with_projection, @@ -545,6 +536,7 @@ pub struct ZoneResourceRuntime { policy_installed: bool, controller_endpoint_registered: bool, watch_admitted: bool, + assignments: AssignmentRegistry, authority_index: Arc>, authority_persistence: Arc, authority_recovery: Arc, @@ -808,6 +800,7 @@ impl ZoneResourceRuntime { }) .unwrap_or_default(); let authorizer = Arc::new(runtime_authorizer(&bundle_resource_types)?); + let assignments = new_assignment_registry(); let acceptor = authorizer .take_store_seal(store_identity.seal_identity()) .map_err(|_| ResourceRuntimeError::StoreSealUnavailable)?; @@ -991,6 +984,7 @@ impl ZoneResourceRuntime { ResourceRuntimeError::AuthorizationUnavailable })?; let bus_authorizer = BusAuthorizer::from_shared(Arc::clone(&authorizer), state.clone()) + .map(|authorizer| authorizer.with_assignment_registry(Arc::clone(&assignments))) .map_err(|error| { tracing::error!(zone = %zone.as_str(), error = ?error, "resource runtime bus authorizer setup failed"); ResourceRuntimeError::AuthorizationUnavailable @@ -1207,6 +1201,7 @@ impl ZoneResourceRuntime { policy_installed, controller_endpoint_registered, watch_admitted, + assignments, authority_index, authority_persistence, authority_recovery, @@ -1239,6 +1234,49 @@ impl ZoneResourceRuntime { self.readiness } + /// Borrow the Zone-scoped Core assignment registry. + pub fn assignment_registry(&self) -> AssignmentRegistry { + Arc::clone(&self.assignments) + } + + /// Admit one controller assignment through the Zone-owned registry. + /// + /// Controller deployment supplies only the committed resource, signed + /// role, installed generations, and authenticated session generation. + /// The registry remains the single owner of assignment epochs and target + /// conflicts; callers never receive a store handle. + pub fn admit_controller_assignment( + &self, + request: AssignmentRequest<'_>, + ) -> Result { + self.assignments + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .admit(request) + } + + /// Revoke assignments bound to a disconnected ComponentSession generation. + pub fn revoke_controller_assignments( + &self, + generation: d2b_contracts_resource::v3::identity::ReconnectGeneration, + ) { + self.assignments + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .revoke_session(generation); + } + + /// Mark one assignment as draining before a target or generation handoff. + pub fn drain_controller_assignment( + &self, + identity: &AssignmentIdentity, + ) -> Result<(), AssignmentError> { + self.assignments + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .begin_drain(identity) + } + /// Return the policy revision committed in the opened resource store. /// /// Interaction Providers bind this snapshot instead of carrying a @@ -3511,8 +3549,7 @@ mod tests { }, }, }); - let canonical_json = - d2b_contracts_resource::v3::canonical_json_bytes(&envelope).unwrap(); + let canonical_json = d2b_contracts_resource::v3::canonical_json_bytes(&envelope).unwrap(); let parsed = ResourceEnvelope::from_json(&canonical_json).unwrap(); StoredResource { resource_ref: ResourceRef::parse(&format!("Provider/{name}")).unwrap(), diff --git a/packages/d2bd/tests/resource_operator_activation.rs b/packages/d2bd/tests/resource_operator_activation.rs index 27c9e71981..8132eef60a 100644 --- a/packages/d2bd/tests/resource_operator_activation.rs +++ b/packages/d2bd/tests/resource_operator_activation.rs @@ -18,6 +18,7 @@ use d2b_contracts_resource::v3::{ ConfigurationGeneration, ControllerGeneration, RESOURCE_ENVELOPE_DOMAIN_TAG, + ResourceEnvelope, ResourceName, ResourceRef, ResourceUid, @@ -45,6 +46,7 @@ use d2b_contracts_resource::v3::identity::{ use d2b_contracts_broker::broker_wire::{ BrokerCallerRole, OpenZoneStoreResponse, ZoneStoreDisposition, }; +use d2b_core_controller::controller_assignment::ScopedCommitTransport; use d2b_resource_api::{ RedbBackend, ResourceBusAdapter, ResourceService, authz::{ @@ -54,7 +56,10 @@ use d2b_resource_api::{ }, }; use d2b_resource_store::{PolicySnapshot, StoreSlot}; -use d2b_resource_store_redb::{RedbResourceStore, StoreIdentity, write_provisioning_marker}; +use d2b_resource_store_redb::{ + DecodedKey, DecodedKeyComponent, DecodedValue, RedbResourceStore, StoreIdentity, ValueKind, + write_provisioning_marker, +}; use d2bd::provider_effects::{ EffectDispatch, GuestLifecycleOperation, GuestLifecycleRequest, GuestLifecycleState, ProviderEffectError, ProviderLifecycleDispatch, ProviderLifecycleEffectPort, @@ -694,6 +699,264 @@ async fn authenticated_operator_reaches_ready_resource_plane_and_refuses_other_s runtime.shutdown().await.expect("shutdown resource runtime"); } +#[tokio::test] +async fn scoped_status_finalizer_batch_reaches_redb_atomically_and_rebinds_assignment() { + let directory = tempfile::tempdir().expect("resource-plane directory"); + let zone = ZoneId::parse("work").unwrap(); + let marker_identity = format!("sha256:{}", "e".repeat(64)); + let database_path = directory.path().join("store.redb"); + let marker_path = directory.path().join("store.marker"); + let snapshot = PolicySnapshot { + policy_revision: 1, + api_catalog_revision: 1, + active_configuration_revision: ConfigurationGeneration::new(1).unwrap(), + controller_generation: Some(ControllerGeneration::new(1).unwrap()), + }; + let store_identity = StoreIdentity::new( + StoreSlot::new(0).unwrap(), + stable_uid("store", &marker_identity), + zone.clone(), + stable_uid("zone", zone.as_str()), + d2b_contracts_resource::v3::Timestamp::parse("1970-01-01T00:00:00.000Z").unwrap(), + snapshot, + ); + seed_host_resource( + &zone, + &database_path, + &marker_path, + &marker_identity, + store_identity, + snapshot, + ) + .await; + + let database = OpenOptions::new() + .read(true) + .write(true) + .open(&database_path) + .expect("reopen redb file"); + let mut runtime = ZoneResourceRuntime::open( + zone.clone(), + OpenedZoneStore { + response: OpenZoneStoreResponse { + zone_store_id: ZoneStoreId::parse("zone-store-work").unwrap(), + store_identity: marker_identity, + disposition: ZoneStoreDisposition::Opened, + fd_index: 0, + }, + database_fd: database.into(), + external_inventory: None, + }, + ) + .await + .expect("open production Zone runtime"); + runtime.set_provider_path_ready(true); + + let (operator_ref, operator_uid) = ZoneResourceRuntime::operator_subject_identity(); + let controller_generation = runtime + .committed_policy_snapshot() + .controller_generation + .expect("runtime controller generation"); + let client = runtime + .bind_operator_resource_client( + operator_context(&zone, operator_ref, operator_uid) + .with_controller_generation(controller_generation), + ) + .expect("bind authenticated operator Resource API client"); + let mut target_identity = wire::ResourceIdentity::new(); + target_identity.zone = zone.as_str().to_owned(); + target_identity.resource_type = "Host".to_owned(); + target_identity.name = "host-system".to_owned(); + let mut get = wire::GetRequest::new(); + let mut get_meta = wire::RequestMeta::new(); + get_meta.operation_id = "scoped-assignment-read".to_owned(); + get_meta.correlation_id = get_meta.operation_id.clone(); + get.meta = MessageField::some(get_meta); + get.target = MessageField::some(target_identity); + let mut projection = wire::Projection::new(); + projection.kind = EnumOrUnknown::new(wire::ProjectionKind::PROJECTION_KIND_FULL); + get.projection = MessageField::some(projection); + let current = client.get(get).await; + assert!( + current.error.is_none(), + "read seeded Host failed: {:?}", + current.error + ); + let current = current.resource.as_ref().expect("seeded Host response"); + let current_identity = current.identity.as_ref().expect("seeded Host identity"); + let uid = ResourceUid::parse(current_identity.uid.as_ref().expect("seeded Host UID")).unwrap(); + let initial_revision = current_identity.revision.expect("seeded Host revision"); + let before_batch = runtime + .backup_before_live_adoption() + .await + .expect("capture pre-batch redb state"); + assert_eq!(before_batch.current_revision, initial_revision); + let batch_revision = initial_revision + .checked_add(1) + .expect("batch revision remains in range"); + let mut status_value = CanonicalJsonValue::parse(¤t.canonical_json).unwrap(); + let CanonicalJsonValue::Object(root) = &mut status_value else { + panic!("seeded Host envelope must be an object"); + }; + let CanonicalJsonValue::Object(status) = root.get_mut("status").expect("seeded Host status") + else { + panic!("seeded Host status must be an object"); + }; + status.insert( + "phase".to_owned(), + CanonicalJsonValue::String("Ready".to_owned()), + ); + let status_payload = status_value.to_canonical_bytes(); + let status_digest = ResourceEnvelope::from_json(&status_payload) + .unwrap() + .digest() + .unwrap(); + let make_precondition = |revision| { + let mut precondition = wire::Precondition::new(); + precondition.kind = + EnumOrUnknown::new(wire::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION); + precondition.expected_revision = Some(revision); + precondition.expected_uid = Some(uid.as_str().to_owned()); + precondition + }; + let make_batch = |operation_id: &str, status_revision: u64, finalizer_revision: u64| { + let mut status_mutation = wire::Mutation::new(); + status_mutation.kind = EnumOrUnknown::new(wire::MutationKind::MUTATION_KIND_UPDATE_STATUS); + status_mutation.target = MessageField::some(current_identity.clone()); + status_mutation.precondition = MessageField::some(make_precondition(status_revision)); + let mut status_body = wire::ResourceEnvelopeBytes::new(); + status_body.identity = MessageField::some(current_identity.clone()); + status_body.canonical_json = status_payload.clone(); + status_body.payload_digest = status_digest.clone(); + status_mutation.resource = MessageField::some(status_body); + + let mut finalizer_mutation = wire::Mutation::new(); + finalizer_mutation.kind = + EnumOrUnknown::new(wire::MutationKind::MUTATION_KIND_UPDATE_FINALIZERS); + finalizer_mutation.target = MessageField::some(current_identity.clone()); + finalizer_mutation.precondition = MessageField::some(make_precondition(finalizer_revision)); + finalizer_mutation + .add_finalizers + .push("core.controller-batch".to_owned()); + + let mut request = wire::CommitBatchRequest::new(); + let mut meta = wire::RequestMeta::new(); + meta.operation_id = operation_id.to_owned(); + meta.idempotency_key = operation_id.to_owned(); + meta.correlation_id = operation_id.to_owned(); + request.meta = MessageField::some(meta); + request.mutations = vec![status_mutation, finalizer_mutation]; + request + }; + let transport = ScopedCommitTransport::decode( + format!( + r#"{{"version":1,"assignment":{{"resourceUid":"{}","resourceRevision":{},"providerGeneration":2,"controllerGeneration":3,"controllerRole":"Process/process-controller","target":{{"kind":"execution","targetKind":"host","reference":"Host/host-system"}},"sessionGeneration":1,"epoch":1}},"mutations":[{{"target":"Host/host-system","verb":"UpdateStatus"}},{{"target":"Host/host-system","verb":"UpdateFinalizers"}}]}}"#, + uid.as_str(), + initial_revision, + ) + .as_bytes(), + ) + .unwrap(); + + let committed = client + .scoped_commit_batch( + make_batch("scoped-assignment-batch", initial_revision, batch_revision), + transport.mutations().to_vec(), + ) + .await; + assert!( + committed.error.is_none(), + "valid scoped batch failed: kind={:?} reason={}", + committed.error.as_ref().map(|error| error.kind), + committed + .error + .as_ref() + .map_or("", |error| error.reason.as_str()) + ); + assert_eq!(committed.revision, batch_revision); + assert_eq!(committed.resources.len(), 2); + + let backup = runtime + .backup_before_live_adoption() + .await + .expect("capture committed redb backup"); + assert_eq!(backup.current_revision, batch_revision); + let resources = backup + .tables + .iter() + .find(|table| table.name == "resources") + .expect("resources table in backup"); + let resource_row = resources + .rows + .iter() + .find(|row| { + let key = DecodedKey::decode(&row.key).expect("decode resource key"); + matches!( + key.components(), + [ + DecodedKeyComponent::Text(resource_type), + DecodedKeyComponent::Text(resource_name) + ] if resource_type == "Host" && resource_name == "host-system" + ) + }) + .expect("durable Host resource row"); + let encoded_record = DecodedValue::decode(&resource_row.value).expect("decode resource record"); + assert_eq!(encoded_record.kind(), ValueKind::ResourceRecord); + let record: serde_json::Value = serde_json::from_slice(encoded_record.canonical_json()) + .expect("decode resource record JSON"); + assert_eq!(record["assignment"]["resourceRevision"], batch_revision); + let canonical_json: Vec = + serde_json::from_value(record["canonical_json"].clone()).expect("decode stored envelope"); + let envelope = ResourceEnvelope::from_json(&canonical_json).expect("stored envelope"); + assert_eq!( + envelope.status().phase(), + d2b_contracts_resource::v3::ResourcePhase::Ready + ); + assert!( + serde_json::from_slice::(&canonical_json) + .expect("decode stored envelope JSON")["metadata"]["finalizers"] + .as_array() + .expect("stored finalizer array") + .iter() + .any(|finalizer| finalizer == "core.controller-batch") + ); + + let stale = client + .scoped_commit_batch( + make_batch( + "scoped-stale-assignment-batch", + batch_revision, + batch_revision + .checked_add(1) + .expect("stale batch revision remains in range"), + ), + transport.mutations().to_vec(), + ) + .await; + assert!( + stale.error.is_some(), + "stale assignment batch must be rejected" + ); + assert_eq!( + stale + .error + .as_ref() + .map(|error| error.kind.enum_value_or_default()), + Some(wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_CONFLICT) + ); + assert_eq!( + stale.error.as_ref().map(|error| error.reason.as_str()), + Some("stale-assignment") + ); + let after_stale = runtime + .backup_before_live_adoption() + .await + .expect("capture redb state after stale batch"); + assert_eq!(after_stale.current_revision, batch_revision); + drop(client); + runtime.shutdown().await.expect("shutdown resource runtime"); +} + #[tokio::test] async fn authenticated_operator_drives_wave6_resources_through_production_boundary() { let directory = tempfile::tempdir().expect("resource-plane directory"); diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json index d3102e8c84..cb3b31e316 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 2418e4f3b9..2614f6b9be 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json index 5f79cef453..d69379d320 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 2418e4f3b9..2614f6b9be 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index f23d1796ac..1510367a5c 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index dd81d84981..88ee820a72 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index af88af33a1..a59acb8a9d 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index dd81d84981..88ee820a72 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 2a89d46e3e..263d060442 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index f923c8e8a3..b26080babf 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index c911d95e9c..b1b809e3ab 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index f923c8e8a3..b26080babf 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json index 55cdedd0c6..5cceec9ebf 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json index 2418e4f3b9..2614f6b9be 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json index 9bbc040c41..e09be333e7 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json index 2418e4f3b9..2614f6b9be 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock index 1faaa1a678..a01d2eac3f 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock @@ -995,9 +995,11 @@ dependencies = [ "async-trait", "d2b-audit", "d2b-contracts", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-controller-toolkit", + "d2b-core-controller", "d2b-resource-api", "d2b-resource-store", "d2b-resource-store-redb", @@ -1840,6 +1842,7 @@ dependencies = [ "d2b-contracts", "d2b-contracts-resource", "d2b-contracts-zone-session", + "d2b-core-controller", "tokio", ] diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json index 21828d721f..d3ff9671a8 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -4552,6 +4552,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "d2b-contracts-provider@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, { "from": "d2b-bus@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -4576,6 +4582,12 @@ "kind": "dev", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "d2b-core-controller@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-bus@0.0.0-bootstrap#path", "to": "d2b-resource-api@0.0.0-bootstrap#path", @@ -7468,6 +7480,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "d2b-core-controller@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json index e228944bc7..7382a2d29e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 427, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock index 1faaa1a678..a01d2eac3f 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock @@ -995,9 +995,11 @@ dependencies = [ "async-trait", "d2b-audit", "d2b-contracts", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-controller-toolkit", + "d2b-core-controller", "d2b-resource-api", "d2b-resource-store", "d2b-resource-store-redb", @@ -1840,6 +1842,7 @@ dependencies = [ "d2b-contracts", "d2b-contracts-resource", "d2b-contracts-zone-session", + "d2b-core-controller", "tokio", ] diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json index c55b5f8bf9..57a9ff0ede 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -4570,6 +4570,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "d2b-core-controller@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-bus@0.0.0-bootstrap#path", "to": "d2b-resource-api@0.0.0-bootstrap#path", @@ -7036,6 +7042,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "d2b-core-controller@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json index e228944bc7..7382a2d29e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 427, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index 31cbdf7917..a98b60853e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index a93c530ca1..1fe31a50f1 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index 031c53a7bd..29bd8848ce 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index a93c530ca1..1fe31a50f1 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json index 7489db8a9d..569283c7b9 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index a8150dddb6..08a8a146c2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json index 58bff05633..23c2917a61 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json index a8150dddb6..08a8a146c2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index ea72b0bd9e..c90c81cda5 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index dd03a7c1ff..0ac23de382 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index 6f3416343e..560840f3b2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index dd03a7c1ff..0ac23de382 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 3f482049a3..bf4a521fa6 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index 375c080318..d6989be88b 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index 6b3c35a94e..e887fc6dfb 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index 375c080318..d6989be88b 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json index 599b8296ce..4394611b45 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json index a8150dddb6..08a8a146c2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json index 94fa81e349..7e4c1fa9b6 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json index a8150dddb6..08a8a146c2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock index 1faaa1a678..a01d2eac3f 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock @@ -995,9 +995,11 @@ dependencies = [ "async-trait", "d2b-audit", "d2b-contracts", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-controller-toolkit", + "d2b-core-controller", "d2b-resource-api", "d2b-resource-store", "d2b-resource-store-redb", @@ -1840,6 +1842,7 @@ dependencies = [ "d2b-contracts", "d2b-contracts-resource", "d2b-contracts-zone-session", + "d2b-core-controller", "tokio", ] diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json index b9f9b0e389..5199cc3ec6 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -4604,6 +4604,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "d2b-contracts-provider@0.0.0-bootstrap#path", + "kind": "dev", + "target": null + }, { "from": "d2b-bus@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -4628,6 +4634,12 @@ "kind": "dev", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "d2b-core-controller@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-bus@0.0.0-bootstrap#path", "to": "d2b-resource-api@0.0.0-bootstrap#path", @@ -7520,6 +7532,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "d2b-core-controller@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json index d3d552f511..9c602da7ee 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 429, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock index 1faaa1a678..a01d2eac3f 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock @@ -995,9 +995,11 @@ dependencies = [ "async-trait", "d2b-audit", "d2b-contracts", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-controller-toolkit", + "d2b-core-controller", "d2b-resource-api", "d2b-resource-store", "d2b-resource-store-redb", @@ -1840,6 +1842,7 @@ dependencies = [ "d2b-contracts", "d2b-contracts-resource", "d2b-contracts-zone-session", + "d2b-core-controller", "tokio", ] diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json index 3b4eedeaf0..f8922e16d8 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -4622,6 +4622,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "d2b-core-controller@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-bus@0.0.0-bootstrap#path", "to": "d2b-resource-api@0.0.0-bootstrap#path", @@ -7088,6 +7094,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "d2b-core-controller@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json index d3d552f511..9c602da7ee 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 429, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index 16f46af033..ee21e8307b 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 93a1406fe3..53947270ba 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index 3a8376affc..3ef6e07d9a 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 93a1406fe3..53947270ba 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "2bc389b67dbd18cd8182764df9ef503e6cbd8aea013e9ffb3b5c90c451b04f35", + "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", From 9212fb5c20fe1ad007c95f2c0987325ee004b8d5 Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 12:55:34 -0700 Subject: [PATCH 04/56] policy: refresh generated lock digests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 2 +- .../broker-default-tests/production/closure.json | 2 +- .../broker-default-tests/production/metadata.json | 2 +- .../broker-fake-backends-tests/policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 2 +- .../broker-production/production/closure.json | 2 +- .../broker-production/production/metadata.json | 2 +- .../main-product/policy/Cargo.lock | 2 ++ .../main-product/policy/closure.json | 14 +++++++++++++- .../main-product/policy/metadata.json | 2 +- .../main-product/production/Cargo.lock | 2 ++ .../main-product/production/closure.json | 2 +- .../main-product/production/metadata.json | 2 +- .../guest-shell-runner-static/policy/closure.json | 2 +- .../guest-shell-runner-static/policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 2 +- .../broker-default-tests/production/closure.json | 2 +- .../broker-default-tests/production/metadata.json | 2 +- .../broker-fake-backends-tests/policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 2 +- .../broker-production/production/closure.json | 2 +- .../broker-production/production/metadata.json | 2 +- .../main-product/policy/Cargo.lock | 2 ++ .../main-product/policy/closure.json | 14 +++++++++++++- .../main-product/policy/metadata.json | 2 +- .../main-product/production/Cargo.lock | 2 ++ .../main-product/production/closure.json | 2 +- .../main-product/production/metadata.json | 2 +- .../guest-shell-runner-static/policy/closure.json | 2 +- .../guest-shell-runner-static/policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- 52 files changed, 80 insertions(+), 48 deletions(-) diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json index cb3b31e316..52b282de6e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 2614f6b9be..56915ff36e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json index d69379d320..53b96983de 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 2614f6b9be..56915ff36e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index 1510367a5c..e5a7934c02 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index 88ee820a72..7474217074 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index a59acb8a9d..0a0119d737 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index 88ee820a72..7474217074 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 263d060442..f87444be26 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index b26080babf..cc4c5723e6 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index b1b809e3ab..88e31cf71d 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index b26080babf..cc4c5723e6 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json index 5cceec9ebf..1df55993c3 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json index 2614f6b9be..56915ff36e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json index e09be333e7..985c880c58 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json index 2614f6b9be..56915ff36e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock index a01d2eac3f..e624f075aa 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock @@ -1007,6 +1007,7 @@ dependencies = [ "d2b-session-unix", "d2b-telemetry", "getrandom 0.3.4", + "protobuf", "quote", "serde_json", "sha2", @@ -1014,6 +1015,7 @@ dependencies = [ "syn 2.0.119", "tempfile", "tokio", + "ttrpc", ] diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json index d3ff9671a8..20f21f7f42 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -4630,6 +4630,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "protobuf@3.7.2#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, { "from": "d2b-bus@0.0.0-bootstrap#path", "to": "quote@1.0.47#registry+https://github.com/rust-lang/crates.io-index", @@ -4678,6 +4684,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "ttrpc@0.9.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json index 7382a2d29e..bb4af90b46 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 427, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock index a01d2eac3f..e624f075aa 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock @@ -1007,6 +1007,7 @@ dependencies = [ "d2b-session-unix", "d2b-telemetry", "getrandom 0.3.4", + "protobuf", "quote", "serde_json", "sha2", @@ -1014,6 +1015,7 @@ dependencies = [ "syn 2.0.119", "tempfile", "tokio", + "ttrpc", ] diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json index 57a9ff0ede..f789cc5bb3 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json index 7382a2d29e..bb4af90b46 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 427, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index a98b60853e..e0defc987f 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 1fe31a50f1..0624f7a7c1 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index 29bd8848ce..7337599d3c 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 1fe31a50f1..0624f7a7c1 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json index 569283c7b9..766508980b 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 08a8a146c2..d0d8c70aae 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json index 23c2917a61..4b83682796 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 08a8a146c2..d0d8c70aae 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index c90c81cda5..6159e8da63 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index 0ac23de382..c7318e291a 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index 560840f3b2..e231e97a11 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index 0ac23de382..c7318e291a 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index bf4a521fa6..f12f50b1f3 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index d6989be88b..164897778f 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index e887fc6dfb..d688fb2a0b 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index d6989be88b..164897778f 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json index 4394611b45..31c9d6381e 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json index 08a8a146c2..d0d8c70aae 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json index 7e4c1fa9b6..57c8f11e7f 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json index 08a8a146c2..d0d8c70aae 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock index a01d2eac3f..e624f075aa 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock @@ -1007,6 +1007,7 @@ dependencies = [ "d2b-session-unix", "d2b-telemetry", "getrandom 0.3.4", + "protobuf", "quote", "serde_json", "sha2", @@ -1014,6 +1015,7 @@ dependencies = [ "syn 2.0.119", "tempfile", "tokio", + "ttrpc", ] diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json index 5199cc3ec6..daa351f2ba 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -4682,6 +4682,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "protobuf@3.7.2#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, { "from": "d2b-bus@0.0.0-bootstrap#path", "to": "quote@1.0.47#registry+https://github.com/rust-lang/crates.io-index", @@ -4730,6 +4736,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-bus@0.0.0-bootstrap#path", + "to": "ttrpc@0.9.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "dev", + "target": null + }, { "from": "d2b-contracts-broker@0.0.0-bootstrap#path", "to": "d2b-contracts-control@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json index 9c602da7ee..fbf6cee443 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 429, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock index a01d2eac3f..e624f075aa 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock @@ -1007,6 +1007,7 @@ dependencies = [ "d2b-session-unix", "d2b-telemetry", "getrandom 0.3.4", + "protobuf", "quote", "serde_json", "sha2", @@ -1014,6 +1015,7 @@ dependencies = [ "syn 2.0.119", "tempfile", "tokio", + "ttrpc", ] diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json index f8922e16d8..476b4d39d8 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json @@ -76,7 +76,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json index 9c602da7ee..fbf6cee443 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 429, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index ee21e8307b..958bf9c9b2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 53947270ba..34a4aa1dc4 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index 3ef6e07d9a..3f4f3229db 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lock_sha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 53947270ba..34a4aa1dc4 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "478e91c1d3a6df0192ab40a8dafcea4ae2c5df8b8b65c51c9dc4597c938781b3", + "lockSha256": "17d8e64257f2679785dbdfcf8be7c3ddbedcd8e9eb7d17b6dd18f1fc2e669fd3", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", From 00633af4b63a962798c9b7f0431bf55eeee2c8ed Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 15:40:31 -0700 Subject: [PATCH 05/56] nix: include provider schemas in fixture sources Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bazel/checks/fixtures/BUILD.bazel | 1 + bazel/checks/nix/BUILD.bazel | 7 +++- .../fix-nix-host-tool-source-schemas.md | 4 ++ tests/unit/nix/BUILD.bazel | 1 + tests/unit/nix/cases/host-tools-source.nix | 41 +++++++++++++++++++ tests/unit/nix/surfaces/broker-profiles.nix | 1 + 6 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fix-nix-host-tool-source-schemas.md create mode 100644 tests/unit/nix/cases/host-tools-source.nix diff --git a/bazel/checks/fixtures/BUILD.bazel b/bazel/checks/fixtures/BUILD.bazel index 3f10741bbb..bec0e3e3f3 100644 --- a/bazel/checks/fixtures/BUILD.bazel +++ b/bazel/checks/fixtures/BUILD.bazel @@ -6,6 +6,7 @@ _FIXTURE_SOURCES = [ "//:bazel_provider_workspace_sources", "//:Cargo.lock", "//:Cargo.toml", + "//:d2b_resource_schemas_v3", "//:examples_workspace_sources", "//:flake.lock", "//:flake.nix", diff --git a/bazel/checks/nix/BUILD.bazel b/bazel/checks/nix/BUILD.bazel index 2cef75a13e..9e73422408 100644 --- a/bazel/checks/nix/BUILD.bazel +++ b/bazel/checks/nix/BUILD.bazel @@ -17,8 +17,13 @@ _SURFACES = { ], }, "broker-profiles": { - "cases": ["guest-broker", "prebuilt-broker"], + "cases": ["guest-broker", "host-tools-source", "prebuilt-broker"], + "package": "bazel/checks/fixtures", "files": [ + "bazel/checks/fixtures/BUILD.bazel", + "docs/reference/schemas/v3/providers/transport-azure-relay.transport-settings.json", + "docs/reference/schemas/v3/providers/transport-vsock.transport-binding.json", + "nixos-modules/rust-host-tools.nix", "nix/prebuilt.json", "nix/prebuilt.nix", "nixos-modules/guest-broker.nix", diff --git a/changelog.d/fix-nix-host-tool-source-schemas.md b/changelog.d/fix-nix-host-tool-source-schemas.md new file mode 100644 index 0000000000..3af39e9873 --- /dev/null +++ b/changelog.d/fix-nix-host-tool-source-schemas.md @@ -0,0 +1,4 @@ +### Fixed + +- Include v3 provider schemas in isolated Nix fixture sources so profile-capable + host tools compile crates that embed their schemas at build time. diff --git a/tests/unit/nix/BUILD.bazel b/tests/unit/nix/BUILD.bazel index 4145940f37..06eb63ff9d 100644 --- a/tests/unit/nix/BUILD.bazel +++ b/tests/unit/nix/BUILD.bazel @@ -29,6 +29,7 @@ exports_files([ "cases/gateway-vm.nix", "cases/generation-cleanup-absent-network.nix", "cases/guest-qemu-media-spec.nix", + "cases/host-tools-source.nix", "cases/ifname-nix-rust-parity.nix", "cases/index.nix", "cases/interaction-providers.nix", diff --git a/tests/unit/nix/cases/host-tools-source.nix b/tests/unit/nix/cases/host-tools-source.nix new file mode 100644 index 0000000000..754b2afa37 --- /dev/null +++ b/tests/unit/nix/cases/host-tools-source.nix @@ -0,0 +1,41 @@ +# Nix source contract for profile-capable host tools. +# +# The isolated fixture source is the same source root consumed by the Nix +# host-tool builders. Keep the provider schemas in that closure because the +# Rust provider crates embed them with include_str!. +{ lib, flakeRoot, d2bLib, ... }: + +let + schemaPaths = [ + "docs/reference/schemas/v3/providers/transport-azure-relay.transport-settings.json" + "docs/reference/schemas/v3/providers/transport-vsock.transport-binding.json" + ]; + filteredSource = d2bLib.cleanRustPackagesSource flakeRoot; + fixtureSource = + builtins.readFile (flakeRoot + "/bazel/checks/fixtures/BUILD.bazel"); + hostToolsSource = + builtins.readFile (flakeRoot + "/nixos-modules/rust-host-tools.nix"); + hostSourceLines = lib.splitString "\n" hostToolsSource; + hostSourceBuilderLines = + lib.filter (line: lib.hasInfix "src = hostSource;" line) hostSourceLines; +in +{ + "host-tools-source/fixture-declares-provider-schemas" = { + expr = lib.hasInfix ''"//:d2b_resource_schemas_v3"'' fixtureSource; + expected = true; + }; + + "host-tools-source/filtered-source-has-provider-schemas" = { + expr = lib.all + (path: builtins.pathExists (filteredSource + "/${path}")) + schemaPaths; + expected = true; + }; + + "host-tools-source/profile-builders-use-schema-capable-source" = { + expr = lib.hasInfix "cp -r " hostToolsSource + && lib.hasInfix ''packagesSrc}/. "$out/"'' hostToolsSource + && builtins.length hostSourceBuilderLines == 2; + expected = true; + }; +} diff --git a/tests/unit/nix/surfaces/broker-profiles.nix b/tests/unit/nix/surfaces/broker-profiles.nix index 03175f935d..e0cb815081 100644 --- a/tests/unit/nix/surfaces/broker-profiles.nix +++ b/tests/unit/nix/surfaces/broker-profiles.nix @@ -5,6 +5,7 @@ import ../helpers/surface.nix { name = "broker-profiles"; caseFiles = [ ../cases/guest-broker.nix + ../cases/host-tools-source.nix ../cases/prebuilt-broker.nix ]; } From bfb31cc5ec3096cbbef8b328a418326b713bada6 Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 17:19:53 -0700 Subject: [PATCH 06/56] d2bd: add fixed Host and Guest modes Add provider-neutral Guest ComponentSession enrollment, bounded target lifecycle, and mode-bound broker composition while preserving the Host daemon surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1 + changelog.d/2026-08-21-v3-guest-u3.md | 3 + packages/d2b-session-unix/src/vsock.rs | 23 +- packages/d2b-session/src/engine.rs | 82 +- packages/d2bd-runtime/BUILD.bazel | 2 + packages/d2bd-runtime/Cargo.toml | 3 +- packages/d2bd-runtime/src/broker_transport.rs | 122 ++- packages/d2bd-runtime/src/guest_mode.rs | 862 ++++++++++++++++++ packages/d2bd-runtime/src/host_mode.rs | 55 ++ packages/d2bd-runtime/src/lib.rs | 3 + .../src/resource_runtime_support.rs | 40 + packages/d2bd-runtime/src/target_runtime.rs | 744 +++++++++++++++ .../d2bd-runtime/tests/runtime_boundary.rs | 4 + packages/d2bd/BUILD.bazel | 41 + packages/d2bd/Cargo.toml | 2 +- packages/d2bd/src/composition.rs | 263 ++++++ packages/d2bd/src/main.rs | 111 ++- packages/d2bd/src/provider_effects.rs | 104 +++ .../tests/guest_mode_component_session.rs | 162 ++++ packages/d2bd/tests/guest_mode_fail_closed.rs | 68 ++ packages/d2bd/tests/mode_separation.rs | 73 ++ .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 2 +- .../broker-production/production/closure.json | 2 +- .../production/metadata.json | 2 +- .../main-product/policy/closure.json | 2 +- .../main-product/policy/metadata.json | 2 +- .../main-product/production/closure.json | 2 +- .../main-product/production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 2 +- .../broker-production/production/closure.json | 2 +- .../production/metadata.json | 2 +- .../main-product/policy/closure.json | 2 +- .../main-product/policy/metadata.json | 2 +- .../main-product/production/closure.json | 2 +- .../main-product/production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- 69 files changed, 2804 insertions(+), 60 deletions(-) create mode 100644 changelog.d/2026-08-21-v3-guest-u3.md create mode 100644 packages/d2bd-runtime/src/guest_mode.rs create mode 100644 packages/d2bd-runtime/src/host_mode.rs create mode 100644 packages/d2bd-runtime/src/target_runtime.rs create mode 100644 packages/d2bd/tests/guest_mode_component_session.rs create mode 100644 packages/d2bd/tests/guest_mode_fail_closed.rs create mode 100644 packages/d2bd/tests/mode_separation.rs diff --git a/Cargo.lock b/Cargo.lock index e88717eab8..cecdb991f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2020,6 +2020,7 @@ dependencies = [ "d2b-contracts", "d2b-contracts-broker", "d2b-contracts-control", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core", diff --git a/changelog.d/2026-08-21-v3-guest-u3.md b/changelog.d/2026-08-21-v3-guest-u3.md new file mode 100644 index 0000000000..b72f04fd1b --- /dev/null +++ b/changelog.d/2026-08-21-v3-guest-u3.md @@ -0,0 +1,3 @@ +- Add fixed Host and Guest `d2bd` modes with provider-neutral Guest + ComponentSession enrollment, bounded target admission, and mode-bound + broker effects. diff --git a/packages/d2b-session-unix/src/vsock.rs b/packages/d2b-session-unix/src/vsock.rs index 14981fef99..6846b086da 100644 --- a/packages/d2b-session-unix/src/vsock.rs +++ b/packages/d2b-session-unix/src/vsock.rs @@ -9,7 +9,7 @@ use d2b_session::{ TransportWriter, }; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tokio_vsock::{VMADDR_CID_ANY, VsockAddr, VsockListener, VsockStream}; +use tokio_vsock::{VMADDR_CID_ANY, VMADDR_CID_HOST, VsockAddr, VsockListener, VsockStream}; pub struct FramedVsockTransport { stream: S, @@ -350,6 +350,14 @@ impl NativeVsockListener { .map(FramedVsockTransport::new) } + /// Accept exactly the parent Host CID. This is intentionally separate + /// from [`Self::accept`], whose remote-peer API rejects reserved CIDs. + pub async fn accept_host(&mut self) -> Result { + accept_host_expected(&mut self.listener) + .await + .map(FramedVsockTransport::new) + } + pub const fn port(&self) -> u32 { self.port } @@ -388,6 +396,19 @@ where } } +async fn accept_host_expected(listener: &mut A) -> Result +where + A: AcceptOne + Send, +{ + loop { + let (stream, cid, port) = listener.accept_one().await?; + if cid == VMADDR_CID_HOST && port != 0 { + return Ok(stream); + } + drop(stream); + } +} + impl fmt::Debug for NativeVsockListener { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter diff --git a/packages/d2b-session/src/engine.rs b/packages/d2b-session/src/engine.rs index 0d3f7aec4f..0897eed10d 100644 --- a/packages/d2b-session/src/engine.rs +++ b/packages/d2b-session/src/engine.rs @@ -5,8 +5,7 @@ use std::{ time::{Duration, Instant}, }; -use d2b_contracts_zone_session::v3::{ - component_session::{ +use d2b_contracts_zone_session::v3::component_session::{ AttachmentAccess, AttachmentCreditClass, AttachmentDescriptor, AttachmentKind, AttachmentPacket, AttachmentPolicyKind, BoundedVec, CancelAck, CancelRequest, CancelResult, ChannelClass, ChannelId, CloseReason, CloseRecord, EndpointPolicy, EndpointPolicyIdentity, @@ -14,7 +13,6 @@ use d2b_contracts_zone_session::v3::{ KeepaliveRecord, KernelObjectType, LimitProfile, MAX_PACKET_ATTACHMENTS, MetricLabels, MetricReason, MetricResult, NoiseProfile, OperationClass, OperationId, PREFACE_LEN, RecordHeader, RecordKind, Remediation, RequestId, ServicePackage, SessionErrorCode, -}, }; use crate::{ @@ -313,6 +311,49 @@ impl SessionEngine { .await } + /// Establish a responder while accepting a strictly newer reconnect + /// generation from the authenticated offer. All policy fields remain + /// exact; only `reconnect_generation` may advance beyond the supplied + /// floor. + pub async fn establish_responder_with_generation_floor( + transport: T, + policy: EndpointPolicy, + credentials: HandshakeCredentials, + minimum_generation: u64, + now: Instant, + ) -> Result { + if minimum_generation == 0 { + return Err(SessionError::new(SessionErrorCode::GenerationMismatch)); + } + let descriptor = transport.descriptor(); + let metric_policy = policy.clone(); + let timeout = Duration::from_millis(u64::from(policy.limits.handshake_deadline_ms)); + let result = match tokio::time::timeout( + timeout, + Self::establish_responder_inner_with_generation_floor( + transport, + policy, + credentials, + minimum_generation, + now, + ), + ) + .await + { + Ok(result) => result, + Err(_) => Err(SessionError::new(SessionErrorCode::HandshakeTimeout)), + }; + record_establishment( + Arc::new(NoopMetrics).as_ref(), + descriptor, + metric_policy.purpose, + metric_policy.service, + metric_policy.noise_profile, + &result, + ); + result + } + pub async fn establish_responder_with_metrics( transport: T, policy: EndpointPolicy, @@ -344,9 +385,26 @@ impl SessionEngine { } async fn establish_responder_inner( + transport: T, + policy: EndpointPolicy, + credentials: HandshakeCredentials, + now: Instant, + ) -> Result { + Self::establish_responder_inner_with_generation_floor( + transport, + policy, + credentials, + 0, + now, + ) + .await + } + + async fn establish_responder_inner_with_generation_floor( mut transport: T, policy: EndpointPolicy, credentials: HandshakeCredentials, + minimum_generation: u64, now: Instant, ) -> Result { validate_transport(&transport, &policy)?; @@ -374,7 +432,23 @@ impl SessionEngine { return Err(SessionError::new(SessionErrorCode::MalformedPreface)); } - let negotiated = negotiate_offer(&first[..PREFACE_LEN], &first[PREFACE_LEN..], &policy)?; + let offer = HandshakeOffer::decode_canonical(&first[PREFACE_LEN..]) + .map_err(|_| SessionError::new(SessionErrorCode::MalformedHandshake))?; + if minimum_generation != 0 && offer.reconnect_generation < minimum_generation { + return Err(SessionError::new(SessionErrorCode::GenerationMismatch)); + } + let negotiated_policy = if minimum_generation == 0 { + policy.clone() + } else { + EndpointPolicyIdentity::from(&policy) + .with_generation(offer.reconnect_generation) + .map_err(SessionError::from)? + }; + let negotiated = negotiate_offer( + &first[..PREFACE_LEN], + &first[PREFACE_LEN..], + &negotiated_policy, + )?; let mut noise = NoiseHandshake::new(HandshakeRole::Responder, &negotiated, credentials)?; let request = receive_clean(&mut transport, policy.limits.protected_ciphertext_bytes).await?; diff --git a/packages/d2bd-runtime/BUILD.bazel b/packages/d2bd-runtime/BUILD.bazel index a6e43536de..b2f6520c48 100644 --- a/packages/d2bd-runtime/BUILD.bazel +++ b/packages/d2bd-runtime/BUILD.bazel @@ -20,6 +20,7 @@ d2bd_runtime_deps = [ "//packages/d2b-contracts:d2b_contracts", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-contracts-control:d2b_contracts_control", + "//packages/d2b-contracts-provider:d2b_contracts_provider", "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-core:d2b_core", @@ -39,6 +40,7 @@ d2bd_runtime_test_deps = [ "//packages/d2b-contracts:d2b_contracts_test_support", "//packages/d2b-contracts-broker:d2b_contracts_broker", "//packages/d2b-contracts-control:d2b_contracts_control", + "//packages/d2b-contracts-provider:d2b_contracts_provider", "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-core-controller:d2b_core_controller_test_support", diff --git a/packages/d2bd-runtime/Cargo.toml b/packages/d2bd-runtime/Cargo.toml index e91964cf2a..b082e9ed5d 100644 --- a/packages/d2bd-runtime/Cargo.toml +++ b/packages/d2bd-runtime/Cargo.toml @@ -19,6 +19,7 @@ d2b-bus = { path = "../d2b-bus", version = "0.0.0-bootstrap" } d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } d2b-contracts-broker = { path = "../d2b-contracts-broker", version = "0.0.0-bootstrap" } d2b-contracts-control = { path = "../d2b-contracts-control", version = "0.0.0-bootstrap" } +d2b-contracts-provider = { path = "../d2b-contracts-provider", version = "0.0.0-bootstrap" } d2b-contracts-zone-session = { path = "../d2b-contracts-zone-session", version = "0.0.0-bootstrap" } d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } d2b-core = { path = "../d2b-core", version = "0.0.0-bootstrap" } @@ -29,7 +30,7 @@ d2b-resource-store = { path = "../d2b-resource-store", version = "0.0.0-bootstra d2b-resource-store-redb = { path = "../d2b-resource-store-redb", version = "0.0.0-bootstrap" } d2b-realm-core = { path = "../d2b-realm-core", version = "0.0.0-bootstrap" } d2b-session = { path = "../d2b-session", version = "0.0.0-bootstrap" } -d2b-session-unix = { path = "../d2b-session-unix", version = "0.0.0-bootstrap", features = ["host-socket"] } +d2b-session-unix = { path = "../d2b-session-unix", version = "0.0.0-bootstrap", features = ["host-socket", "native-vsock"] } libc = "0.2" getrandom = "0.2" nix = { version = "0.29", features = ["fs", "socket", "uio", "user", "signal", "process", "poll"] } diff --git a/packages/d2bd-runtime/src/broker_transport.rs b/packages/d2bd-runtime/src/broker_transport.rs index 2a187ea7c9..6a85546a80 100644 --- a/packages/d2bd-runtime/src/broker_transport.rs +++ b/packages/d2bd-runtime/src/broker_transport.rs @@ -4,16 +4,20 @@ use std::path::Path; use std::time::{Duration, Instant}; use d2b_contracts_broker::broker_wire::{ - AuditJoinContext, BrokerCallerRole, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, - CanonicalAuditDigest, + AuditJoinContext, BrokerCallerRole, BrokerProfile, BrokerRequest, BrokerRequestEnvelope, + BrokerResponse, CanonicalAuditDigest, }; +use crate::target_runtime::DaemonMode; use crate::typed_error::TypedError; use crate::unix_transport::{ connect_seqpacket, connect_seqpacket_with_timeout, read_frame, write_json_frame, }; use socket2::Socket; +/// NixOS gives every workload Guest its own fixed broker socket basename. +pub const GUEST_BROKER_SOCKET_BASENAME: &str = "guest-broker.sock"; + /// Dispatch one broker request over an owned socket path. /// /// The optional timeout bounds the complete connect, write, and read round @@ -188,3 +192,117 @@ pub fn redact_broker_dispatch_failure_for_launcher(op_name: &str) -> (String, St ), ) } + +/// A fixed, mode-bound broker client. +/// +/// The profile is selected when the daemon composition is created. Requests +/// carry no profile field and are checked against the closed catalog before a +/// socket is opened. +#[derive(Debug, Clone)] +pub struct ModeBoundBrokerAdapter { + mode: DaemonMode, + profile: BrokerProfile, + socket_path: std::path::PathBuf, + caller_role: BrokerCallerRole, +} + +impl ModeBoundBrokerAdapter { + pub fn for_mode( + mode: DaemonMode, + socket_path: impl Into, + daemon_uid: u32, + ) -> Self { + Self { + mode, + profile: mode.broker_profile(), + socket_path: socket_path.into(), + caller_role: BrokerCallerRole::AdminUid { uid: daemon_uid }, + } + } + + pub fn host(socket_path: impl Into, daemon_uid: u32) -> Self { + Self::for_mode(DaemonMode::Host, socket_path, daemon_uid) + } + + pub fn guest(socket_path: impl Into, daemon_uid: u32) -> Self { + Self::for_mode(DaemonMode::Guest, socket_path, daemon_uid) + } + + pub const fn mode(&self) -> DaemonMode { + self.mode + } + + pub const fn profile(&self) -> BrokerProfile { + self.profile + } + + pub fn socket_path(&self) -> &std::path::Path { + &self.socket_path + } + + pub fn validate_instance(&self) -> Result<(), ModeBoundBrokerError> { + if !self.socket_path.is_absolute() { + return Err(ModeBoundBrokerError::SocketPath); + } + if self.mode == DaemonMode::Guest + && self.socket_path.file_name().and_then(|name| name.to_str()) + != Some(GUEST_BROKER_SOCKET_BASENAME) + { + return Err(ModeBoundBrokerError::InstanceMismatch); + } + Ok(()) + } + + /// Dispatch a request only through this adapter's fixed profile. + pub fn dispatch( + &self, + request: BrokerRequest, + timeout: Option, + ) -> Result { + if !self.profile.allows_request(&request) { + return Err(ModeBoundBrokerError::RequestDenied { + profile: self.profile, + operation: request.op_name(), + }); + } + self.validate_instance()?; + dispatch_broker_request_to_socket( + &self.socket_path, + request, + self.caller_role.clone(), + timeout, + ) + .map_err(ModeBoundBrokerError::Transport) + } +} + +/// Fixed broker adapter refusal. +#[derive(Debug)] +pub enum ModeBoundBrokerError { + SocketPath, + InstanceMismatch, + RequestDenied { + profile: BrokerProfile, + operation: &'static str, + }, + Transport(TypedError), +} + +impl std::fmt::Display for ModeBoundBrokerError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SocketPath => formatter.write_str("mode-bound-broker-socket-path"), + Self::InstanceMismatch => formatter.write_str("mode-bound-broker-instance-mismatch"), + Self::RequestDenied { profile, operation } => { + write!( + formatter, + "{}-broker-operation-denied:{operation}", + profile.as_str() + ) + } + Self::Transport(error) => formatter.write_str(error.kind()), + } + } +} + +impl std::error::Error for ModeBoundBrokerError {} diff --git a/packages/d2bd-runtime/src/guest_mode.rs b/packages/d2bd-runtime/src/guest_mode.rs new file mode 100644 index 0000000000..6d0b994cd6 --- /dev/null +++ b/packages/d2bd-runtime/src/guest_mode.rs @@ -0,0 +1,862 @@ +//! Provider-neutral Guest target runtime. +//! +//! Guest mode owns only the enrolled parent-Zone ComponentSession and the +//! target-local ProviderDeployment. It deliberately has no constructor for a +//! local Zone store, public operator socket, realm credential, or Host +//! authority. + +use std::{ + fs, + path::{Path, PathBuf}, + sync::{Arc, Mutex, OnceLock, Weak}, + time::Instant, +}; + +use d2b_contracts_resource::v3::{ + ResourceName, ResourceRef, ResourceTypeName, ResourceUid, SchemaFingerprint, ZoneId, + identity::{ + AuthenticatedSubjectContext, BindingDigest, EvidenceClass, Locality, ReconnectGeneration, + ServiceName, SessionBinding, SessionPurpose, + }, +}; +use d2b_contracts_zone_session::v3::component_session::{ + AttachmentPolicy, ComponentSessionPreface, EndpointPolicy, EndpointPolicyIdentity, + EndpointPurpose, EndpointRole, IdentityEvidenceRequirement, LimitProfile, NoiseProfile, + PurposeClass, ServicePackage, TransportBinding, TransportClass, +}; +use d2b_session::{ + AuthenticatedComponentSession, HandshakeCredentials, Secret32, SessionAcceptor, + SessionAuthenticationBinding, SessionAuthorizationRequest, SessionEngine, TransportEvidence, +}; +use d2b_session_unix::NativeVsockListener; +use sha2::{Digest, Sha256}; + +use crate::{ + broker_transport::{ModeBoundBrokerAdapter, ModeBoundBrokerError}, + target_runtime::{ + AdmissionBudget, AdmissionError, AdmissionKind, AdmissionLimits, AssignmentLease, + ControllerAssignmentKey, DaemonMode, DeploymentError, ProviderDeployment, + }, +}; + +/// The enrolled Guest ComponentSession listener port. +pub const GUEST_COMPONENT_SESSION_PORT: u32 = 14_318; +/// The kernel source of truth for boot identity. +pub const KERNEL_BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id"; +/// The fixed enrolled session purpose. +pub const GUEST_COMPONENT_SESSION_PURPOSE: &str = "zone-link"; +/// The parent-Zone service package carried by the session. +pub const GUEST_COMPONENT_SESSION_SERVICE: ServicePackage = ServicePackage::ResourceV3; +/// The session schema domain used by the Guest target agent. +pub const GUEST_COMPONENT_SESSION_SCHEMA_DOMAIN: &[u8] = b"d2b-guest-component-session-v3"; + +/// Reject the retired feature-specific guest-control prelude before any +/// feature payload or per-session state is allocated. +pub fn reject_legacy_guest_control_prelude(bytes: &[u8]) -> Result<(), GuestModeError> { + if bytes.starts_with(crate::guest_control_vsock::GUEST_CONTROL_CONNECT_LINE) + || bytes.starts_with(b"D2BGC") + { + return Err(GuestModeError::OldProtocol); + } + if bytes.len() < d2b_contracts_zone_session::v3::component_session::PREFACE_LEN { + return Err(GuestModeError::OldProtocol); + } + ComponentSessionPreface::parse( + &bytes[..d2b_contracts_zone_session::v3::component_session::PREFACE_LEN], + ) + .map(|_| ()) + .map_err(|_| GuestModeError::OldProtocol) +} + +/// A kernel-derived boot identity. The raw boot-id text never leaves the +/// constructor; only its domain-separated digest participates in binding. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct BootIdentity([u8; 32]); + +impl BootIdentity { + pub fn from_kernel_boot_id(value: &str) -> Result { + let value = value.trim(); + if value.is_empty() || value.len() > 128 || !value.is_ascii() { + return Err(GuestModeError::BootIdentityInvalid); + } + let mut digest = Sha256::new(); + digest.update(b"d2b-kernel-boot-id-v1\0"); + digest.update(value.as_bytes()); + Ok(Self(digest.finalize().into())) + } + + pub fn read(path: impl AsRef) -> Result { + let value = + fs::read_to_string(path).map_err(|_| GuestModeError::BootIdentityUnavailable)?; + Self::from_kernel_boot_id(&value) + } + + pub const fn digest(self) -> [u8; 32] { + self.0 + } +} + +impl std::fmt::Debug for BootIdentity { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("BootIdentity()") + } +} + +/// The complete trusted identity a Guest must bind to its parent session. +#[derive(Clone, PartialEq, Eq)] +pub struct GuestIdentity { + guest_ref: ResourceRef, + guest_uid: ResourceUid, + zone: ZoneId, + boot_identity: BootIdentity, + purpose: SessionPurpose, + schema_fingerprint: SchemaFingerprint, + reconnect_generation: ReconnectGeneration, + provider_generation: u64, + controller_generation: u64, + assignment_epoch: u64, +} + +impl GuestIdentity { + #[allow(clippy::too_many_arguments)] + pub fn new( + guest_ref: ResourceRef, + guest_uid: ResourceUid, + zone: ZoneId, + boot_identity: BootIdentity, + purpose: SessionPurpose, + schema_fingerprint: SchemaFingerprint, + reconnect_generation: ReconnectGeneration, + provider_generation: u64, + controller_generation: u64, + assignment_epoch: u64, + ) -> Result { + if guest_ref.resource_type().as_str() != "Guest" { + return Err(GuestModeError::GuestIdentityWrongKind); + } + if provider_generation == 0 || controller_generation == 0 || assignment_epoch == 0 { + return Err(GuestModeError::GenerationZero); + } + if purpose.as_str() != GUEST_COMPONENT_SESSION_PURPOSE { + return Err(GuestModeError::PurposeMismatch); + } + Ok(Self { + guest_ref, + guest_uid, + zone, + boot_identity, + purpose, + schema_fingerprint, + reconnect_generation, + provider_generation, + controller_generation, + assignment_epoch, + }) + } + + pub fn guest_ref(&self) -> &ResourceRef { + &self.guest_ref + } + + pub fn guest_uid(&self) -> &ResourceUid { + &self.guest_uid + } + + pub fn zone(&self) -> &ZoneId { + &self.zone + } + + pub fn boot_identity(&self) -> BootIdentity { + self.boot_identity + } + + pub fn purpose(&self) -> &SessionPurpose { + &self.purpose + } + + pub fn schema_fingerprint(&self) -> &SchemaFingerprint { + &self.schema_fingerprint + } + + pub const fn reconnect_generation(&self) -> ReconnectGeneration { + self.reconnect_generation + } + + pub const fn provider_generation(&self) -> u64 { + self.provider_generation + } + + pub const fn controller_generation(&self) -> u64 { + self.controller_generation + } + + pub const fn assignment_epoch(&self) -> u64 { + self.assignment_epoch + } + + /// Domain-separated binding for the enrolled Guest identity and + /// ComponentSession transcript. Generation is a separate exact session + /// field so reconnects can advance without changing the enrolled binding. + pub fn channel_binding(&self) -> [u8; 32] { + let mut digest = Sha256::new(); + digest.update(b"d2b-guest-component-session-binding-v3\0"); + digest.update(self.guest_ref.to_canonical_string().as_bytes()); + digest.update(self.guest_uid.as_str().as_bytes()); + digest.update(self.zone.as_str().as_bytes()); + digest.update(self.purpose.as_str().as_bytes()); + digest.update(self.schema_fingerprint.as_str().as_bytes()); + digest.update(self.boot_identity.0); + digest.update(self.provider_generation.to_be_bytes()); + digest.update(self.controller_generation.to_be_bytes()); + digest.update(self.assignment_epoch.to_be_bytes()); + digest.finalize().into() + } + + pub fn channel_binding_for_generation(&self, _generation: u64) -> [u8; 32] { + self.channel_binding() + } + + /// Build the exact enrolled ComponentSession policy expected from the + /// parent Zone. No caller-controlled field is accepted at admission time. + pub fn endpoint_policy(&self) -> EndpointPolicy { + self.endpoint_policy_for_generation(self.reconnect_generation.get()) + } + + pub fn endpoint_policy_for_generation(&self, generation: u64) -> EndpointPolicy { + EndpointPolicy { + purpose: EndpointPurpose::ZoneLink, + purpose_class: PurposeClass::Enrolled, + initiator_role: EndpointRole::ZoneController, + responder_role: EndpointRole::GuestAgent, + service: GUEST_COMPONENT_SESSION_SERVICE, + schema_fingerprint: digest_bytes(self.schema_fingerprint.as_str()), + noise_profile: NoiseProfile::Kk25519ChaChaPolySha256, + limits: LimitProfile::remote_default(), + transport_binding: TransportBinding { + transport: TransportClass::NativeVsock, + locality: d2b_contracts_zone_session::v3::component_session::Locality::GuestLocal, + channel_binding: self.channel_binding(), + identity_evidence: IdentityEvidenceRequirement::EnrolledStaticKeys, + }, + reconnect_generation: generation, + attachment_policy: AttachmentPolicy::disabled(), + } + } + + /// Validate the redacted route metadata produced by ComponentSession + /// admission before any ProviderDeployment state is allocated. + pub fn validate_route( + &self, + binding: &d2b_session::AuthenticatedSessionRouteBinding, + ) -> Result<(), GuestModeError> { + let generation = binding.reconnect_generation().get(); + if generation < self.reconnect_generation.get() + || binding.zone() != &self.zone + || binding.subject_ref() != &self.guest_ref + || binding.subject_uid() != &self.guest_uid + || binding.schema() != &self.schema_fingerprint + || binding.service().as_str() != "d2b.resource.v3" + || binding.context().session_purpose() != &self.purpose + || binding.locality() != Locality::Local + || binding.evidence_class() != EvidenceClass::EnrolledKk + { + return Err(GuestModeError::SessionBindingMismatch); + } + let expected = + BindingDigest::parse(format!("sha256:{}", hex_digest(self.channel_binding()))) + .map_err(|_| GuestModeError::SessionBindingMismatch)?; + if binding.context().transport_binding().binding_digest() != &expected { + return Err(GuestModeError::SessionBindingMismatch); + } + Ok(()) + } +} + +impl std::fmt::Debug for GuestIdentity { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("GuestIdentity()") + } +} + +fn digest_bytes(value: &str) -> [u8; 32] { + let raw = value.strip_prefix("sha256:").unwrap_or(value); + let mut bytes = [0_u8; 32]; + for (index, slot) in bytes.iter_mut().enumerate() { + *slot = u8::from_str_radix(&raw[index * 2..index * 2 + 2], 16) + .expect("validated SHA-256 fingerprint"); + } + + bytes +} + +fn hex_digest(value: [u8; 32]) -> String { + value.iter().map(|byte| format!("{byte:02x}")).collect() +} + +/// A Guest session lease. Dropping it closes the ComponentSession generation +/// and revokes all target-local controller assignments bound to it. +#[derive(Debug)] +pub struct GuestSessionLease { + runtime: Weak, + generation: u64, + _session_permit: crate::target_runtime::AdmissionPermit, +} + +impl GuestSessionLease { + pub const fn generation(&self) -> u64 { + self.generation + } +} + +impl Drop for GuestSessionLease { + fn drop(&mut self) { + if let Some(runtime) = self.runtime.upgrade() { + let _ = runtime.close_generation(self.generation); + } + } +} + +/// Admission reserved before a ComponentSession handshake allocates engine, +/// Noise, or per-session request state. +#[derive(Debug)] +pub struct GuestHandshakeAdmission { + session_permit: crate::target_runtime::AdmissionPermit, + reconnect_permit: Option, +} + +#[derive(Debug)] +struct GuestRuntimeInner { + identity: GuestIdentity, + deployment: ProviderDeployment, + admission: AdmissionBudget, + broker: ModeBoundBrokerAdapter, + active_generation: Mutex>, + active_session_permit: Mutex>, +} + +/// Provider-neutral Guest runtime. It owns no local Zone store or public +/// operator surface. +#[derive(Debug, Clone)] +pub struct GuestRuntime { + inner: Arc, +} + +impl GuestRuntime { + pub fn new( + identity: GuestIdentity, + broker_socket: PathBuf, + broker_uid: u32, + limits: AdmissionLimits, + ) -> Result { + let admission = AdmissionBudget::new(limits).map_err(GuestModeError::Admission)?; + let deployment = ProviderDeployment::new(DaemonMode::Guest, limits) + .map_err(GuestModeError::Admission)?; + let broker = ModeBoundBrokerAdapter::guest(broker_socket, broker_uid); + broker.validate_instance().map_err(GuestModeError::Broker)?; + Ok(Self { + inner: Arc::new(GuestRuntimeInner { + identity, + deployment, + admission, + broker, + active_generation: Mutex::new(None), + active_session_permit: Mutex::new(None), + }), + }) + } + + pub fn identity(&self) -> &GuestIdentity { + &self.inner.identity + } + + pub fn deployment(&self) -> &ProviderDeployment { + &self.inner.deployment + } + + pub fn broker(&self) -> &ModeBoundBrokerAdapter { + &self.inner.broker + } + + pub const fn component_session_port() -> u32 { + GUEST_COMPONENT_SESSION_PORT + } + + pub const fn surfaces(&self) -> crate::target_runtime::ModeSurfaces { + DaemonMode::Guest.surfaces() + } + + /// Host authority surfaces are deliberately unavailable in Guest mode. + pub fn local_zone_store(&self) -> Result<(), GuestModeError> { + Err(GuestModeError::HostSurfaceUnavailable) + } + + pub fn public_operator_socket(&self) -> Result<(), GuestModeError> { + Err(GuestModeError::HostSurfaceUnavailable) + } + + pub fn realm_credentials(&self) -> Result<(), GuestModeError> { + Err(GuestModeError::HostSurfaceUnavailable) + } + + /// Admit a route only after identity, boot, Zone, purpose, schema, and + /// generation binding has passed. Reconnects require a new generation and + /// are bounded before replacing state. + pub fn admit_route( + &self, + binding: &d2b_session::AuthenticatedSessionRouteBinding, + ) -> Result { + let admission = self.admit_handshake()?; + self.admit_route_with_handshake(binding, admission) + } + + /// Reserve the bounded session/reconnect budget before reading or + /// allocating a ComponentSession handshake. + pub fn admit_handshake(&self) -> Result { + let active = self + .inner + .active_generation + .lock() + .map_err(|_| GuestModeError::StateUnavailable)?; + let reconnect_permit = if active.is_some() { + Some( + self.inner + .admission + .try_admit_reconnect(Instant::now()) + .map_err(GuestModeError::Admission)?, + ) + } else { + None + }; + drop(active); + let session_permit = self + .inner + .admission + .try_admit(AdmissionKind::Session) + .map_err(GuestModeError::Admission)?; + Ok(GuestHandshakeAdmission { + session_permit, + reconnect_permit, + }) + } + + fn admit_route_with_handshake( + &self, + binding: &d2b_session::AuthenticatedSessionRouteBinding, + admission: GuestHandshakeAdmission, + ) -> Result { + self.inner.identity.validate_route(binding)?; + let generation = binding.reconnect_generation().get(); + self.admit_generation_with_handshake(generation, admission) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn admit_generation_for_tests( + &self, + generation: u64, + ) -> Result { + if generation < self.inner.identity.reconnect_generation.get() { + return Err(GuestModeError::StaleSession); + } + let admission = self.admit_handshake()?; + self.admit_generation_with_handshake(generation, admission) + } + + fn admit_generation_with_handshake( + &self, + generation: u64, + admission: GuestHandshakeAdmission, + ) -> Result { + let mut active = self + .inner + .active_generation + .lock() + .map_err(|_| GuestModeError::StateUnavailable)?; + if let Some(previous) = *active { + if generation <= previous { + return Err(GuestModeError::StaleSession); + } + self.inner + .deployment + .revoke_session(previous) + .map_err(GuestModeError::Deployment)?; + } else if admission.reconnect_permit.is_some() { + return Err(GuestModeError::StaleSession); + } + { + let mut active_permit = self + .inner + .active_session_permit + .lock() + .map_err(|_| GuestModeError::StateUnavailable)?; + if let Some(permit) = active_permit.take() { + permit.release(); + } + *active_permit = Some(admission.session_permit.clone()); + } + *active = Some(generation); + Ok(GuestSessionLease { + runtime: Arc::downgrade(&self.inner), + generation, + _session_permit: admission.session_permit, + }) + } + + /// Close the current session and revoke its target-local assignments. + pub fn close_session(&self, generation: u64) -> Result<(), GuestModeError> { + self.inner.close_generation(generation) + } + + /// Admit a controller assignment only for this Guest's active session. + pub fn admit_assignment( + &self, + key: ControllerAssignmentKey, + ) -> Result { + let active = self + .inner + .active_generation + .lock() + .map_err(|_| GuestModeError::StateUnavailable)?; + if *active != Some(key.session_generation) { + return Err(GuestModeError::StaleSession); + } + self.inner + .deployment + .admit_assignment(key) + .map_err(GuestModeError::Deployment) + } + + /// Establish a real authenticated responder ComponentSession over the + /// fixed native vsock transport. The returned session is not a resource + /// authority until its route lease is retained by the caller. + pub async fn establish_component_session( + &self, + transport: T, + local_private: Secret32, + parent_public: [u8; 32], + ) -> Result<(AuthenticatedComponentSession<()>, GuestSessionLease), GuestModeError> + where + T: d2b_session::OwnedTransport + 'static, + { + let admission = self.admit_handshake()?; + let identity = self.inner.identity.clone(); + let policy = identity.endpoint_policy(); + let engine = SessionEngine::establish_responder_with_generation_floor( + transport, + policy.clone(), + HandshakeCredentials::Kk { + local_private, + remote_public: parent_public, + }, + identity.reconnect_generation().get(), + Instant::now(), + ) + .await + .map_err(GuestModeError::Session)?; + let generation = engine.generation(); + let acceptor_policy = EndpointPolicyIdentity::from(&identity.endpoint_policy()) + .with_generation(generation) + .map_err(|_| GuestModeError::SessionBindingMismatch)?; + let expected = identity.clone(); + let authorize_identity = identity.clone(); + let acceptor = SessionAcceptor::from_verified_adapter( + acceptor_policy, + identity.zone.clone(), + move |_evidence, binding, _expected_zone, now_tick| { + authenticate_guest_subject(&expected, binding, now_tick) + }, + move |subject, request, previous, now_tick| { + authorize_guest_request(&authorize_identity, subject, request, previous, now_tick) + }, + (), + ) + .map_err(GuestModeError::Session)?; + let session = acceptor + .admit( + engine, + TransportEvidence::new( + EvidenceClass::EnrolledKk, + BindingDigest::parse(format!( + "sha256:{}", + hex_digest(identity.channel_binding()) + )) + .map_err(|_| GuestModeError::SessionBindingMismatch)?, + ), + monotonic_tick(), + ) + .await + .map_err(GuestModeError::Session)?; + let route = session.route_binding(); + let lease = self.admit_route_with_handshake(&route, admission)?; + Ok((session, lease)) + } + + /// Bind the fixed Guest listener. Host mode has no equivalent entrypoint. + pub fn bind_listener(&self) -> Result { + NativeVsockListener::bind(GUEST_COMPONENT_SESSION_PORT).map_err(GuestModeError::Transport) + } + + /// Accept only the parent Host CID on the fixed Guest listener. + pub async fn accept_from_parent( + &self, + listener: &mut NativeVsockListener, + local_private: Secret32, + parent_public: [u8; 32], + ) -> Result<(AuthenticatedComponentSession<()>, GuestSessionLease), GuestModeError> { + let transport = listener + .accept_host() + .await + .map_err(GuestModeError::Transport)?; + self.establish_component_session(transport, local_private, parent_public) + .await + } +} + +impl GuestRuntimeInner { + fn close_generation(&self, generation: u64) -> Result<(), GuestModeError> { + let mut active = self + .active_generation + .lock() + .map_err(|_| GuestModeError::StateUnavailable)?; + if *active == Some(generation) { + self.deployment + .revoke_session(generation) + .map_err(GuestModeError::Deployment)?; + if let Ok(mut permit) = self.active_session_permit.lock() { + if let Some(permit) = permit.take() { + permit.release(); + } + } + *active = None; + } + Ok(()) + } +} + +fn authenticate_guest_subject( + identity: &GuestIdentity, + binding: &SessionAuthenticationBinding, + now_tick: u64, +) -> d2b_session::Result<( + AuthenticatedSubjectContext, + d2b_contracts_zone_session::v3::component_session::AuthorizationLease, +)> { + if binding.purpose() != identity.purpose() + || binding.schema_fingerprint() != identity.schema_fingerprint() + || binding.reconnect_generation().get() < identity.reconnect_generation.get() + || binding.transport_binding().locality() != Locality::Local + || binding.transport_binding().binding_digest() + != &BindingDigest::parse(format!("sha256:{}", hex_digest(identity.channel_binding()))) + .map_err(|_| { + d2b_session::SessionError::new( + d2b_session::contract::SessionErrorCode::PolicyDenied, + ) + })? + { + return Err(d2b_session::SessionError::new( + d2b_session::contract::SessionErrorCode::PolicyDenied, + )); + } + let session = SessionBinding::new( + identity.schema_fingerprint.clone(), + binding.transport_binding().clone(), + binding.reconnect_generation(), + binding.transcript_hash().clone(), + ); + let context = AuthenticatedSubjectContext::new( + identity.guest_ref.clone(), + identity.guest_uid.clone(), + ResourceRef::new( + ResourceTypeName::parse("Zone").expect("Zone type"), + ResourceName::parse(identity.zone.as_str()).expect("Zone name"), + ), + EvidenceClass::EnrolledKk, + identity.purpose.clone(), + ServiceName::parse("d2b.resource.v3").expect("resource service"), + session, + ) + .with_execution_ref(identity.guest_ref.clone()) + .with_provider_generation( + d2b_contracts_resource::v3::ResourceGeneration::new(identity.provider_generation).map_err( + |_| { + d2b_session::SessionError::new( + d2b_session::contract::SessionErrorCode::PolicyDenied, + ) + }, + )?, + ) + .with_controller_generation( + d2b_contracts_resource::v3::ControllerGeneration::new(identity.controller_generation) + .map_err(|_| { + d2b_session::SessionError::new( + d2b_session::contract::SessionErrorCode::PolicyDenied, + ) + })?, + ); + let expiry = now_tick.checked_add(60_000).ok_or_else(|| { + d2b_session::SessionError::new(d2b_session::contract::SessionErrorCode::PolicyDenied) + })?; + let lease = + d2b_contracts_zone_session::v3::component_session::AuthorizationLease::new(1, expiry) + .map_err(|_| { + d2b_session::SessionError::new( + d2b_session::contract::SessionErrorCode::PolicyDenied, + ) + })?; + Ok((context, lease)) +} + +fn authorize_guest_request( + identity: &GuestIdentity, + subject: &AuthenticatedSubjectContext, + request: &SessionAuthorizationRequest, + previous: d2b_contracts_zone_session::v3::component_session::AuthorizationLease, + now_tick: u64, +) -> d2b_session::Result { + use d2b_resource_api::authz::SessionVerb; + if subject.subject_ref() != &identity.guest_ref + || request.target_zone() != &identity.zone + || !matches!( + request.verb(), + SessionVerb::Invoke + | SessionVerb::OpenStream + | SessionVerb::Observe + | SessionVerb::Cancel + ) + || !previous.is_valid_at(now_tick) + { + return Err(d2b_session::SessionError::new( + d2b_session::contract::SessionErrorCode::PolicyDenied, + )); + } + Ok(previous) +} + +fn monotonic_tick() -> u64 { + static START: OnceLock = OnceLock::new(); + START + .get_or_init(Instant::now) + .elapsed() + .as_millis() + .try_into() + .unwrap_or(1) + .max(1) +} + +/// Guest-mode failures are closed and identity-free. +#[derive(Debug)] +pub enum GuestModeError { + Admission(AdmissionError), + Deployment(DeploymentError), + GuestIdentityWrongKind, + BootIdentityInvalid, + BootIdentityUnavailable, + GenerationZero, + PurposeMismatch, + SessionBindingMismatch, + StaleSession, + StateUnavailable, + HostSurfaceUnavailable, + OldProtocol, + Broker(ModeBoundBrokerError), + Session(d2b_session::SessionError), + Transport(d2b_session::TransportError), +} + +impl std::fmt::Display for GuestModeError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Admission(error) => return error.fmt(formatter), + Self::Deployment(error) => return error.fmt(formatter), + Self::GuestIdentityWrongKind => "guest-mode-identity-wrong-kind", + Self::BootIdentityInvalid => "guest-mode-boot-identity-invalid", + Self::BootIdentityUnavailable => "guest-mode-boot-identity-unavailable", + Self::GenerationZero => "guest-mode-generation-zero", + Self::PurposeMismatch => "guest-mode-purpose-mismatch", + Self::SessionBindingMismatch => "guest-mode-session-binding-mismatch", + Self::StaleSession => "guest-mode-stale-session", + Self::StateUnavailable => "guest-mode-state-unavailable", + Self::HostSurfaceUnavailable => "guest-mode-host-surface-unavailable", + Self::OldProtocol => "guest-mode-old-protocol", + Self::Broker(error) => return error.fmt(formatter), + Self::Session(error) => return error.fmt(formatter), + Self::Transport(_) => "guest-mode-transport-failed", + }) + } +} + +impl std::error::Error for GuestModeError {} + +#[cfg(test)] +mod tests { + use super::*; + use d2b_contracts_resource::v3::identity::ReconnectGeneration; + + fn identity(generation: u64) -> GuestIdentity { + GuestIdentity::new( + ResourceRef::parse("Guest/workload").expect("guest"), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").expect("uid"), + ZoneId::parse("work").expect("zone"), + BootIdentity::from_kernel_boot_id("boot-id-for-test").expect("boot"), + SessionPurpose::parse(GUEST_COMPONENT_SESSION_PURPOSE).expect("purpose"), + SchemaFingerprint::parse( + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + ) + .expect("schema"), + ReconnectGeneration::new(generation).expect("generation"), + 1, + 1, + 1, + ) + .expect("identity") + } + + #[test] + fn boot_identity_is_kernel_derived_and_redacted() { + let first = BootIdentity::from_kernel_boot_id("boot-a").expect("boot"); + let second = BootIdentity::from_kernel_boot_id("boot-b").expect("boot"); + assert_ne!(first, second); + assert!(!format!("{first:?}").contains("boot-a")); + } + + #[test] + fn guest_policy_binds_zone_purpose_schema_boot_and_generation() { + let identity = identity(7); + let policy = identity.endpoint_policy(); + assert_eq!(policy.reconnect_generation, 7); + assert_eq!(policy.purpose, EndpointPurpose::ZoneLink); + assert_eq!(policy.service, ServicePackage::ResourceV3); + assert_eq!( + policy.transport_binding.transport, + TransportClass::NativeVsock + ); + assert_ne!(policy.transport_binding.channel_binding, [0; 32]); + } + + #[test] + fn guest_runtime_exposes_no_host_authority_surfaces() { + let identity = identity(1); + let runtime = GuestRuntime::new( + identity, + PathBuf::from("/run/d2b/guest-broker.sock"), + 997, + AdmissionLimits::guest_default(), + ) + .expect("runtime"); + assert!(!runtime.surfaces().local_zone_store); + assert!(runtime.local_zone_store().is_err()); + assert!(runtime.public_operator_socket().is_err()); + assert!(runtime.realm_credentials().is_err()); + } + + #[test] + fn retired_guest_control_prelude_is_rejected_before_allocation() { + assert!(matches!( + reject_legacy_guest_control_prelude(b"CONNECT 14318\n"), + Err(GuestModeError::OldProtocol) + )); + assert!(matches!( + reject_legacy_guest_control_prelude(b"D2BGC-old"), + Err(GuestModeError::OldProtocol) + )); + } +} diff --git a/packages/d2bd-runtime/src/host_mode.rs b/packages/d2bd-runtime/src/host_mode.rs new file mode 100644 index 0000000000..ec868e0310 --- /dev/null +++ b/packages/d2bd-runtime/src/host_mode.rs @@ -0,0 +1,55 @@ +//! Provider-neutral Host target runtime boundary. +//! +//! Host mode keeps the existing d2bd composition responsible for the Zone +//! store, public operator socket, realm routing, and Host-target Provider +//! assignments. The type is intentionally small: the existing Host startup +//! path remains the authoritative implementation, while this module gives +//! shared code an explicit mode-bound surface contract. + +use std::path::PathBuf; + +use crate::target_runtime::{AdmissionLimits, DaemonMode, ModeSurfaces, ProviderDeployment}; + +/// Host-only startup inputs. Guest mode has no conversion from this type. +#[derive(Debug, Clone)] +pub struct HostRuntimeConfig { + pub public_socket: PathBuf, + pub broker_socket: PathBuf, + pub state_dir: PathBuf, + pub realm_identity: PathBuf, +} + +/// Provider-neutral Host runtime descriptor used by static composition. +#[derive(Debug, Clone)] +pub struct HostRuntime { + deployment: ProviderDeployment, + config: HostRuntimeConfig, +} + +impl HostRuntime { + pub fn new( + config: HostRuntimeConfig, + limits: AdmissionLimits, + ) -> Result { + Ok(Self { + deployment: ProviderDeployment::new(DaemonMode::Host, limits)?, + config, + }) + } + + pub const fn mode(&self) -> DaemonMode { + DaemonMode::Host + } + + pub const fn surfaces(&self) -> ModeSurfaces { + DaemonMode::Host.surfaces() + } + + pub fn deployment(&self) -> &ProviderDeployment { + &self.deployment + } + + pub fn config(&self) -> &HostRuntimeConfig { + &self.config + } +} diff --git a/packages/d2bd-runtime/src/lib.rs b/packages/d2bd-runtime/src/lib.rs index f554d02e87..f6919adaa0 100644 --- a/packages/d2bd-runtime/src/lib.rs +++ b/packages/d2bd-runtime/src/lib.rs @@ -26,6 +26,8 @@ pub mod guest_control_health; pub mod guest_control_bridge; pub mod guest_control_runtime; pub mod guest_control_vsock; +pub mod guest_mode; +pub mod host_mode; pub mod json_io; pub mod kernel_module_check; pub mod known_hosts_refresh; @@ -47,6 +49,7 @@ pub mod shell_backend; pub mod ssh_host_key_preflight; pub mod supervisor; pub mod terminal_session; +pub mod target_runtime; pub mod typed_error; pub mod typed_shell_targets; pub mod unix_transport; diff --git a/packages/d2bd-runtime/src/resource_runtime_support.rs b/packages/d2bd-runtime/src/resource_runtime_support.rs index 47f1c86be9..dfe5e4833c 100644 --- a/packages/d2bd-runtime/src/resource_runtime_support.rs +++ b/packages/d2bd-runtime/src/resource_runtime_support.rs @@ -62,6 +62,10 @@ use d2b_core_controller::{ }, zone_status::ZoneRuntimeMetadata, }; +use crate::target_runtime::{ + AdmissionError, AdmissionLimits, AssignmentLease, ControllerAssignmentKey, DaemonMode, + DeploymentError, ProviderDeployment, +}; /// Provider-neutral Core assignment registry shared by Resource API and bus /// admission for one Zone runtime. @@ -71,6 +75,42 @@ pub type AssignmentRegistry = Arc>; pub fn new_assignment_registry() -> AssignmentRegistry { Arc::new(Mutex::new(ControllerAssignmentRegistry::default())) } + +/// Shared target-local resource lifecycle owner. +/// +/// Host and Guest use the same assignment/lease machinery; only the static +/// composition chooses the mode and its effect adapter. +#[derive(Debug, Clone)] +pub struct TargetResourceLifecycle { + deployment: ProviderDeployment, +} + +impl TargetResourceLifecycle { + pub fn new(mode: DaemonMode, limits: AdmissionLimits) -> Result { + Ok(Self { + deployment: ProviderDeployment::new(mode, limits)?, + }) + } + + pub const fn mode(&self) -> DaemonMode { + self.deployment.mode() + } + + pub fn admit_controller( + &self, + assignment: ControllerAssignmentKey, + ) -> Result { + self.deployment.admit_assignment(assignment) + } + + pub fn revoke_session(&self, generation: u64) -> Result { + self.deployment.revoke_session(generation) + } + + pub fn active_assignments(&self) -> Result { + self.deployment.active_assignments() + } +} use d2b_resource_api::{ RedbBackend, ResourceApiClient, ResourceBusAdapter, ResourceService, authz::{ diff --git a/packages/d2bd-runtime/src/target_runtime.rs b/packages/d2bd-runtime/src/target_runtime.rs new file mode 100644 index 0000000000..d2460ae8e1 --- /dev/null +++ b/packages/d2bd-runtime/src/target_runtime.rs @@ -0,0 +1,744 @@ +//! Provider-neutral target runtime and bounded admission contracts. +//! +//! The composition crate selects concrete Providers and effect adapters. This +//! module owns the authority-neutral lifecycle that both Host and Guest +//! instances use: fixed process-start mode, bounded admission, target-scoped +//! controller assignments, and revocation on session loss. + +use std::{ + collections::{BTreeMap, VecDeque}, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicUsize, Ordering}, + }, + time::{Duration, Instant}, +}; + +use d2b_contracts_broker::broker_wire::BrokerProfile; +use d2b_contracts_resource::v3::{ResourceRef, ZoneId}; + +/// The only daemon modes. A mode is selected before the runtime starts and is +/// never read from a request or changed on a live instance. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DaemonMode { + Host, + Guest, +} + +impl DaemonMode { + /// Stable process-start spelling. + pub const fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::Guest => "guest", + } + } + + /// Parse only the two closed process-start spellings. + pub fn parse(value: &str) -> Result { + match value { + "host" => Ok(Self::Host), + "guest" => Ok(Self::Guest), + _ => Err(ModeParseError::UnknownMode), + } + } + + /// The broker profile bound to this daemon mode. + pub const fn broker_profile(self) -> BrokerProfile { + match self { + Self::Host => BrokerProfile::Host, + Self::Guest => BrokerProfile::Guest, + } + } + + /// The execution target kind owned by this daemon mode. + pub const fn target_kind(self) -> TargetKind { + match self { + Self::Host => TargetKind::Host, + Self::Guest => TargetKind::Guest, + } + } + + /// Surfaces that may be initialized by the mode. + pub const fn surfaces(self) -> ModeSurfaces { + match self { + Self::Host => ModeSurfaces { + local_zone_store: true, + public_operator_socket: true, + realm_credentials: true, + host_controller_authority: true, + parent_component_session: false, + }, + Self::Guest => ModeSurfaces { + local_zone_store: false, + public_operator_socket: false, + realm_credentials: false, + host_controller_authority: false, + parent_component_session: true, + }, + } + } +} + +impl std::fmt::Display for DaemonMode { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// Closed mode parser failures. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModeParseError { + UnknownMode, +} + +impl std::fmt::Display for ModeParseError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("daemon-mode-unknown") + } +} + +impl std::error::Error for ModeParseError {} + +/// Target kinds accepted by the shared ProviderDeployment lifecycle. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TargetKind { + Host, + Guest, +} + +impl TargetKind { + pub const fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::Guest => "guest", + } + } +} + +/// Authority-bearing surfaces a mode is allowed to initialize. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ModeSurfaces { + pub local_zone_store: bool, + pub public_operator_socket: bool, + pub realm_credentials: bool, + pub host_controller_authority: bool, + pub parent_component_session: bool, +} + +impl ModeSurfaces { + pub const fn host() -> Self { + DaemonMode::Host.surfaces() + } + + pub const fn guest() -> Self { + DaemonMode::Guest.surfaces() + } +} + +/// Bounded admission classes used before per-request state is allocated. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AdmissionKind { + Session, + Reconnect, + Controller, + Watch, + Stream, +} + +impl AdmissionKind { + pub const fn as_str(self) -> &'static str { + match self { + Self::Session => "session", + Self::Reconnect => "reconnect", + Self::Controller => "controller", + Self::Watch => "watch", + Self::Stream => "stream", + } + } +} + +/// Fixed admission budgets for a target runtime. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AdmissionLimits { + pub max_sessions: usize, + pub max_reconnects_per_window: usize, + pub max_controllers: usize, + pub max_watches: usize, + pub max_streams: usize, + pub reconnect_window: Duration, +} + +impl AdmissionLimits { + /// Conservative Guest defaults. Host callers may use a larger explicit + /// profile, but zero is never treated as unlimited. + pub const fn guest_default() -> Self { + Self { + max_sessions: 4, + max_reconnects_per_window: 8, + max_controllers: 32, + max_watches: 64, + max_streams: 128, + reconnect_window: Duration::from_secs(60), + } + } + + /// Host defaults share the same machinery while allowing the normal + /// operator-facing concurrency envelope. + pub const fn host_default() -> Self { + Self { + max_sessions: 64, + max_reconnects_per_window: 64, + max_controllers: 256, + max_watches: 512, + max_streams: 1024, + reconnect_window: Duration::from_secs(60), + } + } + + fn validate(self) -> Result { + if self.max_sessions == 0 + || self.max_reconnects_per_window == 0 + || self.max_controllers == 0 + || self.max_watches == 0 + || self.max_streams == 0 + || self.reconnect_window.is_zero() + { + return Err(AdmissionError::InvalidLimits); + } + Ok(self) + } +} + +/// Bounded admission failure. The class is a closed label, never a caller +/// supplied diagnostic string. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AdmissionError { + InvalidLimits, + LimitExceeded(AdmissionKind), + ReconnectWindowUnavailable, +} + +impl std::fmt::Display for AdmissionError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidLimits => formatter.write_str("target-runtime-invalid-limits"), + Self::LimitExceeded(kind) => { + write!(formatter, "target-runtime-{}-limit", kind.as_str()) + } + Self::ReconnectWindowUnavailable => { + formatter.write_str("target-runtime-reconnect-window-unavailable") + } + } + } +} + +impl std::error::Error for AdmissionError {} + +#[derive(Debug)] +struct AdmissionCounters { + sessions: AtomicUsize, + controllers: AtomicUsize, + watches: AtomicUsize, + streams: AtomicUsize, + reconnects: AtomicUsize, + reconnect_history: Mutex>, +} + +/// Shared admission state for one mode-bound runtime. +#[derive(Debug, Clone)] +pub struct AdmissionBudget { + limits: AdmissionLimits, + counters: Arc, +} + +impl AdmissionBudget { + pub fn new(limits: AdmissionLimits) -> Result { + Ok(Self { + limits: limits.validate()?, + counters: Arc::new(AdmissionCounters { + sessions: AtomicUsize::new(0), + controllers: AtomicUsize::new(0), + watches: AtomicUsize::new(0), + streams: AtomicUsize::new(0), + reconnects: AtomicUsize::new(0), + reconnect_history: Mutex::new(VecDeque::new()), + }), + }) + } + + pub const fn limits(&self) -> AdmissionLimits { + self.limits + } + + /// Reserve a non-reconnect class without allocating class state first. + pub fn try_admit(&self, kind: AdmissionKind) -> Result { + let (counter, limit) = match kind { + AdmissionKind::Session => (&self.counters.sessions, self.limits.max_sessions), + AdmissionKind::Controller => (&self.counters.controllers, self.limits.max_controllers), + AdmissionKind::Watch => (&self.counters.watches, self.limits.max_watches), + AdmissionKind::Stream => (&self.counters.streams, self.limits.max_streams), + AdmissionKind::Reconnect => return self.try_admit_reconnect(Instant::now()), + }; + reserve(counter, limit, kind).map(|_| permit(kind, Arc::clone(&self.counters))) + } + + /// Reserve one reconnect attempt inside the fixed sliding window. + pub fn try_admit_reconnect(&self, now: Instant) -> Result { + let mut history = self + .counters + .reconnect_history + .lock() + .map_err(|_| AdmissionError::ReconnectWindowUnavailable)?; + while history.front().is_some_and(|started| { + now.saturating_duration_since(*started) >= self.limits.reconnect_window + }) { + history.pop_front(); + } + if history.len() >= self.limits.max_reconnects_per_window { + return Err(AdmissionError::LimitExceeded(AdmissionKind::Reconnect)); + } + reserve( + &self.counters.reconnects, + self.limits.max_reconnects_per_window, + AdmissionKind::Reconnect, + )?; + history.push_back(now); + Ok(permit(AdmissionKind::Reconnect, Arc::clone(&self.counters))) + } + + pub fn active(&self, kind: AdmissionKind) -> usize { + match kind { + AdmissionKind::Session => self.counters.sessions.load(Ordering::Acquire), + AdmissionKind::Reconnect => self.counters.reconnects.load(Ordering::Acquire), + AdmissionKind::Controller => self.counters.controllers.load(Ordering::Acquire), + AdmissionKind::Watch => self.counters.watches.load(Ordering::Acquire), + AdmissionKind::Stream => self.counters.streams.load(Ordering::Acquire), + } + } +} + +fn reserve(counter: &AtomicUsize, limit: usize, kind: AdmissionKind) -> Result<(), AdmissionError> { + let mut current = counter.load(Ordering::Acquire); + loop { + if current >= limit { + return Err(AdmissionError::LimitExceeded(kind)); + } + match counter.compare_exchange_weak( + current, + current + 1, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => return Ok(()), + Err(observed) => current = observed, + } + } +} + +/// RAII reservation released when the admitted operation is closed. +#[derive(Debug)] +struct PermitState { + kind: AdmissionKind, + counters: Arc, + released: AtomicBool, +} + +#[derive(Debug, Clone)] +pub struct AdmissionPermit { + inner: Arc, +} + +impl AdmissionPermit { + pub fn kind(&self) -> AdmissionKind { + self.inner.kind + } + + pub fn release(&self) { + if !self.inner.released.swap(true, Ordering::AcqRel) { + let counter = match self.inner.kind { + AdmissionKind::Session => &self.inner.counters.sessions, + AdmissionKind::Reconnect => &self.inner.counters.reconnects, + AdmissionKind::Controller => &self.inner.counters.controllers, + AdmissionKind::Watch => &self.inner.counters.watches, + AdmissionKind::Stream => &self.inner.counters.streams, + }; + counter.fetch_sub(1, Ordering::AcqRel); + } + } +} + +impl Drop for AdmissionPermit { + fn drop(&mut self) { + self.release(); + } +} + +fn permit(kind: AdmissionKind, counters: Arc) -> AdmissionPermit { + AdmissionPermit { + inner: Arc::new(PermitState { + kind, + counters, + released: AtomicBool::new(false), + }), + } +} + +/// The immutable identity of one controller assignment. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ControllerAssignmentKey { + pub zone: ZoneId, + pub provider: ResourceRef, + pub target: ResourceRef, + pub provider_generation: u64, + pub controller_generation: u64, + pub session_generation: u64, + pub assignment_epoch: u64, +} + +impl ControllerAssignmentKey { + pub fn validate(&self, mode: DaemonMode) -> Result<(), DeploymentError> { + if self.provider_generation == 0 + || self.controller_generation == 0 + || self.session_generation == 0 + || self.assignment_epoch == 0 + { + return Err(DeploymentError::GenerationZero); + } + let expected = mode.target_kind(); + let actual = match self.target.resource_type().as_str() { + "Host" => TargetKind::Host, + "Guest" => TargetKind::Guest, + _ => return Err(DeploymentError::TargetWrongKind), + }; + if actual != expected { + return Err(DeploymentError::TargetWrongKind); + } + if self.provider.resource_type().as_str() != "Provider" { + return Err(DeploymentError::ProviderWrongKind); + } + Ok(()) + } +} + +#[derive(Debug)] +struct AssignmentState { + active: AtomicBool, + permit: Mutex>, +} + +impl AssignmentState { + fn new() -> Self { + Self { + active: AtomicBool::new(true), + permit: Mutex::new(None), + } + } + + fn install_permit(&self, permit: AdmissionPermit) -> Result<(), DeploymentError> { + let mut slot = self + .permit + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + *slot = Some(permit); + Ok(()) + } + + fn revoke(&self) { + self.active.store(false, Ordering::Release); + if let Ok(mut slot) = self.permit.lock() { + if let Some(permit) = slot.take() { + permit.release(); + } + } + } +} + +/// A revocable controller assignment lease. +#[derive(Debug)] +pub struct AssignmentLease { + key: ControllerAssignmentKey, + state: Arc, + registry: Arc>>>, + _controller_permit: AdmissionPermit, +} + +impl AssignmentLease { + pub fn key(&self) -> &ControllerAssignmentKey { + &self.key + } + + pub fn is_active(&self) -> bool { + self.state.active.load(Ordering::Acquire) + } + + pub fn revoke(&self) { + self.state.revoke(); + unregister_assignment(&self.registry, &self.key, &self.state); + } +} + +impl Drop for AssignmentLease { + fn drop(&mut self) { + self.state.revoke(); + unregister_assignment(&self.registry, &self.key, &self.state); + } +} + +fn unregister_assignment( + registry: &Mutex>>, + key: &ControllerAssignmentKey, + state: &Arc, +) { + if let Ok(mut assignments) = registry.lock() + && assignments + .get(key) + .is_some_and(|current| Arc::ptr_eq(current, state)) + { + assignments.remove(key); + } +} + +/// Shared ProviderDeployment behavior used by Host and Guest compositions. +#[derive(Debug, Clone)] +pub struct ProviderDeployment { + mode: DaemonMode, + admission: AdmissionBudget, + assignments: Arc>>>, +} + +impl ProviderDeployment { + pub fn new(mode: DaemonMode, limits: AdmissionLimits) -> Result { + Ok(Self { + mode, + admission: AdmissionBudget::new(limits)?, + assignments: Arc::new(Mutex::new(BTreeMap::new())), + }) + } + + pub const fn mode(&self) -> DaemonMode { + self.mode + } + + pub const fn target_kind(&self) -> TargetKind { + self.mode.target_kind() + } + + pub fn admission(&self) -> &AdmissionBudget { + &self.admission + } + + /// Admit exactly one target-scoped controller assignment. + /// + /// The controller reservation is acquired before inserting any assignment + /// record, so a flood cannot allocate unbounded per-controller state. + pub fn admit_assignment( + &self, + key: ControllerAssignmentKey, + ) -> Result { + key.validate(self.mode)?; + let permit = self + .admission + .try_admit(AdmissionKind::Controller) + .map_err(DeploymentError::Admission)?; + let mut assignments = self + .assignments + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if assignments.contains_key(&key) { + return Err(DeploymentError::AssignmentAlreadyActive); + } + let state = Arc::new(AssignmentState::new()); + state.install_permit(permit.clone())?; + assignments.insert(key.clone(), Arc::clone(&state)); + Ok(AssignmentLease { + key, + state, + registry: Arc::clone(&self.assignments), + _controller_permit: permit, + }) + } + + /// Revoke every assignment bound to one ComponentSession generation. + pub fn revoke_session(&self, session_generation: u64) -> Result { + if session_generation == 0 { + return Err(DeploymentError::GenerationZero); + } + let mut assignments = self + .assignments + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + let keys = assignments + .keys() + .filter(|key| key.session_generation == session_generation) + .cloned() + .collect::>(); + for key in &keys { + if let Some(state) = assignments.remove(key) { + state.revoke(); + } + } + Ok(keys.len()) + } + + /// Revoke every assignment before replacing the target's generation. + pub fn revoke_target(&self, target: &ResourceRef) -> Result { + let mut assignments = self + .assignments + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + let keys = assignments + .keys() + .filter(|key| &key.target == target) + .cloned() + .collect::>(); + for key in &keys { + if let Some(state) = assignments.remove(key) { + state.revoke(); + } + } + Ok(keys.len()) + } + + pub fn active_assignments(&self) -> Result { + let assignments = self + .assignments + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + Ok(assignments + .values() + .filter(|state| state.active.load(Ordering::Acquire)) + .count()) + } +} + +/// ProviderDeployment refusal. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DeploymentError { + Admission(AdmissionError), + GenerationZero, + TargetWrongKind, + ProviderWrongKind, + AssignmentAlreadyActive, + StateUnavailable, +} + +impl std::fmt::Display for DeploymentError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Admission(error) => return error.fmt(formatter), + Self::GenerationZero => "provider-deployment-generation-zero", + Self::TargetWrongKind => "provider-deployment-target-wrong-kind", + Self::ProviderWrongKind => "provider-deployment-provider-wrong-kind", + Self::AssignmentAlreadyActive => "provider-deployment-assignment-active", + Self::StateUnavailable => "provider-deployment-state-unavailable", + }) + } +} + +impl std::error::Error for DeploymentError {} + +#[cfg(test)] +mod tests { + use super::*; + use d2b_contracts_resource::v3::{ResourceName, ResourceTypeName}; + + fn resource(kind: &str, name: &str) -> ResourceRef { + ResourceRef::new( + ResourceTypeName::parse(kind).expect("resource type"), + ResourceName::parse(name).expect("resource name"), + ) + } + + fn assignment(session_generation: u64) -> ControllerAssignmentKey { + ControllerAssignmentKey { + zone: ZoneId::parse("work").expect("zone"), + provider: resource("Provider", "system-systemd"), + target: resource("Guest", "workload"), + provider_generation: 1, + controller_generation: 1, + session_generation, + assignment_epoch: 1, + } + } + + #[test] + fn mode_is_closed_and_surfaces_are_not_widenable() { + assert_eq!(DaemonMode::parse("host"), Ok(DaemonMode::Host)); + assert_eq!(DaemonMode::parse("guest"), Ok(DaemonMode::Guest)); + assert!(DaemonMode::parse("both").is_err()); + assert!(!DaemonMode::Guest.surfaces().local_zone_store); + assert!(!DaemonMode::Guest.surfaces().public_operator_socket); + assert!(!DaemonMode::Guest.surfaces().realm_credentials); + assert!(DaemonMode::Guest.surfaces().parent_component_session); + } + + #[test] + fn admission_refuses_before_controller_state_is_allocated() { + let budget = AdmissionBudget::new(AdmissionLimits { + max_sessions: 1, + max_reconnects_per_window: 1, + max_controllers: 1, + max_watches: 1, + max_streams: 1, + reconnect_window: Duration::from_secs(60), + }) + .expect("valid limits"); + let first = budget + .try_admit(AdmissionKind::Controller) + .expect("first controller"); + assert!(matches!( + budget.try_admit(AdmissionKind::Controller), + Err(AdmissionError::LimitExceeded(AdmissionKind::Controller)) + )); + drop(first); + assert!(budget.try_admit(AdmissionKind::Controller).is_ok()); + } + + #[test] + fn disconnect_revokes_all_assignments_for_the_session_generation() { + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()) + .expect("deployment"); + let lease = deployment + .admit_assignment(assignment(7)) + .expect("assignment"); + assert!(lease.is_active()); + assert_eq!(deployment.revoke_session(7).expect("revoke"), 1); + assert!(!lease.is_active()); + assert_eq!(deployment.active_assignments().expect("count"), 0); + } + + #[test] + fn assignment_drop_releases_controller_slot_and_key() { + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()) + .expect("deployment"); + let lease = deployment + .admit_assignment(assignment(7)) + .expect("assignment"); + drop(lease); + assert_eq!(deployment.active_assignments().expect("count"), 0); + assert!(deployment.admit_assignment(assignment(7)).is_ok()); + } + + #[test] + fn guest_cannot_admit_a_host_target_or_non_provider_controller() { + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()) + .expect("deployment"); + let mut host = assignment(1); + host.target = resource("Host", "host-system"); + assert!(matches!( + deployment.admit_assignment(host), + Err(DeploymentError::TargetWrongKind) + )); + + let mut wrong_provider = assignment(1); + wrong_provider.provider = resource("Process", "controller"); + assert!(matches!( + deployment.admit_assignment(wrong_provider), + Err(DeploymentError::ProviderWrongKind) + )); + } +} diff --git a/packages/d2bd-runtime/tests/runtime_boundary.rs b/packages/d2bd-runtime/tests/runtime_boundary.rs index cfe619a0b0..1b877c3eb4 100644 --- a/packages/d2bd-runtime/tests/runtime_boundary.rs +++ b/packages/d2bd-runtime/tests/runtime_boundary.rs @@ -70,6 +70,8 @@ const RUNTIME_FILES: &[(&str, &str)] = &[ "src/guest_control_vsock.rs", include_str!("../src/guest_control_vsock.rs"), ), + ("src/guest_mode.rs", include_str!("../src/guest_mode.rs")), + ("src/host_mode.rs", include_str!("../src/host_mode.rs")), ("src/json_io.rs", include_str!("../src/json_io.rs")), ( "src/kernel_module_check.rs", @@ -163,6 +165,7 @@ const RUNTIME_FILES: &[(&str, &str)] = &[ "src/terminal_session.rs", include_str!("../src/terminal_session.rs"), ), + ("src/target_runtime.rs", include_str!("../src/target_runtime.rs")), ("src/typed_error.rs", include_str!("../src/typed_error.rs")), ( "src/typed_shell_targets.rs", @@ -224,6 +227,7 @@ fn runtime_uses_narrow_direct_contracts() { "d2b-contracts", "d2b-contracts-broker", "d2b-contracts-control", + "d2b-contracts-provider", "d2b-contracts-resource", "d2b-contracts-zone-session", ] { diff --git a/packages/d2bd/BUILD.bazel b/packages/d2bd/BUILD.bazel index 58678832f0..47ce383bc5 100644 --- a/packages/d2bd/BUILD.bazel +++ b/packages/d2bd/BUILD.bazel @@ -460,6 +460,47 @@ rust_test( ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) +rust_test( + name = "guest_mode_fail_closed", + srcs = ["tests/guest_mode_fail_closed.rs"], + compile_data = ["Cargo.toml"], + data = [":d2bd"], + env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, + rustc_env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, + tags = ["no-remote-cache"], + visibility = ["//visibility:public"], + deps = all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_test( + name = "guest_mode_component_session", + srcs = ["tests/guest_mode_component_session.rs"], + compile_data = ["Cargo.toml"], + visibility = ["//visibility:public"], + deps = [ + "//packages/d2b-contracts-resource:d2b_contracts_resource", + "//packages/d2b-session:d2b_session_test_support", + "//packages/d2b-session-unix:d2b_session_unix_test_support", + "//packages/d2bd-runtime:d2bd_runtime_test_support", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_test( + name = "mode_separation", + srcs = ["tests/mode_separation.rs"], + compile_data = ["Cargo.toml"], + data = [":d2bd"], + env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, + rustc_env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, + tags = ["no-remote-cache"], + visibility = ["//visibility:public"], + deps = [ + ":d2bd_lib_test_support", + "//packages/d2b-contracts-resource:d2b_contracts_resource", + "//packages/d2bd-runtime:d2bd_runtime_test_support", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + # keep filegroup( name = "cargo_workspace_sources", diff --git a/packages/d2bd/Cargo.toml b/packages/d2bd/Cargo.toml index b3beb2eaea..e2fda6a08b 100644 --- a/packages/d2bd/Cargo.toml +++ b/packages/d2bd/Cargo.toml @@ -33,7 +33,7 @@ d2b-process = { path = "../d2b-process", version = "0.0.0-bootstrap" } d2b-process-conformance = { path = "../d2b-process-conformance", version = "0.0.0-bootstrap" } d2b-bus = { path = "../d2b-bus", version = "0.0.0-bootstrap" } d2b-session = { path = "../d2b-session", version = "0.0.0-bootstrap" } -d2b-session-unix = { path = "../d2b-session-unix", version = "0.0.0-bootstrap", features = ["host-socket"] } +d2b-session-unix = { path = "../d2b-session-unix", version = "0.0.0-bootstrap", features = ["host-socket", "native-vsock"] } d2b-resource-api = { path = "../d2b-resource-api", version = "0.0.0-bootstrap" } d2b-resource-store = { path = "../d2b-resource-store", version = "0.0.0-bootstrap" } d2b-resource-store-redb = { path = "../d2b-resource-store-redb", version = "0.0.0-bootstrap" } diff --git a/packages/d2bd/src/composition.rs b/packages/d2bd/src/composition.rs index 25b2bf9e42..253c782704 100644 --- a/packages/d2bd/src/composition.rs +++ b/packages/d2bd/src/composition.rs @@ -79,12 +79,14 @@ use d2b_contracts_resource::v3::{ ResourceGeneration, ResourceRef, ResourceUid, + SchemaFingerprint, ZoneId, guest::GuestSpec, network::NetworkSpec, process::ProcessSpec, volume::{VolumeAttachment, VolumeSpec}, }; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; use d2b_core::bundle::Bundle; use d2b_core::bundle_resolver::{ BundleResolver, intent_id_activation, intent_id_bridge_env, intent_id_gc_host, @@ -286,6 +288,71 @@ const EMPTY_VMM_CLEANUP_GRACE: Duration = Duration::from_secs(5); const CGROUP_EMPTY_POST_KILL_WAIT: Duration = Duration::from_secs(5); const GATEWAY_DISPLAY_SESSION_TTL: Duration = Duration::from_secs(3600); +/// The static composition root is one artifact for both process-start modes. +pub const D2BD_ARTIFACT_FAMILY: &str = "d2bd"; + +/// Stable build identity shared by Host and Guest compositions. The actual +/// executable digest remains the package artifact digest; this value is the +/// mode-independent family identity used in runtime evidence and tests. +pub fn shared_artifact_digest() -> String { + let mut digest = Sha256::new(); + digest.update(D2BD_ARTIFACT_FAMILY.as_bytes()); + digest.update(env!("CARGO_PKG_VERSION").as_bytes()); + digest.update(b"\0d2b-v3\0"); + format!("sha256:{:x}", digest.finalize()) +} + +/// Shared ProviderDeployment and fixed EffectPort composition for one daemon +/// instance. Provider selection remains static in this crate; lifecycle and +/// admission behavior live in `d2bd-runtime`. +#[derive(Debug, Clone)] +pub struct StaticProviderComposition { + mode: d2bd_runtime::target_runtime::DaemonMode, + deployment: d2bd_runtime::target_runtime::ProviderDeployment, + effects: provider_effects::FixedEffectAdapter, +} + +impl StaticProviderComposition { + pub fn new( + mode: d2bd_runtime::target_runtime::DaemonMode, + broker_socket: PathBuf, + daemon_uid: u32, + limits: d2bd_runtime::target_runtime::AdmissionLimits, + ) -> Result { + let deployment = + d2bd_runtime::target_runtime::ProviderDeployment::new(mode, limits)?; + let effects = match mode { + d2bd_runtime::target_runtime::DaemonMode::Host => { + provider_effects::FixedEffectAdapter::host(broker_socket, daemon_uid) + } + d2bd_runtime::target_runtime::DaemonMode::Guest => { + provider_effects::FixedEffectAdapter::guest(broker_socket, daemon_uid) + } + }; + Ok(Self { + mode, + deployment, + effects, + }) + } + + pub const fn mode(&self) -> d2bd_runtime::target_runtime::DaemonMode { + self.mode + } + + pub fn deployment(&self) -> &d2bd_runtime::target_runtime::ProviderDeployment { + &self.deployment + } + + pub fn effects(&self) -> &provider_effects::FixedEffectAdapter { + &self.effects + } + + pub fn artifact_digest(&self) -> String { + shared_artifact_digest() + } +} + /// Write deadline for the typed refusal frame (authz reject / busy) the /// accept loop sends before closing - never block the accept loop on a /// slow/abusive peer. @@ -1383,6 +1450,202 @@ pub async fn serve(options: ServeOptions) -> Result<(), TypedError> { Ok(()) } +/// Process-start Guest mode options. These options contain only the Guest +/// target identity and its own broker/state roots; Host bundle, public socket, +/// and realm paths are intentionally absent. +#[derive(Debug, Clone)] +pub struct GuestServeOptions { + pub guest_ref: String, + pub guest_uid: String, + pub zone: String, + pub purpose: String, + pub schema_fingerprint: String, + pub reconnect_generation: u64, + pub provider_generation: u64, + pub controller_generation: u64, + pub assignment_epoch: u64, + pub broker_socket_path: PathBuf, + pub broker_uid: u32, + pub state_dir: PathBuf, + pub boot_id_path: PathBuf, + pub local_private_key_path: Option, + pub parent_public_key_path: Option, + pub validate_only: bool, + pub once: bool, +} + +/// Start the fixed Guest target agent. Unlike [`serve`], this path never +/// loads `DaemonConfig`, binds a public operator socket, opens a Zone store, +/// or loads realm credentials. +pub async fn serve_guest(options: GuestServeOptions) -> Result<(), TypedError> { + if !options.state_dir.is_absolute() || !options.broker_socket_path.is_absolute() { + return Err(TypedError::InternalConfig { + detail: "guest mode requires absolute state and broker paths".to_owned(), + }); + } + // The production path is sealed to the kernel's boot-id file. A custom + // path is accepted only by the validation-only characterization route, + // which never establishes a session or allocates Guest authority. + let boot_id_path = if options.validate_only { + options.boot_id_path + } else { + PathBuf::from(d2bd_runtime::guest_mode::KERNEL_BOOT_ID_PATH) + }; + let boot = d2bd_runtime::guest_mode::BootIdentity::read(&boot_id_path) + .map_err(|_| TypedError::InternalConfig { + detail: "guest kernel boot identity unavailable".to_owned(), + })?; + let guest_ref = ResourceRef::parse(&options.guest_ref).map_err(|_| { + TypedError::InternalConfig { + detail: "guest identity reference is invalid".to_owned(), + } + })?; + let guest_uid = ResourceUid::parse(options.guest_uid).map_err(|_| { + TypedError::InternalConfig { + detail: "guest identity UID is invalid".to_owned(), + } + })?; + let zone = ZoneId::parse(options.zone).map_err(|_| TypedError::InternalConfig { + detail: "guest Zone identity is invalid".to_owned(), + })?; + let purpose = d2b_contracts_resource::v3::identity::SessionPurpose::parse(options.purpose) + .map_err(|_| TypedError::InternalConfig { + detail: "guest session purpose is invalid".to_owned(), + })?; + let schema = SchemaFingerprint::parse(options.schema_fingerprint).map_err(|_| { + TypedError::InternalConfig { + detail: "guest session schema fingerprint is invalid".to_owned(), + } + })?; + let reconnect_generation = + ReconnectGeneration::new(options.reconnect_generation).map_err(|_| { + TypedError::InternalConfig { + detail: "guest reconnect generation is invalid".to_owned(), + } + })?; + let identity = d2bd_runtime::guest_mode::GuestIdentity::new( + guest_ref, + guest_uid, + zone, + boot, + purpose, + schema, + reconnect_generation, + options.provider_generation, + options.controller_generation, + options.assignment_epoch, + ) + .map_err(|error| TypedError::InternalConfig { + detail: error.to_string(), + })?; + let runtime = d2bd_runtime::guest_mode::GuestRuntime::new( + identity, + options.broker_socket_path, + options.broker_uid, + d2bd_runtime::target_runtime::AdmissionLimits::guest_default(), + ) + .map_err(|error| TypedError::InternalConfig { + detail: error.to_string(), + })?; + if options.validate_only { + return Ok(()); + } + fs::create_dir_all(&options.state_dir).map_err(|_| TypedError::InternalConfig { + detail: "guest state root unavailable".to_owned(), + })?; + let local_private_path = options + .local_private_key_path + .ok_or_else(|| TypedError::InternalConfig { + detail: "guest ComponentSession private key is unavailable".to_owned(), + })?; + let parent_public_path = options + .parent_public_key_path + .ok_or_else(|| TypedError::InternalConfig { + detail: "parent Zone ComponentSession key is unavailable".to_owned(), + })?; + let parent_public = read_public_key32(&parent_public_path)?; + let mut listener = runtime.bind_listener().map_err(|error| TypedError::InternalConfig { + detail: error.to_string(), + })?; + let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .map_err(|_| TypedError::InternalIo { + context: "install Guest SIGTERM handler".to_owned(), + detail: "signal handler unavailable".to_owned(), + })?; + let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) + .map_err(|_| TypedError::InternalIo { + context: "install Guest SIGINT handler".to_owned(), + detail: "signal handler unavailable".to_owned(), + })?; + loop { + let local_private = read_secret32(&local_private_path)?; + let accepted = tokio::select! { + _ = sigterm.recv() => break, + _ = sigint.recv() => break, + result = runtime.accept_from_parent(&mut listener, local_private, parent_public) => result, + }; + let (mut session, lease) = match accepted { + Ok(session) => session, + Err(error) => { + if options.once { + return Err(TypedError::InternalConfig { + detail: error.to_string(), + }); + } + tracing::warn!(error = %error, "Guest ComponentSession handshake refused"); + continue; + } + }; + if options.once { + drop(lease); + return Ok(()); + } + let shutdown = loop { + tokio::select! { + _ = sigterm.recv() => break true, + _ = sigint.recv() => break true, + result = session.receive_ttrpc() => { + if result.is_err() { + break false; + } + } + } + }; + drop(lease); + if shutdown { + break; + } + } + Ok(()) +} + +fn read_secret32(path: &Path) -> Result { + let bytes = fs::read(path).map_err(|_| TypedError::InternalConfig { + detail: "guest ComponentSession private key unavailable".to_owned(), + })?; + let bytes: [u8; 32] = bytes.try_into().map_err(|_| TypedError::InternalConfig { + detail: "guest ComponentSession private key has invalid length".to_owned(), + })?; + d2b_session::Secret32::new(bytes).map_err(|_| TypedError::InternalConfig { + detail: "guest ComponentSession private key is invalid".to_owned(), + }) +} + +fn read_public_key32(path: &Path) -> Result<[u8; 32], TypedError> { + let bytes = fs::read(path).map_err(|_| TypedError::InternalConfig { + detail: "parent Zone ComponentSession key unavailable".to_owned(), + })?; + let bytes: [u8; 32] = bytes.try_into().map_err(|_| TypedError::InternalConfig { + detail: "parent Zone ComponentSession key has invalid length".to_owned(), + })?; + if bytes == [0; 32] { + return Err(TypedError::InternalConfig { + detail: "parent Zone ComponentSession key is invalid".to_owned(), + }); + } + Ok(bytes) +} + async fn finalize_daemon_interactions(state: &ServerState) -> Result<(), TypedError> { if let Some(listeners) = state .interaction_listeners diff --git a/packages/d2bd/src/main.rs b/packages/d2bd/src/main.rs index 41128a799a..bafeac120a 100644 --- a/packages/d2bd/src/main.rs +++ b/packages/d2bd/src/main.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; use d2bd::{ - DEFAULT_CONFIG_PATH, LockOnlyOptions, ServeOptions, TestClientOptions, banner, banner_note, - lock_only, run_test_client, serve, + DEFAULT_CONFIG_PATH, GuestServeOptions, LockOnlyOptions, ServeOptions, TestClientOptions, + banner, banner_note, lock_only, run_test_client, serve, serve_guest, }; #[derive(Debug, Parser)] @@ -15,6 +15,10 @@ struct Cli { #[derive(Debug, Subcommand)] enum Command { + /// Run the fixed Host authority mode. + Host(HostArgs), + /// Run the fixed Guest target-agent mode. + Guest(GuestArgs), Serve { #[arg(long, default_value = DEFAULT_CONFIG_PATH)] config: PathBuf, @@ -57,6 +61,70 @@ enum Command { }, } +#[derive(Debug, Clone, Args)] +struct HostArgs { + #[arg(long, default_value = DEFAULT_CONFIG_PATH)] + config: PathBuf, + #[arg(long)] + public_socket: Option, + #[arg(long)] + broker_socket: Option, + #[arg(long)] + state_lock: Option, + #[arg(long)] + locks_dir: Option, + #[arg(long)] + once: bool, + #[arg(long, hide = true)] + test_listen_on: Option, + #[arg(long, hide = true)] + allow_unprivileged_runtime_dir: bool, + #[arg(long)] + no_drop_privileges: bool, + #[arg(long, hide = true)] + daemon_state_dir: Option, + #[arg(long, hide = true)] + test_state_restore_report: Option, +} + +#[derive(Debug, Clone, Args)] +struct GuestArgs { + #[arg(long)] + guest_ref: String, + #[arg(long)] + guest_uid: String, + #[arg(long)] + zone: String, + #[arg(long, default_value = "zone-link")] + purpose: String, + #[arg(long)] + schema_fingerprint: String, + #[arg(long, default_value_t = 1)] + reconnect_generation: u64, + #[arg(long, default_value_t = 1)] + provider_generation: u64, + #[arg(long, default_value_t = 1)] + controller_generation: u64, + #[arg(long, default_value_t = 1)] + assignment_epoch: u64, + #[arg(long, default_value = "/run/d2b/guest-broker.sock")] + broker_socket: PathBuf, + #[arg(long, default_value_t = 997)] + broker_uid: u32, + #[arg(long, default_value = "/var/lib/d2b/guest-state")] + state_dir: PathBuf, + #[arg(long, hide = true, default_value = "/proc/sys/kernel/random/boot_id")] + boot_id_path: PathBuf, + #[arg(long, hide = true)] + local_private_key: Option, + #[arg(long, hide = true)] + parent_public_key: Option, + #[arg(long, hide = true)] + validate_only: bool, + #[arg(long, hide = true)] + once: bool, +} + #[tokio::main(flavor = "multi_thread")] async fn main() { // v1.1.1 live-deploy fu9: route tracing to stderr so @@ -75,6 +143,43 @@ async fn main() { println!("{}", banner_note()); Ok(()) } + Some(Command::Host(args)) => { + serve(ServeOptions { + config_path: args.config, + public_socket_path: args.test_listen_on.or(args.public_socket), + broker_socket_path: args.broker_socket, + state_lock_path: args.state_lock, + locks_dir: args.locks_dir, + once: args.once, + allow_unprivileged_runtime_dir: args.allow_unprivileged_runtime_dir, + drop_privileges: !args.no_drop_privileges, + daemon_state_dir: args.daemon_state_dir, + test_state_restore_report_path: args.test_state_restore_report, + }) + .await + } + Some(Command::Guest(args)) => { + serve_guest(GuestServeOptions { + guest_ref: args.guest_ref, + guest_uid: args.guest_uid, + zone: args.zone, + purpose: args.purpose, + schema_fingerprint: args.schema_fingerprint, + reconnect_generation: args.reconnect_generation, + provider_generation: args.provider_generation, + controller_generation: args.controller_generation, + assignment_epoch: args.assignment_epoch, + broker_socket_path: args.broker_socket, + broker_uid: args.broker_uid, + state_dir: args.state_dir, + boot_id_path: args.boot_id_path, + local_private_key_path: args.local_private_key, + parent_public_key_path: args.parent_public_key, + validate_only: args.validate_only, + once: args.once, + }) + .await + } Some(Command::Serve { config, public_socket, diff --git a/packages/d2bd/src/provider_effects.rs b/packages/d2bd/src/provider_effects.rs index 4b0cce6744..a5b9b3a942 100644 --- a/packages/d2bd/src/provider_effects.rs +++ b/packages/d2bd/src/provider_effects.rs @@ -19,10 +19,114 @@ use std::{ }; use d2b_contracts_broker::broker_wire::BrokerCallerRole; +use d2b_contracts_broker::broker_wire::{BrokerRequest, BrokerResponse}; use d2b_contracts_resource::v3::{ ResourceRef, ZoneId, }; +use d2bd_runtime::{ + broker_transport::{ModeBoundBrokerAdapter, ModeBoundBrokerError}, + target_runtime::DaemonMode, +}; + +/// Provider-neutral EffectPort classes wired by the static d2bd composition +/// root. These are capability classes, not request names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ProviderEffectClass { + Runtime, + ProcessLaunch, + ProcessObserve, + ProcessSignal, +} + +impl ProviderEffectClass { + pub const fn as_str(self) -> &'static str { + match self { + Self::Runtime => "runtime", + Self::ProcessLaunch => "process-launch", + Self::ProcessObserve => "process-observe", + Self::ProcessSignal => "process-signal", + } + } +} + +/// Static, mode-bound Provider effect adapter. +#[derive(Debug, Clone)] +pub struct FixedEffectAdapter { + mode: DaemonMode, + broker: ModeBoundBrokerAdapter, +} + +impl FixedEffectAdapter { + pub fn host(socket_path: PathBuf, daemon_uid: u32) -> Self { + Self { + mode: DaemonMode::Host, + broker: ModeBoundBrokerAdapter::host(socket_path, daemon_uid), + } + } + + pub fn guest(socket_path: PathBuf, daemon_uid: u32) -> Self { + Self { + mode: DaemonMode::Guest, + broker: ModeBoundBrokerAdapter::guest(socket_path, daemon_uid), + } + } + + pub const fn mode(&self) -> DaemonMode { + self.mode + } + + pub const fn broker_profile(&self) -> d2b_contracts_broker::broker_wire::BrokerProfile { + self.broker.profile() + } + + pub fn allows(&self, class: ProviderEffectClass) -> bool { + match self.mode { + DaemonMode::Host => true, + DaemonMode::Guest => matches!( + class, + ProviderEffectClass::Runtime + | ProviderEffectClass::ProcessLaunch + | ProviderEffectClass::ProcessObserve + | ProviderEffectClass::ProcessSignal + ), + } + } + + pub fn dispatch( + &self, + class: ProviderEffectClass, + request: BrokerRequest, + timeout: Option, + ) -> Result { + if !self.allows(class) { + return Err(FixedEffectError::EffectClassDenied(class)); + } + self.broker + .dispatch(request, timeout) + .map_err(FixedEffectError::Broker) + } +} + +/// Refusal at the fixed EffectPort boundary. +#[derive(Debug)] +pub enum FixedEffectError { + EffectClassDenied(ProviderEffectClass), + Broker(ModeBoundBrokerError), +} + +impl std::fmt::Display for FixedEffectError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EffectClassDenied(class) => { + write!(formatter, "provider-effect-class-denied:{}", class.as_str()) + } + Self::Broker(error) => error.fmt(formatter), + } + } +} + +impl std::error::Error for FixedEffectError {} /// Maximum retained lifecycle mutation keys. pub const MAX_TRACKED_LIFECYCLE_MUTATIONS: usize = 256; diff --git a/packages/d2bd/tests/guest_mode_component_session.rs b/packages/d2bd/tests/guest_mode_component_session.rs new file mode 100644 index 0000000000..3a3a822a60 --- /dev/null +++ b/packages/d2bd/tests/guest_mode_component_session.rs @@ -0,0 +1,162 @@ +use d2b_contracts_resource::v3::{ + ResourceName, ResourceRef, ResourceTypeName, ResourceUid, SchemaFingerprint, ZoneId, + identity::{ReconnectGeneration, SessionPurpose}, +}; +use d2b_session::{HandshakeCredentials, Secret32, SessionEngine, x25519_public_key}; +use d2b_session_unix::FramedVsockTransport; +use d2bd_runtime::{ + guest_mode::{ + BootIdentity, GUEST_COMPONENT_SESSION_PORT, GUEST_COMPONENT_SESSION_PURPOSE, GuestIdentity, + GuestRuntime, reject_legacy_guest_control_prelude, + }, + target_runtime::{AdmissionBudget, AdmissionKind, AdmissionLimits, ControllerAssignmentKey}, +}; +use std::time::Instant; + +fn identity(generation: u64) -> GuestIdentity { + GuestIdentity::new( + ResourceRef::parse("Guest/workload").expect("Guest ref"), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").expect("Guest UID"), + ZoneId::parse("work").expect("Zone"), + BootIdentity::from_kernel_boot_id("boot-id-u3").expect("boot ID"), + SessionPurpose::parse(GUEST_COMPONENT_SESSION_PURPOSE).expect("purpose"), + SchemaFingerprint::parse( + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + ) + .expect("schema"), + ReconnectGeneration::new(generation).expect("generation"), + 1, + 1, + 1, + ) + .expect("Guest identity") +} + +fn runtime() -> GuestRuntime { + GuestRuntime::new( + identity(1), + "/run/d2b/guest-broker.sock".into(), + 997, + AdmissionLimits::guest_default(), + ) + .expect("Guest runtime") +} + +#[test] +fn component_session_uses_fixed_vsock_port_and_identity_binding() { + let identity = identity(3); + let policy = identity.endpoint_policy(); + assert_eq!(GUEST_COMPONENT_SESSION_PORT, 14_318); + assert_eq!(policy.reconnect_generation, 3); + assert_ne!(policy.transport_binding.channel_binding, [0; 32]); + let reconnect = identity.endpoint_policy_for_generation(4); + assert_eq!(reconnect.reconnect_generation, 4); + assert_eq!( + policy.transport_binding.channel_binding, + reconnect.transport_binding.channel_binding + ); +} + +#[test] +fn reconnect_and_stream_admission_refuse_floods_before_state_allocation() { + let budget = AdmissionBudget::new(AdmissionLimits { + max_sessions: 1, + max_reconnects_per_window: 1, + max_controllers: 1, + max_watches: 1, + max_streams: 1, + reconnect_window: std::time::Duration::from_secs(60), + }) + .expect("valid budget"); + let session = budget.try_admit(AdmissionKind::Session).expect("session"); + assert!(budget.try_admit(AdmissionKind::Session).is_err()); + let stream = budget.try_admit(AdmissionKind::Stream).expect("stream"); + assert!(budget.try_admit(AdmissionKind::Stream).is_err()); + drop(session); + drop(stream); + assert!( + budget + .try_admit_reconnect(std::time::Instant::now()) + .is_ok() + ); + assert!( + budget + .try_admit_reconnect(std::time::Instant::now()) + .is_err() + ); +} + +#[test] +fn old_guest_control_prelude_is_rejected() { + assert!(reject_legacy_guest_control_prelude(b"CONNECT 14318\n").is_err()); + assert!(reject_legacy_guest_control_prelude(b"D2BGC-old").is_err()); +} + +#[test] +fn reconnect_revokes_stale_assignments_and_drops_the_new_lease() { + let runtime = runtime(); + let first = runtime + .admit_generation_for_tests(1) + .expect("first session"); + let key = |generation| ControllerAssignmentKey { + zone: ZoneId::parse("work").expect("Zone"), + provider: ResourceRef::new( + ResourceTypeName::parse("Provider").expect("Provider type"), + ResourceName::parse("system-systemd").expect("Provider name"), + ), + target: ResourceRef::new( + ResourceTypeName::parse("Guest").expect("Guest type"), + ResourceName::parse("workload").expect("Guest name"), + ), + provider_generation: 1, + controller_generation: 1, + session_generation: generation, + assignment_epoch: generation, + }; + let stale = runtime.admit_assignment(key(1)).expect("first assignment"); + let second = runtime + .admit_generation_for_tests(2) + .expect("new generation"); + assert!(!stale.is_active()); + let current = runtime.admit_assignment(key(2)).expect("new assignment"); + drop(second); + drop(current); + assert_eq!(runtime.deployment().active_assignments().unwrap(), 0); + drop(first); +} + +#[tokio::test] +async fn newer_generation_completes_the_real_component_session_path() { + let runtime = runtime(); + let parent_private_bytes = [2_u8; 32]; + let guest_private_bytes = [3_u8; 32]; + let parent_public = x25519_public_key(&parent_private_bytes).expect("parent public key"); + let guest_public = x25519_public_key(&guest_private_bytes).expect("Guest public key"); + let parent_private = Secret32::new(parent_private_bytes).expect("parent private key"); + let guest_private = Secret32::new(guest_private_bytes).expect("Guest private key"); + let policy = identity(2).endpoint_policy(); + let (left, right) = tokio::io::duplex(16 * 1024); + let parent = tokio::spawn(async move { + SessionEngine::establish_initiator( + FramedVsockTransport::new(left), + policy, + HandshakeCredentials::Kk { + local_private: parent_private, + remote_public: guest_public, + }, + Instant::now(), + ) + .await + }); + let (_, lease) = runtime + .establish_component_session( + FramedVsockTransport::new(right), + guest_private, + parent_public, + ) + .await + .expect("Guest accepts newer generation"); + let parent = parent.await.expect("parent task").expect("parent session"); + assert_eq!(lease.generation(), 2); + assert_eq!(parent.generation(), 2); +} diff --git a/packages/d2bd/tests/guest_mode_fail_closed.rs b/packages/d2bd/tests/guest_mode_fail_closed.rs new file mode 100644 index 0000000000..0df6fdf2fa --- /dev/null +++ b/packages/d2bd/tests/guest_mode_fail_closed.rs @@ -0,0 +1,68 @@ +use std::{fs, path::PathBuf, process::Command}; + +fn scratch(name: &str) -> PathBuf { + let root = std::env::current_dir() + .expect("current directory") + .join("target") + .join(format!("u3-guest-{name}-{}", std::process::id())); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).expect("create project scratch"); + root +} + +fn guest_args(root: &PathBuf) -> Vec { + vec![ + "guest".to_owned(), + "--guest-ref".to_owned(), + "Guest/workload".to_owned(), + "--guest-uid".to_owned(), + "123e4567-e89b-42d3-a456-426614174000".to_owned(), + "--zone".to_owned(), + "work".to_owned(), + "--schema-fingerprint".to_owned(), + "sha256:0000000000000000000000000000000000000000000000000000000000000001".to_owned(), + "--broker-socket".to_owned(), + root.join("guest-broker.sock").display().to_string(), + "--state-dir".to_owned(), + root.join("state").display().to_string(), + "--boot-id-path".to_owned(), + root.join("boot-id").display().to_string(), + "--validate-only".to_owned(), + ] +} + +#[test] +fn guest_validation_never_materializes_host_store_public_or_realm_surfaces() { + let root = scratch("surfaces"); + fs::write(root.join("boot-id"), "boot-id-u3\n").expect("write boot identity"); + let output = Command::new(env!("CARGO_BIN_EXE_d2bd")) + .args(guest_args(&root)) + .env("D2B_PUBLIC_SOCKET", root.join("forbidden-public.sock")) + .env("D2B_REALM_IDENTITY", root.join("forbidden-realm.json")) + .output() + .expect("run d2bd guest"); + assert!( + output.status.success(), + "guest validation failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(!root.join("forbidden-public.sock").exists()); + assert!(!root.join("forbidden-realm.json").exists()); + assert!(!root.join("state").exists()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn guest_mode_rejects_host_style_config_flags_at_process_start() { + let output = Command::new(env!("CARGO_BIN_EXE_d2bd")) + .args([ + "guest", + "--config", + "/etc/d2b/daemon-config.json", + "--guest-ref", + "Guest/workload", + ]) + .output() + .expect("run d2bd guest"); + assert_eq!(output.status.code(), Some(2)); +} diff --git a/packages/d2bd/tests/mode_separation.rs b/packages/d2bd/tests/mode_separation.rs new file mode 100644 index 0000000000..ff27d47784 --- /dev/null +++ b/packages/d2bd/tests/mode_separation.rs @@ -0,0 +1,73 @@ +use std::{fs, path::PathBuf, process::Command}; + +use d2bd::StaticProviderComposition; +use d2bd_runtime::{ + broker_transport::ModeBoundBrokerAdapter, + target_runtime::{AdmissionLimits, DaemonMode}, +}; + +#[test] +fn host_and_guest_modes_are_process_start_surfaces() { + let host = Command::new(env!("CARGO_BIN_EXE_d2bd")) + .args(["host", "--help"]) + .output() + .expect("host help"); + let guest = Command::new(env!("CARGO_BIN_EXE_d2bd")) + .args(["guest", "--help"]) + .output() + .expect("guest help"); + assert!(host.status.success()); + assert!(guest.status.success()); + assert_ne!(host.stdout, guest.stdout); +} + +#[test] +fn host_and_guest_compositions_share_the_artifact_family_but_not_effect_profile() { + let host = StaticProviderComposition::new( + DaemonMode::Host, + PathBuf::from("/run/d2b/host-broker.sock"), + 997, + AdmissionLimits::host_default(), + ) + .expect("Host composition"); + let guest = StaticProviderComposition::new( + DaemonMode::Guest, + PathBuf::from("/run/d2b/guest-broker.sock"), + 997, + AdmissionLimits::guest_default(), + ) + .expect("Guest composition"); + assert_eq!(host.artifact_digest(), guest.artifact_digest()); + assert_eq!(host.effects().broker_profile().as_str(), "host"); + assert_eq!(guest.effects().broker_profile().as_str(), "guest"); + assert!( + host.deployment().admission().limits().max_sessions + > guest.deployment().admission().limits().max_sessions + ); +} + +#[test] +fn guest_identity_validation_uses_kernel_boot_id_not_writable_guest_state() { + let root = std::env::current_dir() + .expect("current directory") + .join("target") + .join(format!("u3-boot-{}", std::process::id())); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).expect("scratch"); + fs::write(root.join("boot-id"), "kernel-boot-id").expect("boot id"); + let first = + d2bd_runtime::guest_mode::BootIdentity::read(root.join("boot-id")).expect("kernel boot id"); + fs::write(root.join("guest-state"), "forged-boot-id").expect("state"); + let second = + d2bd_runtime::guest_mode::BootIdentity::read(root.join("boot-id")).expect("kernel boot id"); + assert_eq!(first, second); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn guest_effect_adapter_rejects_the_host_broker_instance() { + let guest = ModeBoundBrokerAdapter::guest(PathBuf::from("/run/d2b/priv.sock"), 997); + assert!(guest.validate_instance().is_err()); + let guest = ModeBoundBrokerAdapter::guest(PathBuf::from("/run/d2b/guest-broker.sock"), 997); + assert!(guest.validate_instance().is_ok()); +} diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json index 2042d59e5e..f6d9cdadc0 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 8d687128e7..832eed4fbf 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json index 4869c1bf52..6346ff91c3 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 8d687128e7..832eed4fbf 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index f7ac7bd03c..b4bd13f7ed 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index 2aea9a01aa..855ed6eb62 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index adbb0a67d0..2fa5a7f620 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index 2aea9a01aa..855ed6eb62 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index bc0a0c372f..10fb75a888 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index 3e7eb0e443..03c8bcf216 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index e862820dd6..83469e79aa 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index 3e7eb0e443..03c8bcf216 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json index 422139b130..e2051b3fa1 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json index 8d687128e7..832eed4fbf 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json index 4601d37fac..69bfa89266 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json index 8d687128e7..832eed4fbf 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json index aca848dbab..96cf25d39c 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json @@ -72,7 +72,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json index 1474f8e66b..7f2f5f00f0 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 420, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json index 3d12692edb..5a829e9ec1 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json @@ -72,7 +72,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json index 1474f8e66b..7f2f5f00f0 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 420, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index d34307e5d0..770bb45995 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index a255d3d7ee..4b93ff6d9e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index 76d72c514b..abda343c6c 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index a255d3d7ee..4b93ff6d9e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json index 8a5adac7e0..0800c66e72 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 9b27a2e836..49faecdbd3 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json index cc9701636c..1da9b8f0f1 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 9b27a2e836..49faecdbd3 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index b4a1b08501..7a2f5a2b3d 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index 9c696b74b1..82ee3fbe0d 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index c1a1ddf7a2..6e2d490248 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index 9c696b74b1..82ee3fbe0d 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 131d5bd750..0654cc7bd6 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index 468a632a45..35c7429596 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index 1f0a9120d3..8494ca69ad 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index 468a632a45..35c7429596 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json index 4e2f0a9c7b..f7aacb8b97 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json index 9b27a2e836..49faecdbd3 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json index 36585d0732..53202f782e 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json index 9b27a2e836..49faecdbd3 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json index 2afce781a3..b340cb24e6 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json @@ -72,7 +72,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json index 6291f4b85f..f7ef9a0bf7 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 422, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json index ec07818996..31e8856aef 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json @@ -72,7 +72,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json index 6291f4b85f..f7ef9a0bf7 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 422, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index 4bf01eccca..f40ca32d01 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 0243347840..04fba978f8 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index 6dbae4f571..c1d1ae7baa 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 0243347840..04fba978f8 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "79c955ec2ddb80a68bfccf872bf137887793cb1d48d6478d0b503719233a0657", + "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", From ed4d9dbdd0f772bcaf1cb6ade50bc0de551c9801 Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 21:13:33 -0700 Subject: [PATCH 07/56] feat(process): deploy target-local controllers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- changelog.d/phase-v3-guest-u4.md | 3 + packages/d2b-process-conformance/src/suite.rs | 154 +- .../d2b-process-conformance/src/ticket.rs | 399 +++- .../d2b-provider-system-minijail/README.md | 3 + .../src/effect_port.rs | 146 +- .../d2b-provider-system-minijail/src/lib.rs | 108 +- .../tests/conformance.rs | 166 +- .../tests/execution_parents.rs | 71 +- .../d2b-provider-system-systemd/README.md | 3 + .../src/controller.rs | 12 +- .../src/effect_port.rs | 140 +- .../d2b-provider-system-systemd/src/lib.rs | 108 +- .../tests/conformance.rs | 166 +- .../tests/controller.rs | 96 +- .../tests/execution_parents.rs | 71 +- packages/d2b-provider-toolkit/src/agent.rs | 198 +- packages/d2b-provider-toolkit/src/runtime.rs | 579 ++++- packages/d2b-provider-toolkit/src/server.rs | 47 +- packages/d2bd-runtime/src/target_runtime.rs | 2074 ++++++++++++++++- packages/d2bd/src/process_provider_runtime.rs | 374 ++- packages/d2bd/src/provider_effects.rs | 99 +- packages/d2bd/src/provider_registry.rs | 397 +++- .../d2bd/tests/zone_provider_acceptance.rs | 250 +- 23 files changed, 5554 insertions(+), 110 deletions(-) create mode 100644 changelog.d/phase-v3-guest-u4.md diff --git a/changelog.d/phase-v3-guest-u4.md b/changelog.d/phase-v3-guest-u4.md new file mode 100644 index 0000000000..6f951d524f --- /dev/null +++ b/changelog.d/phase-v3-guest-u4.md @@ -0,0 +1,3 @@ +### Fixed + +- Process Provider controllers now validate launch tickets and exact process identity before readiness, adoption, and stop effects on Host and Guest targets. diff --git a/packages/d2b-process-conformance/src/suite.rs b/packages/d2b-process-conformance/src/suite.rs index d0cc9c7629..ef500fd4ee 100644 --- a/packages/d2b-process-conformance/src/suite.rs +++ b/packages/d2b-process-conformance/src/suite.rs @@ -13,10 +13,12 @@ use d2b_contracts_resource::v3::ResourceRef; use d2b_contracts_resource::v3::execution_policy::ExecutionDomain; use crate::error::ProcessConformanceError; -use crate::identity::IdentityBinding; +use crate::identity::{IdentityBinding, WaitReapOwner}; use crate::provider::{AdoptionOutcome, ProcessProvider}; +use crate::sandbox::{StopProof, validate_stop_proof}; use crate::status::{AdoptionCondition, ProcessPhaseClass}; use crate::testing::{PortCall, ScriptedEffectPort, block_on, fixtures}; +use crate::ticket::LaunchTicket; /// Field or value fragments that must never appear in public status. const FORBIDDEN_STATUS_FRAGMENTS: [&str; 12] = [ @@ -65,6 +67,10 @@ pub fn assert_launch_is_locality_neutral(provider: &P, provi assert_eq!(report.provider.as_str(), provider_name); assert_eq!(report.wait_reap_owner, port_owner); assert_eq!(report.execution_ref, execution_ref); + assert_eq!(report.domain, ticket.domain()); + assert_eq!(report.user_ref.as_ref(), ticket.user_ref()); + assert_eq!(report.digests, *ticket.digests()); + assert!(!report.identity.is_zero()); assert_eq!(report.phase, ProcessPhaseClass::Running); assert_eq!(report.adoption, AdoptionCondition::NotApplicable); assert!(report.last_exit.is_none()); @@ -240,6 +246,64 @@ pub fn assert_pidfd_open_follows_verification(port_calls: &[PortCall]) { } } +/// A fresh target-local controller launch is not an assignment. +pub fn assert_controller_launch_has_no_resource_client(ticket: &LaunchTicket) { + assert!( + ticket.validate_controller_launch().is_ok(), + "controller launch must carry only target-side launch evidence" + ); + assert!(ticket.provider_generation().is_some()); + assert!(!ticket.has_assignment_binding()); + assert!(ticket.resource_client_binding().is_none()); +} + +/// An assigned controller or child ticket is fenced to one session and epoch. +pub fn assert_assignment_is_session_fenced( + ticket: &LaunchTicket, + session_generation: u64, + assignment_epoch: u64, +) { + assert!(ticket.validate_assignment().is_ok()); + assert_eq!( + ticket + .session_generation() + .map(|generation| generation.get()), + Some(session_generation) + ); + assert_eq!(ticket.assignment_epoch(), Some(assignment_epoch)); + assert!(ticket.resource_client_binding().is_some()); +} + +/// Finalizer release requires the exact stop and owner-specific terminal +/// evidence; an ambiguous child cannot be treated as cleaned up. +pub fn assert_finalizer_requires_verified_stop(owner: WaitReapOwner) { + assert_eq!( + validate_stop_proof(owner, StopProof::default()), + Err(ProcessConformanceError::StopProofMissing) + ); + let complete = StopProof { + exact_main_signaled: true, + broker_reaped: owner == WaitReapOwner::Local, + cgroup_empty: true, + manager_terminal: owner == WaitReapOwner::ServiceManager, + }; + assert!(validate_stop_proof(owner, complete).is_ok()); +} + +/// Return whether every observed child supplied a complete, owner-specific +/// stop proof. A controller with no owned children is already converged. +pub fn children_have_verified_stop_proofs( + owner: WaitReapOwner, + expected_children: usize, + proofs: &[StopProof], +) -> bool { + proofs.len() == expected_children + && proofs + .iter() + .copied() + .all(|proof| validate_stop_proof(owner, proof).is_ok()) +} + /// Public status carries no PID, pidfd, unit name, cgroup, path, argv, /// environment, or numeric identity. pub fn assert_status_is_redacted(provider: &P, provider_name: &str) { @@ -271,3 +335,91 @@ pub fn assert_status_is_redacted(provider: &P, provider_name "ProcessIdentityDigest()" ); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::ConfigurationDigest; + use d2b_contracts_resource::v3::{ResourceGeneration, identity::ReconnectGeneration}; + + #[test] + fn controller_launch_and_assignment_authority_are_separate() { + let launch = fixtures::ticket_builder() + .build() + .unwrap() + .with_resource_revision(d2b_contracts_resource::v3::ZoneRevision::new(1)) + .unwrap() + .with_controller_launch_binding( + ResourceGeneration::new(2).unwrap(), + ReconnectGeneration::new(4).unwrap(), + ConfigurationDigest::from_bytes([1; 32]), + ConfigurationDigest::from_bytes([2; 32]), + ) + .unwrap(); + assert_controller_launch_has_no_resource_client(&launch); + + let assignment = fixtures::ticket_builder() + .build() + .unwrap() + .with_resource_revision(d2b_contracts_resource::v3::ZoneRevision::new(1)) + .unwrap() + .with_assignment_binding( + ResourceGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + 9, + ConfigurationDigest::from_bytes([3; 32]), + ) + .unwrap(); + assert_assignment_is_session_fenced(&assignment, 4, 9); + assert_ne!(launch, assignment); + } + + #[test] + fn reconnect_and_finalizer_proofs_fail_closed_on_stale_evidence() { + let first = fixtures::ticket_builder() + .build() + .unwrap() + .with_resource_revision(d2b_contracts_resource::v3::ZoneRevision::new(1)) + .unwrap() + .with_assignment_binding( + ResourceGeneration::new(1).unwrap(), + ReconnectGeneration::new(1).unwrap(), + 1, + ConfigurationDigest::from_bytes([4; 32]), + ) + .unwrap(); + let reconnect = fixtures::ticket_builder() + .build() + .unwrap() + .with_resource_revision(d2b_contracts_resource::v3::ZoneRevision::new(2)) + .unwrap() + .with_assignment_binding( + ResourceGeneration::new(1).unwrap(), + ReconnectGeneration::new(2).unwrap(), + 2, + ConfigurationDigest::from_bytes([5; 32]), + ) + .unwrap(); + assert_ne!(first.session_generation(), reconnect.session_generation()); + assert_ne!(first.assignment_epoch(), reconnect.assignment_epoch()); + assert_finalizer_requires_verified_stop(WaitReapOwner::Local); + assert_finalizer_requires_verified_stop(WaitReapOwner::ServiceManager); + let complete = StopProof { + exact_main_signaled: true, + broker_reaped: true, + cgroup_empty: true, + manager_terminal: true, + }; + assert!(children_have_verified_stop_proofs(WaitReapOwner::Local, 0, &[])); + assert!(!children_have_verified_stop_proofs( + WaitReapOwner::Local, + 2, + &[StopProof::default(), complete] + )); + assert!(children_have_verified_stop_proofs( + WaitReapOwner::Local, + 2, + &[complete, complete] + )); + } +} diff --git a/packages/d2b-process-conformance/src/ticket.rs b/packages/d2b-process-conformance/src/ticket.rs index 8f8abc87a6..aa40bd9649 100644 --- a/packages/d2b-process-conformance/src/ticket.rs +++ b/packages/d2b-process-conformance/src/ticket.rs @@ -10,8 +10,10 @@ use d2b_contracts_resource::v3::{ ControllerGeneration, ResourceGeneration, ResourceRef, + ZoneRevision, ResourceUid, }; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; use crate::error::ProcessConformanceError; use crate::identity::{ConfigurationDigest, IdentityBinding, ProcessIdentityDigest}; @@ -150,6 +152,20 @@ impl ReadinessExpectation { } Ok(Self::Condition { timeout_ms }) } + + /// Validate an expectation constructed from a decoded or otherwise + /// untrusted value. + pub const fn validate(self) -> Result<(), ProcessConformanceError> { + match self { + Self::None => Ok(()), + Self::Condition { timeout_ms } + if timeout_ms > 0 && timeout_ms <= MAX_LAUNCH_DEADLINE_MS => + { + Ok(()) + } + Self::Condition { .. } => Err(ProcessConformanceError::InvalidTicket), + } + } } /// The private inherited-fd table binding carried by a launch ticket. @@ -162,6 +178,33 @@ pub struct InheritedFdTable { count: u16, } +/// The target-side proof carried by a static controller launch. +/// +/// The target session and readiness commitments are opaque evidence produced +/// by Core. They do not grant a ResourceClient and cannot be combined with an +/// assignment binding on the same ticket. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ControllerLaunchBinding { + provider_generation: ResourceGeneration, + target_session_generation: ReconnectGeneration, + signed_descriptor_digest: ConfigurationDigest, + target_readiness_digest: ConfigurationDigest, +} + +/// The Core-issued assignment proof carried by an assigned controller or +/// Process child. +/// +/// The ResourceClient itself remains in Core. Only its opaque binding digest +/// crosses the Process Provider seam, together with the exact Provider, +/// session, and assignment generations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ControllerAssignmentBinding { + provider_generation: ResourceGeneration, + session_generation: ReconnectGeneration, + assignment_epoch: u64, + resource_client_binding: ConfigurationDigest, +} + impl InheritedFdTable { /// Build a bounded private table binding. pub fn new(digest: ConfigurationDigest, count: u16) -> Result { @@ -185,15 +228,20 @@ impl InheritedFdTable { /// The ticket a Process controller hands to the fixed process effect /// adapter. /// -/// It binds the resource, the owning Provider component and template, the -/// placement, the selected Process Provider, the compiled configuration -/// digests, the operation, and the identity bindings the launch is expected -/// to establish. Nothing in it names an executable, a host path, a numeric -/// UID or GID, a cgroup path, a broker operation, or an environment value. +/// It binds the resource and committed revision, the owning Provider +/// component and template, the placement, the selected Process Provider, the +/// compiled configuration digests, the operation, and the identity bindings +/// the launch is expected to establish. Static controller launches carry +/// target-session, signed-descriptor, and target-readiness commitments; +/// assignment-bound launches carry only an opaque ResourceClient commitment +/// plus the exact Provider/session/epoch fence. Nothing in it names an +/// executable, a host path, a numeric UID or GID, a cgroup path, a broker +/// operation, or an environment value. #[derive(Clone, PartialEq, Eq)] pub struct LaunchTicket { process_ref: ResourceRef, process_uid: ResourceUid, + resource_revision: Option, resource_generation: ResourceGeneration, controller_generation: ControllerGeneration, owner_provider: BoundedToken, @@ -211,6 +259,8 @@ pub struct LaunchTicket { readiness: ReadinessExpectation, inherited_fd_table: InheritedFdTable, sandbox: Option, + controller_launch: Option, + assignment: Option, } impl LaunchTicket { @@ -268,6 +318,7 @@ impl LaunchTicket { Ok(Self { process_ref, process_uid, + resource_revision: None, resource_generation, controller_generation, owner_provider, @@ -285,9 +336,152 @@ impl LaunchTicket { readiness: ReadinessExpectation::None, inherited_fd_table, sandbox: None, + controller_launch: None, + assignment: None, }) } + /// Bind the committed resource revision to this launch. + pub fn with_resource_revision( + mut self, + resource_revision: ZoneRevision, + ) -> Result { + if resource_revision.get() == 0 || self.resource_revision.is_some() { + return Err(ProcessConformanceError::InvalidTicket); + } + self.resource_revision = Some(resource_revision); + Ok(self) + } + + /// Attach the Core-produced proof for a target-local controller launch. + /// + /// A fresh controller receives only its Provider generation, target + /// session, signed component descriptor, and target readiness + /// commitments. It does not receive a ResourceClient or assignment + /// authority until a separate controller session is authenticated and + /// admitted. + pub fn with_controller_launch_binding( + mut self, + provider_generation: ResourceGeneration, + target_session_generation: ReconnectGeneration, + signed_descriptor_digest: ConfigurationDigest, + target_readiness_digest: ConfigurationDigest, + ) -> Result { + if signed_descriptor_digest.is_zero() + || target_readiness_digest.is_zero() + || self.assignment.is_some() + || self.controller_launch.is_some() + || self.process_ref.resource_type().as_str() != "Process" + { + return Err(ProcessConformanceError::InvalidTicket); + } + self.controller_launch = Some(ControllerLaunchBinding { + provider_generation, + target_session_generation, + signed_descriptor_digest, + target_readiness_digest, + }); + Ok(self) + } + + /// Attach the separate Core-issued assignment and ResourceClient proof. + /// + /// This operation is intentionally incompatible with a static controller + /// launch binding. A controller must authenticate and become Ready before + /// it can receive this assignment-scoped authority. + pub fn with_assignment_binding( + mut self, + provider_generation: ResourceGeneration, + session_generation: ReconnectGeneration, + assignment_epoch: u64, + resource_client_binding: ConfigurationDigest, + ) -> Result { + if assignment_epoch == 0 + || resource_client_binding.is_zero() + || self.controller_launch.is_some() + || self.assignment.is_some() + { + return Err(ProcessConformanceError::InvalidTicket); + } + self.assignment = Some(ControllerAssignmentBinding { + provider_generation, + session_generation, + assignment_epoch, + resource_client_binding, + }); + Ok(self) + } + + /// Validate this ticket before handing it to an effect adapter. + pub fn validate(&self) -> Result<(), ProcessConformanceError> { + if !matches!( + self.process_ref.resource_type().as_str(), + "Process" | "EphemeralProcess" + ) || !matches!( + self.execution_ref.resource_type().as_str(), + "Host" | "Guest" + ) || (self.domain == ExecutionDomain::User && self.user_ref.is_none()) + || (self.domain == ExecutionDomain::System && self.user_ref.is_some()) + || self.expected_identity.is_empty() + { + return Err(ProcessConformanceError::InvalidTicket); + } + if self + .resource_revision + .is_some_and(|revision| revision.get() == 0) + { + return Err(ProcessConformanceError::InvalidTicket); + } + self.digests.validate()?; + self.readiness.validate()?; + if let Some(binding) = self.controller_launch { + if binding.provider_generation.get() == 0 + || binding.target_session_generation.get() == 0 + || binding.signed_descriptor_digest.is_zero() + || binding.target_readiness_digest.is_zero() + || self.process_ref.resource_type().as_str() != "Process" + { + return Err(ProcessConformanceError::InvalidTicket); + } + } + if let Some(binding) = self.assignment { + if binding.provider_generation.get() == 0 + || binding.session_generation.get() == 0 + || binding.assignment_epoch == 0 + || binding.resource_client_binding.is_zero() + { + return Err(ProcessConformanceError::InvalidTicket); + } + } + Ok(()) + } + + /// Validate that this ticket is a static target-local controller launch. + pub fn validate_controller_launch(&self) -> Result<(), ProcessConformanceError> { + self.validate()?; + if self.controller_launch.is_some() + && self.assignment.is_none() + && self.resource_revision.is_some() + { + Ok(()) + } else { + Err(ProcessConformanceError::InvalidTicket) + } + } + + /// Validate that this ticket carries an assignment-scoped capability. + pub fn validate_assignment(&self) -> Result<(), ProcessConformanceError> { + self.validate()?; + if self.assignment.is_some() + && self.controller_launch.is_none() + && self.resource_revision.is_some() + { + Ok(()) + } else { + Err(ProcessConformanceError::InvalidTicket) + } + } + /// Attach a bounded readiness expectation. #[must_use] pub fn with_readiness(mut self, readiness: ReadinessExpectation) -> Self { @@ -295,6 +489,16 @@ impl LaunchTicket { self } + /// Attach a readiness expectation while rejecting malformed decoded data. + pub fn try_with_readiness( + mut self, + readiness: ReadinessExpectation, + ) -> Result { + readiness.validate()?; + self.readiness = readiness; + Ok(self) + } + /// Bind the complete typed sandbox plan to the launch. #[must_use] pub fn with_sandbox_plan(mut self, sandbox: SandboxPlan) -> Self { @@ -317,7 +521,7 @@ impl LaunchTicket { mut self, identity: ProcessIdentityDigest, ) -> Result { - if identity.is_zero() { + if identity.is_zero() || self.expected_identity_digest.is_some() { return Err(ProcessConformanceError::InvalidTicket); } self.expected_identity_digest = Some(identity); @@ -341,6 +545,11 @@ impl LaunchTicket { &self.process_uid } + /// Return the committed resource revision, when one was bound. + pub const fn resource_revision(&self) -> Option { + self.resource_revision + } + /// Return the resource generation this launch is for. pub const fn resource_generation(&self) -> ResourceGeneration { self.resource_generation @@ -411,6 +620,22 @@ impl LaunchTicket { self.expected_identity_digest.as_ref() } + /// Validate a returned process identity against this ticket's optional + /// adoption/terminal identity seal. + pub fn validate_process_identity( + &self, + identity: &ProcessIdentityDigest, + ) -> Result<(), ProcessConformanceError> { + if self + .expected_identity_digest + .is_some_and(|expected| &expected != identity) + { + Err(ProcessConformanceError::TerminalEvidenceMismatch) + } else { + Ok(()) + } + } + /// Return the readiness expectation. pub const fn readiness(&self) -> ReadinessExpectation { self.readiness @@ -420,6 +645,76 @@ impl LaunchTicket { pub const fn inherited_fd_table(&self) -> &InheritedFdTable { &self.inherited_fd_table } + + /// Whether this ticket carries a static controller launch proof. + pub const fn has_controller_launch_binding(&self) -> bool { + self.controller_launch.is_some() + } + + /// Return the target session generation bound to a static controller + /// launch. + pub const fn target_session_generation(&self) -> Option { + match self.controller_launch { + Some(binding) => Some(binding.target_session_generation), + None => None, + } + } + + /// Return the signed component descriptor commitment. + pub const fn signed_descriptor_digest(&self) -> Option { + match self.controller_launch { + Some(binding) => Some(binding.signed_descriptor_digest), + None => None, + } + } + + /// Return the target readiness commitment. + pub const fn target_readiness_digest(&self) -> Option { + match self.controller_launch { + Some(binding) => Some(binding.target_readiness_digest), + None => None, + } + } + + /// Whether this ticket carries a Core-issued assignment proof. + pub const fn has_assignment_binding(&self) -> bool { + self.assignment.is_some() + } + + /// Return the Provider generation bound to this launch or assignment. + pub const fn provider_generation(&self) -> Option { + match self.assignment { + Some(binding) => Some(binding.provider_generation), + None => match self.controller_launch { + Some(binding) => Some(binding.provider_generation), + None => None, + }, + } + } + + /// Return the authenticated session generation bound to the assignment. + pub const fn session_generation(&self) -> Option { + match self.assignment { + Some(binding) => Some(binding.session_generation), + None => None, + } + } + + /// Return the assignment epoch bound to the assignment. + pub const fn assignment_epoch(&self) -> Option { + match self.assignment { + Some(binding) => Some(binding.assignment_epoch), + None => None, + } + } + + /// Borrow the opaque ResourceClient binding commitment. + pub const fn resource_client_binding(&self) -> Option { + match self.assignment { + Some(binding) => Some(binding.resource_client_binding), + None => None, + } + } } impl fmt::Debug for LaunchTicket { @@ -484,4 +779,96 @@ mod tests { let ticket = fixtures::ticket_builder().build().unwrap(); assert_eq!(format!("{ticket:?}"), "LaunchTicket()"); } + + #[test] + fn controller_launch_proof_cannot_carry_assignment_authority() { + assert_eq!( + fixtures::ticket_builder() + .build() + .unwrap() + .with_resource_revision(ZoneRevision::new(1)) + .unwrap() + .validate_controller_launch(), + Err(ProcessConformanceError::InvalidTicket) + ); + let ticket = fixtures::ticket_builder() + .build() + .unwrap() + .with_resource_revision(ZoneRevision::new(1)) + .unwrap() + .with_controller_launch_binding( + ResourceGeneration::new(2).unwrap(), + d2b_contracts_resource::v3::identity::ReconnectGeneration::new(2).unwrap(), + ConfigurationDigest::from_bytes([8; 32]), + ConfigurationDigest::from_bytes([9; 32]), + ) + .unwrap(); + assert!(ticket.has_controller_launch_binding()); + assert!(!ticket.has_assignment_binding()); + assert!(ticket.resource_client_binding().is_none()); + assert_eq!(format!("{ticket:?}"), "LaunchTicket()"); + assert_eq!( + ticket.with_assignment_binding( + ResourceGeneration::new(2).unwrap(), + d2b_contracts_resource::v3::identity::ReconnectGeneration::new(2).unwrap(), + 1, + ConfigurationDigest::from_bytes([10; 32]), + ), + Err(ProcessConformanceError::InvalidTicket) + ); + } + + #[test] + fn assignment_binding_requires_nonzero_session_epoch_and_client_commitment() { + let ticket = fixtures::ticket_builder().build().unwrap(); + let session = d2b_contracts_resource::v3::identity::ReconnectGeneration::new(3).unwrap(); + let bound = ticket + .with_resource_revision(ZoneRevision::new(1)) + .unwrap() + .with_assignment_binding( + ResourceGeneration::new(2).unwrap(), + session, + 7, + ConfigurationDigest::from_bytes([11; 32]), + ) + .unwrap(); + assert!(bound.has_assignment_binding()); + assert_eq!(bound.provider_generation().unwrap().get(), 2); + assert_eq!(bound.session_generation().unwrap().get(), 3); + assert_eq!(bound.assignment_epoch(), Some(7)); + assert_eq!( + bound.resource_client_binding(), + Some(ConfigurationDigest::from_bytes([11; 32])) + ); + } + + #[test] + fn malformed_readiness_is_rejected_by_ticket_validation() { + let ticket = fixtures::ticket_builder() + .build() + .unwrap() + .with_readiness(ReadinessExpectation::Condition { timeout_ms: 0 }); + assert_eq!( + ticket.validate(), + Err(ProcessConformanceError::InvalidTicket) + ); + } + + #[test] + fn an_expected_process_identity_seal_rejects_a_reused_identity() { + let ticket = fixtures::ticket_builder() + .build() + .unwrap() + .with_expected_identity_digest(ProcessIdentityDigest::from_bytes([1; 32])) + .unwrap(); + assert!( + ticket + .validate_process_identity(&ProcessIdentityDigest::from_bytes([1; 32])) + .is_ok() + ); + assert_eq!( + ticket.validate_process_identity(&ProcessIdentityDigest::from_bytes([2; 32])), + Err(ProcessConformanceError::TerminalEvidenceMismatch) + ); + } } diff --git a/packages/d2b-provider-system-minijail/README.md b/packages/d2b-provider-system-minijail/README.md index 14d0f638ac..f0ccdce852 100644 --- a/packages/d2b-provider-system-minijail/README.md +++ b/packages/d2b-provider-system-minijail/README.md @@ -57,6 +57,9 @@ generation before any `pidfd_open`. The pid and start-time pair is the pid-reuse guard: a matching pid whose start time disagrees is a different process, so it is ambiguity. Ambiguity quarantines and reports Unknown; it never signals, kills, or reuses. +Launch tickets are validated before the effect boundary, and a requested +readiness condition must observe the same verified identity before status +reports `Ready`; an unmet condition terminates only that verified launch. ## State and telemetry diff --git a/packages/d2b-provider-system-minijail/src/effect_port.rs b/packages/d2b-provider-system-minijail/src/effect_port.rs index 97690ad837..f54f55f800 100644 --- a/packages/d2b-provider-system-minijail/src/effect_port.rs +++ b/packages/d2b-provider-system-minijail/src/effect_port.rs @@ -3,8 +3,9 @@ use std::future::Future; use d2b_process_conformance::{ - AdoptionCandidate, LaunchTicket, LaunchedProcess, PidfdEvidence, ProcessConformanceError, - ProcessIdentityDigest, ProcessLaunchEffectPort, StopClass, + AdoptionCandidate, CancellationBinding, IdentityBinding, LaunchTicket, LaunchedProcess, + PidfdEvidence, ProcessConformanceError, ProcessIdentityDigest, ProcessLaunchEffectPort, + StopClass, WaitReapOwner, }; /// The core-owned minijail effect port. @@ -38,6 +39,25 @@ pub trait MinijailProcessEffectPort: Send + Sync { /// Adapt the minijail-specific effect port to the neutral conformance seam. pub struct EffectPortAdapter

(pub P); +fn validate_ticket(ticket: &LaunchTicket) -> Result<(), ProcessConformanceError> { + ticket.validate()?; + if ticket.has_controller_launch_binding() { + ticket.validate_controller_launch()?; + } + if ticket.has_assignment_binding() { + ticket.validate_assignment()?; + } + if ticket.selected_provider().as_str() != crate::PROVIDER_NAME + || ticket.provider_ref().to_canonical_string() != "Provider/system-minijail" + { + return Err(ProcessConformanceError::ProviderMismatch); + } + if ticket.operation().cancellation() == CancellationBinding::Cancelled { + return Err(ProcessConformanceError::Cancelled); + } + Ok(()) +} + impl

ProcessLaunchEffectPort for EffectPortAdapter

where P: MinijailProcessEffectPort, @@ -46,6 +66,7 @@ where &self, ticket: &LaunchTicket, ) -> Result { + validate_ticket(ticket)?; self.0.spawn(ticket).await } @@ -53,6 +74,7 @@ where &self, ticket: &LaunchTicket, ) -> Result, ProcessConformanceError> { + validate_ticket(ticket)?; self.0.observe(ticket).await } @@ -60,6 +82,17 @@ where &self, candidate: &AdoptionCandidate, ) -> Result { + if candidate.wait_reap_owner != WaitReapOwner::Local { + return Err(ProcessConformanceError::WaitOwnerMismatch); + } + candidate.validate(&std::collections::BTreeSet::from([ + IdentityBinding::Pid, + IdentityBinding::ProcessStartTime, + IdentityBinding::Cgroup, + IdentityBinding::Executable, + IdentityBinding::Template, + IdentityBinding::Generation, + ]))?; self.0.duplicate_pidfd(candidate).await } @@ -68,6 +101,115 @@ where identity: &ProcessIdentityDigest, class: StopClass, ) -> Result<(), ProcessConformanceError> { + if identity.is_zero() { + return Err(ProcessConformanceError::IdentityUnverified); + } self.0.stop(identity, class).await } } + +#[cfg(test)] +mod tests { + use std::sync::Mutex; + + use d2b_process_conformance::testing::fixtures; + use d2b_process_conformance::{ + ObservedIdentity, PidfdEvidence, ProcessIdentityDigest, WaitReapOwner, + }; + + use super::*; + + #[derive(Debug, Default)] + struct RecordingPort { + calls: Mutex>, + } + + impl MinijailProcessEffectPort for RecordingPort { + fn spawn( + &self, + _ticket: &LaunchTicket, + ) -> impl Future> + Send { + self.calls.lock().unwrap().push("spawn"); + std::future::ready(Ok(LaunchedProcess { + identity: ProcessIdentityDigest::from_bytes([0x11; 32]), + observed: ObservedIdentity::from_verified([ + IdentityBinding::Pid, + IdentityBinding::ProcessStartTime, + IdentityBinding::Cgroup, + IdentityBinding::Executable, + IdentityBinding::Template, + IdentityBinding::Generation, + ]), + pidfd: PidfdEvidence::held(), + wait_reap_owner: WaitReapOwner::Local, + })) + } + + fn observe( + &self, + _ticket: &LaunchTicket, + ) -> impl Future, ProcessConformanceError>> + Send + { + self.calls.lock().unwrap().push("observe"); + std::future::ready(Ok(None)) + } + + fn duplicate_pidfd( + &self, + _candidate: &AdoptionCandidate, + ) -> impl Future> + Send { + self.calls.lock().unwrap().push("duplicate_pidfd"); + std::future::ready(Ok(PidfdEvidence::held())) + } + + fn stop( + &self, + _identity: &ProcessIdentityDigest, + _class: StopClass, + ) -> impl Future> + Send { + self.calls.lock().unwrap().push("stop"); + std::future::ready(Ok(())) + } + } + + #[test] + fn effect_port_rejects_invalid_tickets_before_the_minijail_boundary() { + let adapter = EffectPortAdapter(RecordingPort::default()); + let ticket = fixtures::ticket_builder() + .selected_provider(crate::PROVIDER_NAME) + .build() + .unwrap() + .with_readiness(d2b_process_conformance::ReadinessExpectation::Condition { + timeout_ms: 0, + }); + + assert_eq!( + d2b_process_conformance::testing::block_on(adapter.launch(&ticket)).unwrap_err(), + ProcessConformanceError::InvalidTicket + ); + assert!(adapter.0.calls.lock().unwrap().is_empty()); + } + + #[test] + fn effect_port_rejects_unverified_pidfd_candidates_and_zero_stops() { + let adapter = EffectPortAdapter(RecordingPort::default()); + let candidate = AdoptionCandidate { + identity: ProcessIdentityDigest::from_bytes([0x11; 32]), + observed: ObservedIdentity::default(), + wait_reap_owner: WaitReapOwner::Local, + }; + assert_eq!( + d2b_process_conformance::testing::block_on(adapter.open_pidfd(&candidate)).unwrap_err(), + ProcessConformanceError::IdentityUnverified + ); + assert_eq!( + d2b_process_conformance::testing::block_on(adapter.stop( + &ProcessIdentityDigest::from_bytes([0; 32]), + StopClass::Terminate, + )) + .unwrap_err(), + ProcessConformanceError::IdentityUnverified + ); + assert!(adapter.0.calls.lock().unwrap().is_empty()); + } +} diff --git a/packages/d2b-provider-system-minijail/src/lib.rs b/packages/d2b-provider-system-minijail/src/lib.rs index 143b185a74..5deafc9ff9 100644 --- a/packages/d2b-provider-system-minijail/src/lib.rs +++ b/packages/d2b-provider-system-minijail/src/lib.rs @@ -35,13 +35,12 @@ pub mod user_ns; use std::collections::BTreeSet; -use d2b_contracts_resource::v3::{ - execution_policy::{BoundedToken, ExecutionDomain}, -}; +use d2b_contracts_resource::v3::execution_policy::{BoundedToken, ExecutionDomain}; use d2b_process_conformance::{ - AdoptionCondition, AdoptionOutcome, CancellationBinding, IdentityBinding, LaunchTicket, - ProcessConformanceError, ProcessIdentityDigest, ProcessLaunchEffectPort, ProcessPhaseClass, - ProcessProvider, ProcessProviderProfile, ProcessStatusReport, StopClass, WaitReapOwner, + AdoptionCandidate, AdoptionCondition, AdoptionOutcome, CancellationBinding, IdentityBinding, + LaunchTicket, LaunchedProcess, ProcessConformanceError, ProcessIdentityDigest, + ProcessLaunchEffectPort, ProcessPhaseClass, ProcessProvider, ProcessProviderProfile, + ProcessStatusReport, ReadinessExpectation, StopClass, WaitReapOwner, }; /// The Provider name this controller implements. @@ -120,6 +119,13 @@ impl MinijailProcessProvider

{ } fn validate(&self, ticket: &LaunchTicket) -> Result<(), ProcessConformanceError> { + ticket.validate()?; + if ticket.has_controller_launch_binding() { + ticket.validate_controller_launch()?; + } + if ticket.has_assignment_binding() { + ticket.validate_assignment()?; + } if ticket.selected_provider().as_str() != PROVIDER_NAME { return Err(ProcessConformanceError::ProviderMismatch); } @@ -138,6 +144,61 @@ impl MinijailProcessProvider

{ Ok(()) } + async fn cleanup_failed_launch( + &self, + launched: &LaunchedProcess, + error: ProcessConformanceError, + ) -> ProcessConformanceError { + if launched.identity.is_zero() { + return error; + } + match self + .port + .stop(&launched.identity, StopClass::Terminate) + .await + { + Ok(()) => error, + Err(_) => ProcessConformanceError::StopUnavailable, + } + } + + async fn readiness_phase( + &self, + ticket: &LaunchTicket, + identity: ProcessIdentityDigest, + ) -> Result { + match ticket.readiness() { + ReadinessExpectation::None => Ok(ProcessPhaseClass::Running), + ReadinessExpectation::Condition { .. } => { + // The fixed adapter's probe is the readiness observation; + // it does not open or retain another pidfd. + let Some(candidate) = self.port.probe(ticket).await? else { + return Err(ProcessConformanceError::DeadlineExceeded); + }; + if !self.candidate_matches(ticket, &candidate, identity) { + return Err(ProcessConformanceError::AdoptionAmbiguous); + } + Ok(ProcessPhaseClass::Ready) + } + } + } + + fn candidate_matches( + &self, + ticket: &LaunchTicket, + candidate: &AdoptionCandidate, + identity: ProcessIdentityDigest, + ) -> bool { + candidate.identity == identity + && candidate.wait_reap_owner == WaitReapOwner::Local + && candidate + .validate(self.profile.required_identity_bindings()) + .is_ok() + && ticket + .validate_process_identity(&candidate.identity) + .is_ok() + } + fn report( &self, ticket: &LaunchTicket, @@ -176,12 +237,16 @@ impl ProcessProvider for MinijailProcessProvider

return Err(ProcessConformanceError::WaitOwnerMismatch); } launched.validate(self.profile.required_identity_bindings())?; - Ok(self.report( - ticket, - launched.identity, - ProcessPhaseClass::Running, - AdoptionCondition::NotApplicable, - )) + ticket.validate_process_identity(&launched.identity)?; + match self.readiness_phase(ticket, launched.identity).await { + Ok(phase) => Ok(self.report( + ticket, + launched.identity, + phase, + AdoptionCondition::NotApplicable, + )), + Err(error) => Err(self.cleanup_failed_launch(&launched, error).await), + } } async fn adopt( @@ -195,6 +260,9 @@ impl ProcessProvider for MinijailProcessProvider

let identity_ok = candidate.wait_reap_owner == WaitReapOwner::Local && candidate .validate(self.profile.required_identity_bindings()) + .is_ok() + && ticket + .validate_process_identity(&candidate.identity) .is_ok(); if !identity_ok { return Ok(AdoptionOutcome::Quarantined(self.report( @@ -204,11 +272,22 @@ impl ProcessProvider for MinijailProcessProvider

AdoptionCondition::Quarantined, ))); } + let phase = match self.readiness_phase(ticket, candidate.identity).await { + Ok(phase) => phase, + Err(_) => { + return Ok(AdoptionOutcome::Quarantined(self.report( + ticket, + candidate.identity, + ProcessPhaseClass::Unknown, + AdoptionCondition::Quarantined, + ))); + } + }; let _pidfd = self.port.open_pidfd(&candidate).await?; Ok(AdoptionOutcome::Adopted(self.report( ticket, candidate.identity, - ProcessPhaseClass::Running, + phase, AdoptionCondition::Adopted, ))) } @@ -218,6 +297,9 @@ impl ProcessProvider for MinijailProcessProvider

identity: &ProcessIdentityDigest, class: StopClass, ) -> Result<(), ProcessConformanceError> { + if identity.is_zero() { + return Err(ProcessConformanceError::IdentityUnverified); + } self.port.stop(identity, class).await } } diff --git a/packages/d2b-provider-system-minijail/tests/conformance.rs b/packages/d2b-provider-system-minijail/tests/conformance.rs index bc564fa686..e13994b6ce 100644 --- a/packages/d2b-provider-system-minijail/tests/conformance.rs +++ b/packages/d2b-provider-system-minijail/tests/conformance.rs @@ -2,10 +2,14 @@ //! plus the minijail-specific pidfd, wait-ownership, and adoption //! obligations. +use d2b_contracts_resource::v3::ResourceGeneration; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; use d2b_process_conformance::suite; use d2b_process_conformance::testing::{PortCall, ScriptedEffectPort, block_on, fixtures}; use d2b_process_conformance::{ - AdoptionOutcome, IdentityBinding, ProcessConformanceError, ProcessProvider, WaitReapOwner, + AdoptionOutcome, ConfigurationDigest, IdentityBinding, ProcessConformanceError, + ProcessIdentityDigest, ProcessPhaseClass, ProcessProvider, ReadinessExpectation, StopClass, + WaitReapOwner, }; use d2b_provider_system_minijail::{MinijailProcessProvider, PROVIDER_NAME}; @@ -39,6 +43,7 @@ fn shared_conformance_holds() { suite::assert_status_is_redacted(&launching(), PROVIDER_NAME); suite::assert_incomplete_launch_identity_fails_closed(provider, PROVIDER_NAME); suite::assert_adoption_verifies_identity_before_opening_a_pidfd(provider, PROVIDER_NAME); + suite::assert_finalizer_requires_verified_stop(WaitReapOwner::Local); } #[test] @@ -60,6 +65,7 @@ fn d2b_owns_wait_and_reap() { block_on(mismatched.launch(&ticket)).unwrap_err(), ProcessConformanceError::WaitOwnerMismatch ); + assert_eq!(mismatched.port().calls(), vec![PortCall::Launch]); } #[test] @@ -134,3 +140,161 @@ fn a_fully_verified_candidate_is_adopted_after_observation() { assert_eq!(calls, vec![PortCall::Observe, PortCall::OpenPidfd]); suite::assert_pidfd_open_follows_verification(&calls); } + +#[test] +fn malformed_readiness_is_rejected_before_effect_dispatch() { + let provider = provider(ScriptedEffectPort::launching( + required(), + WaitReapOwner::Local, + )); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_readiness(ReadinessExpectation::Condition { timeout_ms: 0 }); + + assert_eq!( + block_on(provider.launch(&ticket)).unwrap_err(), + ProcessConformanceError::InvalidTicket + ); + assert!(provider.port().calls().is_empty()); +} + +#[test] +fn readiness_is_verified_before_reporting_ready() { + let provider = provider( + ScriptedEffectPort::launching(required(), WaitReapOwner::Local) + .with_candidate(required(), WaitReapOwner::Local), + ); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_readiness(ReadinessExpectation::condition(1_000).expect("bounded readiness")); + + let report = block_on(provider.launch(&ticket)).expect("ready launch"); + assert_eq!(report.phase, ProcessPhaseClass::Ready); + assert_eq!( + provider.port().calls(), + vec![PortCall::Launch, PortCall::Observe] + ); +} + +#[test] +fn a_readiness_timeout_stops_the_exact_launched_identity() { + let provider = launching(); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_readiness(ReadinessExpectation::condition(1_000).expect("bounded readiness")); + + assert_eq!( + block_on(provider.launch(&ticket)).unwrap_err(), + ProcessConformanceError::DeadlineExceeded + ); + assert_eq!( + provider.port().calls(), + vec![ + PortCall::Launch, + PortCall::Observe, + PortCall::Stop(StopClass::Terminate) + ] + ); +} + +#[test] +fn an_adoption_identity_seal_mismatch_is_quarantined_before_pidfd_open() { + let provider = provider( + ScriptedEffectPort::launching(required(), WaitReapOwner::Local) + .with_candidate(required(), WaitReapOwner::Local), + ); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_expected_identity_digest(ProcessIdentityDigest::from_bytes([0x22; 32])) + .expect("nonzero identity seal"); + + assert!(matches!( + block_on(provider.adopt(&ticket)).expect("adoption result"), + AdoptionOutcome::Quarantined(report) + if report.phase == ProcessPhaseClass::Unknown + )); + assert_eq!(provider.port().calls(), vec![PortCall::Observe]); +} + +#[test] +fn a_launch_identity_seal_mismatch_fails_closed_without_signalling() { + let provider = launching(); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_expected_identity_digest(ProcessIdentityDigest::from_bytes([0x22; 32])) + .expect("nonzero identity seal"); + + assert_eq!( + block_on(provider.launch(&ticket)).unwrap_err(), + ProcessConformanceError::TerminalEvidenceMismatch + ); + assert_eq!(provider.port().calls(), vec![PortCall::Launch]); +} + +#[test] +fn stopping_a_zero_identity_is_rejected_without_an_effect() { + let provider = launching(); + let identity = ProcessIdentityDigest::from_bytes([0; 32]); + + assert_eq!( + block_on(provider.stop(&identity, StopClass::Terminate)).unwrap_err(), + ProcessConformanceError::IdentityUnverified + ); + assert!(provider.port().calls().is_empty()); +} + +#[test] +fn controller_authority_requires_a_committed_revision_before_launch() { + let controller_ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_controller_launch_binding( + ResourceGeneration::new(2).expect("provider generation"), + ReconnectGeneration::new(4).expect("session generation"), + ConfigurationDigest::from_bytes([1; 32]), + ConfigurationDigest::from_bytes([2; 32]), + ) + .expect("controller binding"); + let provider = launching(); + assert_eq!( + block_on(provider.launch(&controller_ticket)).unwrap_err(), + ProcessConformanceError::InvalidTicket + ); + assert!(provider.port().calls().is_empty()); + + let assignment_ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_assignment_binding( + ResourceGeneration::new(2).expect("provider generation"), + ReconnectGeneration::new(4).expect("session generation"), + 9, + ConfigurationDigest::from_bytes([3; 32]), + ) + .expect("assignment binding"); + let provider = launching(); + assert_eq!( + block_on(provider.launch(&assignment_ticket)).unwrap_err(), + ProcessConformanceError::InvalidTicket + ); + assert!(provider.port().calls().is_empty()); +} diff --git a/packages/d2b-provider-system-minijail/tests/execution_parents.rs b/packages/d2b-provider-system-minijail/tests/execution_parents.rs index 8647ed9f8e..2ac18d0d82 100644 --- a/packages/d2b-provider-system-minijail/tests/execution_parents.rs +++ b/packages/d2b-provider-system-minijail/tests/execution_parents.rs @@ -9,10 +9,13 @@ use d2b_contracts_resource::v3::ResourceRef; use d2b_contracts_resource::v3::execution_policy::ExecutionDomain; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; +use d2b_contracts_resource::v3::{ResourceGeneration, ZoneRevision}; use d2b_process_conformance::testing::{PortCall, ScriptedEffectPort, block_on, fixtures}; use d2b_process_conformance::{ - AdoptionCondition, AdoptionOutcome, IdentityBinding, ProcessConformanceError, ProcessProvider, - WaitReapOwner, + AdoptionCondition, AdoptionOutcome, ConfigurationDigest, IdentityBinding, + ProcessConformanceError, ProcessIdentityDigest, ProcessPhaseClass, ProcessProvider, + ReadinessExpectation, WaitReapOwner, }; use d2b_provider_system_minijail::{MinijailProcessProvider, PROVIDER_NAME}; @@ -164,3 +167,67 @@ fn a_candidate_whose_wait_owner_disagrees_is_quarantined() { )); assert!(!provider.port().calls().contains(&PortCall::OpenPidfd)); } + +#[test] +fn readiness_and_identity_seals_are_parent_neutral() { + for execution_ref in [host_ref(), guest_ref()] { + let provider = provider( + ScriptedEffectPort::launching(required(), WaitReapOwner::Local) + .with_candidate(required(), WaitReapOwner::Local), + ); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .execution_ref(execution_ref.clone()) + .build() + .expect("conformant ticket") + .with_readiness(ReadinessExpectation::condition(1_000).expect("bounded readiness")) + .with_expected_identity_digest(ProcessIdentityDigest::from_bytes([0x11; 32])) + .expect("identity seal"); + + let report = block_on(provider.launch(&ticket)).expect("ready launch"); + assert_eq!(report.execution_ref, execution_ref); + assert_eq!(report.phase, ProcessPhaseClass::Ready); + assert_eq!( + provider.port().calls(), + vec![PortCall::Launch, PortCall::Observe] + ); + } +} + +#[test] +fn controller_launch_and_assignment_authority_remain_disjoint_on_each_parent() { + for execution_ref in [host_ref(), guest_ref()] { + let launch = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .execution_ref(execution_ref.clone()) + .build() + .expect("conformant ticket") + .with_resource_revision(ZoneRevision::new(1)) + .expect("revision binding") + .with_controller_launch_binding( + ResourceGeneration::new(2).expect("provider generation"), + ReconnectGeneration::new(4).expect("session generation"), + ConfigurationDigest::from_bytes([1; 32]), + ConfigurationDigest::from_bytes([2; 32]), + ) + .expect("controller launch binding"); + d2b_process_conformance::suite::assert_controller_launch_has_no_resource_client(&launch); + + let assignment = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .execution_ref(execution_ref) + .build() + .expect("conformant ticket") + .with_resource_revision(ZoneRevision::new(1)) + .expect("revision binding") + .with_assignment_binding( + ResourceGeneration::new(2).expect("provider generation"), + ReconnectGeneration::new(4).expect("session generation"), + 9, + ConfigurationDigest::from_bytes([3; 32]), + ) + .expect("assignment binding"); + d2b_process_conformance::suite::assert_assignment_is_session_fenced(&assignment, 4, 9); + } +} diff --git a/packages/d2b-provider-system-systemd/README.md b/packages/d2b-provider-system-systemd/README.md index b2977b8f36..c7ce9b6b46 100644 --- a/packages/d2b-provider-system-systemd/README.md +++ b/packages/d2b-provider-system-systemd/README.md @@ -58,6 +58,9 @@ implements and which is the sole caller of the systemd effect owner. Adoption revalidates every required identity binding before a pidfd is opened. Ambiguity quarantines; it never signals, kills, or reuses. +Launch tickets are validated before the effect boundary, and a requested +readiness condition must observe the same verified identity before status +reports `Ready`; an unmet condition terminates only that verified launch. ## State and telemetry diff --git a/packages/d2b-provider-system-systemd/src/controller.rs b/packages/d2b-provider-system-systemd/src/controller.rs index 1fe0a5bb12..2f1854aae8 100644 --- a/packages/d2b-provider-system-systemd/src/controller.rs +++ b/packages/d2b-provider-system-systemd/src/controller.rs @@ -71,9 +71,15 @@ impl SystemdProcessControll SystemdReconcileAction::Adopt(_) => self.config.user_manager_check_timeout, SystemdReconcileAction::Stop(_, _) => self.config.termination_grace_sec.max(1), }; - let permit = Arc::clone(&self.launch_slots) - .try_acquire_owned() - .map_err(|_| ProcessConformanceError::DeadlineExceeded)?; + let permit = if matches!(action, SystemdReconcileAction::Start(_)) { + Some( + Arc::clone(&self.launch_slots) + .try_acquire_owned() + .map_err(|_| ProcessConformanceError::DeadlineExceeded)?, + ) + } else { + None + }; let operation = async { match action { SystemdReconcileAction::Start(ticket) => self diff --git a/packages/d2b-provider-system-systemd/src/effect_port.rs b/packages/d2b-provider-system-systemd/src/effect_port.rs index b6760b980d..aec123794e 100644 --- a/packages/d2b-provider-system-systemd/src/effect_port.rs +++ b/packages/d2b-provider-system-systemd/src/effect_port.rs @@ -7,8 +7,9 @@ use std::future::Future; use d2b_process_conformance::{ - AdoptionCandidate, LaunchTicket, LaunchedProcess, ProcessConformanceError, - ProcessIdentityDigest, ProcessLaunchEffectPort, StopClass, + AdoptionCandidate, CancellationBinding, IdentityBinding, LaunchTicket, LaunchedProcess, + ProcessConformanceError, ProcessIdentityDigest, ProcessLaunchEffectPort, StopClass, + WaitReapOwner, }; /// The systemd-specific effect seam. @@ -44,6 +45,21 @@ pub trait SystemdProcessEffectPort: Send + Sync { /// Adapt a systemd effect port to the neutral Process Provider seam. pub struct EffectPortAdapter

(pub P); +fn validate_ticket(ticket: &LaunchTicket) -> Result<(), ProcessConformanceError> { + ticket.validate()?; + if ticket.has_controller_launch_binding() { + ticket.validate_controller_launch()?; + } + if ticket.has_assignment_binding() { + ticket.validate_assignment()?; + } + crate::launch::validate_launch_ticket(ticket)?; + if ticket.operation().cancellation() == CancellationBinding::Cancelled { + return Err(ProcessConformanceError::Cancelled); + } + Ok(()) +} + impl

ProcessLaunchEffectPort for EffectPortAdapter

where P: SystemdProcessEffectPort, @@ -52,6 +68,7 @@ where &self, ticket: &LaunchTicket, ) -> Result { + validate_ticket(ticket)?; self.0.start(ticket).await } @@ -59,6 +76,7 @@ where &self, ticket: &LaunchTicket, ) -> Result, ProcessConformanceError> { + validate_ticket(ticket)?; self.0.observe(ticket).await } @@ -66,6 +84,17 @@ where &self, candidate: &AdoptionCandidate, ) -> Result { + if candidate.wait_reap_owner != WaitReapOwner::ServiceManager { + return Err(ProcessConformanceError::WaitOwnerMismatch); + } + candidate.validate(&std::collections::BTreeSet::from([ + IdentityBinding::UnitInvocationId, + IdentityBinding::Cgroup, + IdentityBinding::UnitMainPid, + IdentityBinding::ProcessStartTime, + IdentityBinding::Template, + IdentityBinding::Generation, + ]))?; self.0.open_pidfd(candidate).await } @@ -74,6 +103,113 @@ where identity: &ProcessIdentityDigest, class: StopClass, ) -> Result<(), ProcessConformanceError> { + if identity.is_zero() { + return Err(ProcessConformanceError::IdentityUnverified); + } self.0.stop(identity, class).await } } + +#[cfg(test)] +mod tests { + use std::sync::Mutex; + + use d2b_process_conformance::testing::fixtures; + use d2b_process_conformance::{ + ObservedIdentity, PidfdEvidence, ProcessIdentityDigest, WaitReapOwner, + }; + + use super::*; + + #[derive(Debug, Default)] + struct RecordingPort { + calls: Mutex>, + } + + impl SystemdProcessEffectPort for RecordingPort { + fn start( + &self, + _ticket: &LaunchTicket, + ) -> impl Future> + Send { + self.calls.lock().unwrap().push("start"); + std::future::ready(Ok(LaunchedProcess { + identity: ProcessIdentityDigest::from_bytes([0x11; 32]), + observed: ObservedIdentity::from_verified([ + IdentityBinding::UnitInvocationId, + IdentityBinding::Cgroup, + IdentityBinding::UnitMainPid, + IdentityBinding::ProcessStartTime, + IdentityBinding::Template, + IdentityBinding::Generation, + ]), + pidfd: PidfdEvidence::held(), + wait_reap_owner: WaitReapOwner::ServiceManager, + })) + } + + fn observe( + &self, + _ticket: &LaunchTicket, + ) -> impl Future, ProcessConformanceError>> + Send + { + self.calls.lock().unwrap().push("observe"); + std::future::ready(Ok(None)) + } + + fn open_pidfd( + &self, + _candidate: &AdoptionCandidate, + ) -> impl Future< + Output = Result, + > + Send { + self.calls.lock().unwrap().push("open_pidfd"); + std::future::ready(Ok(PidfdEvidence::held())) + } + + fn stop( + &self, + _identity: &ProcessIdentityDigest, + _class: StopClass, + ) -> impl Future> + Send { + self.calls.lock().unwrap().push("stop"); + std::future::ready(Ok(())) + } + } + + #[test] + fn effect_port_rejects_invalid_tickets_before_the_systemd_boundary() { + let adapter = EffectPortAdapter(RecordingPort::default()); + let ticket = fixtures::ticket_builder().build().unwrap().with_readiness( + d2b_process_conformance::ReadinessExpectation::Condition { timeout_ms: 0 }, + ); + + assert_eq!( + d2b_process_conformance::testing::block_on(adapter.launch(&ticket)).unwrap_err(), + ProcessConformanceError::InvalidTicket + ); + assert!(adapter.0.calls.lock().unwrap().is_empty()); + } + + #[test] + fn effect_port_rejects_unverified_pidfd_candidates_and_zero_stops() { + let adapter = EffectPortAdapter(RecordingPort::default()); + let candidate = AdoptionCandidate { + identity: ProcessIdentityDigest::from_bytes([0x11; 32]), + observed: ObservedIdentity::default(), + wait_reap_owner: WaitReapOwner::ServiceManager, + }; + assert_eq!( + d2b_process_conformance::testing::block_on(adapter.open_pidfd(&candidate)).unwrap_err(), + ProcessConformanceError::IdentityUnverified + ); + assert_eq!( + d2b_process_conformance::testing::block_on(adapter.stop( + &ProcessIdentityDigest::from_bytes([0; 32]), + StopClass::Terminate, + )) + .unwrap_err(), + ProcessConformanceError::IdentityUnverified + ); + assert!(adapter.0.calls.lock().unwrap().is_empty()); + } +} diff --git a/packages/d2b-provider-system-systemd/src/lib.rs b/packages/d2b-provider-system-systemd/src/lib.rs index c0180e55d8..f58bf68010 100644 --- a/packages/d2b-provider-system-systemd/src/lib.rs +++ b/packages/d2b-provider-system-systemd/src/lib.rs @@ -43,13 +43,12 @@ pub use manifest::SystemdManifest; use std::collections::BTreeSet; -use d2b_contracts_resource::v3::{ - execution_policy::{BoundedToken, ExecutionDomain}, -}; +use d2b_contracts_resource::v3::execution_policy::{BoundedToken, ExecutionDomain}; use d2b_process_conformance::{ - AdoptionCondition, AdoptionOutcome, CancellationBinding, IdentityBinding, LaunchTicket, - ProcessConformanceError, ProcessIdentityDigest, ProcessLaunchEffectPort, ProcessPhaseClass, - ProcessProvider, ProcessProviderProfile, ProcessStatusReport, StopClass, WaitReapOwner, + AdoptionCandidate, AdoptionCondition, AdoptionOutcome, CancellationBinding, IdentityBinding, + LaunchTicket, LaunchedProcess, ProcessConformanceError, ProcessIdentityDigest, + ProcessLaunchEffectPort, ProcessPhaseClass, ProcessProvider, ProcessProviderProfile, + ProcessStatusReport, ReadinessExpectation, StopClass, WaitReapOwner, }; /// The Provider name this controller implements. @@ -91,6 +90,13 @@ impl SystemdProcessProvider

{ } fn validate(&self, ticket: &LaunchTicket) -> Result<(), ProcessConformanceError> { + ticket.validate()?; + if ticket.has_controller_launch_binding() { + ticket.validate_controller_launch()?; + } + if ticket.has_assignment_binding() { + ticket.validate_assignment()?; + } if ticket.selected_provider().as_str() != PROVIDER_NAME { return Err(ProcessConformanceError::ProviderMismatch); } @@ -106,6 +112,61 @@ impl SystemdProcessProvider

{ Ok(()) } + async fn cleanup_failed_launch( + &self, + launched: &LaunchedProcess, + error: ProcessConformanceError, + ) -> ProcessConformanceError { + if launched.identity.is_zero() { + return error; + } + match self + .port + .stop(&launched.identity, StopClass::Terminate) + .await + { + Ok(()) => error, + Err(_) => ProcessConformanceError::StopUnavailable, + } + } + + async fn readiness_phase( + &self, + ticket: &LaunchTicket, + identity: ProcessIdentityDigest, + ) -> Result { + match ticket.readiness() { + ReadinessExpectation::None => Ok(ProcessPhaseClass::Running), + ReadinessExpectation::Condition { .. } => { + // The fixed adapter's probe is the readiness observation; + // it does not open or retain another pidfd. + let Some(candidate) = self.port.probe(ticket).await? else { + return Err(ProcessConformanceError::DeadlineExceeded); + }; + if !self.candidate_matches(ticket, &candidate, identity) { + return Err(ProcessConformanceError::AdoptionAmbiguous); + } + Ok(ProcessPhaseClass::Ready) + } + } + } + + fn candidate_matches( + &self, + ticket: &LaunchTicket, + candidate: &AdoptionCandidate, + identity: ProcessIdentityDigest, + ) -> bool { + candidate.identity == identity + && candidate.wait_reap_owner == WaitReapOwner::ServiceManager + && candidate + .validate(self.profile.required_identity_bindings()) + .is_ok() + && ticket + .validate_process_identity(&candidate.identity) + .is_ok() + } + fn report( &self, ticket: &LaunchTicket, @@ -143,12 +204,16 @@ impl ProcessProvider for SystemdProcessProvider

{ return Err(ProcessConformanceError::WaitOwnerMismatch); } launched.validate(self.profile.required_identity_bindings())?; - Ok(self.report( - ticket, - launched.identity, - ProcessPhaseClass::Running, - AdoptionCondition::NotApplicable, - )) + ticket.validate_process_identity(&launched.identity)?; + match self.readiness_phase(ticket, launched.identity).await { + Ok(phase) => Ok(self.report( + ticket, + launched.identity, + phase, + AdoptionCondition::NotApplicable, + )), + Err(error) => Err(self.cleanup_failed_launch(&launched, error).await), + } } async fn adopt( @@ -164,6 +229,9 @@ impl ProcessProvider for SystemdProcessProvider

{ let identity_ok = candidate.wait_reap_owner == WaitReapOwner::ServiceManager && candidate .validate(self.profile.required_identity_bindings()) + .is_ok() + && ticket + .validate_process_identity(&candidate.identity) .is_ok(); if !identity_ok { return Ok(AdoptionOutcome::Quarantined(self.report( @@ -173,11 +241,22 @@ impl ProcessProvider for SystemdProcessProvider

{ AdoptionCondition::Quarantined, ))); } + let phase = match self.readiness_phase(ticket, candidate.identity).await { + Ok(phase) => phase, + Err(_) => { + return Ok(AdoptionOutcome::Quarantined(self.report( + ticket, + candidate.identity, + ProcessPhaseClass::Unknown, + AdoptionCondition::Quarantined, + ))); + } + }; let _pidfd = self.port.open_pidfd(&candidate).await?; Ok(AdoptionOutcome::Adopted(self.report( ticket, candidate.identity, - ProcessPhaseClass::Running, + phase, AdoptionCondition::Adopted, ))) } @@ -187,6 +266,9 @@ impl ProcessProvider for SystemdProcessProvider

{ identity: &ProcessIdentityDigest, class: StopClass, ) -> Result<(), ProcessConformanceError> { + if identity.is_zero() { + return Err(ProcessConformanceError::IdentityUnverified); + } self.port.stop(identity, class).await } } diff --git a/packages/d2b-provider-system-systemd/tests/conformance.rs b/packages/d2b-provider-system-systemd/tests/conformance.rs index 94f26737bf..ec42070a12 100644 --- a/packages/d2b-provider-system-systemd/tests/conformance.rs +++ b/packages/d2b-provider-system-systemd/tests/conformance.rs @@ -1,10 +1,14 @@ //! The shared Process conformance suite run against `system-systemd`, //! plus the systemd-specific identity and adoption obligations. +use d2b_contracts_resource::v3::ResourceGeneration; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; use d2b_process_conformance::suite; use d2b_process_conformance::testing::{PortCall, ScriptedEffectPort, block_on, fixtures}; use d2b_process_conformance::{ - AdoptionOutcome, IdentityBinding, ProcessConformanceError, ProcessProvider, WaitReapOwner, + AdoptionOutcome, ConfigurationDigest, IdentityBinding, ProcessConformanceError, + ProcessIdentityDigest, ProcessPhaseClass, ProcessProvider, ReadinessExpectation, StopClass, + WaitReapOwner, }; use d2b_provider_system_systemd::{PROVIDER_NAME, SystemdProcessProvider}; @@ -38,6 +42,7 @@ fn shared_conformance_holds() { suite::assert_status_is_redacted(&launching(), PROVIDER_NAME); suite::assert_incomplete_launch_identity_fails_closed(provider, PROVIDER_NAME); suite::assert_adoption_verifies_identity_before_opening_a_pidfd(provider, PROVIDER_NAME); + suite::assert_finalizer_requires_verified_stop(WaitReapOwner::ServiceManager); } #[test] @@ -57,6 +62,7 @@ fn systemd_owns_wait_and_reap() { block_on(mismatched.launch(&ticket)).unwrap_err(), ProcessConformanceError::WaitOwnerMismatch ); + assert_eq!(mismatched.port().calls(), vec![PortCall::Launch]); } fn provider_with_wait_owner(owner: WaitReapOwner) -> SystemdProcessProvider { @@ -110,6 +116,164 @@ fn adoption_never_opens_a_pidfd_for_an_ambiguous_scope() { suite::assert_pidfd_open_follows_verification(&calls); } +#[test] +fn malformed_readiness_is_rejected_before_effect_dispatch() { + let provider = provider(ScriptedEffectPort::launching( + required(), + WaitReapOwner::ServiceManager, + )); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_readiness(ReadinessExpectation::Condition { timeout_ms: 0 }); + + assert_eq!( + block_on(provider.launch(&ticket)).unwrap_err(), + ProcessConformanceError::InvalidTicket + ); + assert!(provider.port().calls().is_empty()); +} + +#[test] +fn readiness_is_verified_before_reporting_ready() { + let provider = provider( + ScriptedEffectPort::launching(required(), WaitReapOwner::ServiceManager) + .with_candidate(required(), WaitReapOwner::ServiceManager), + ); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_readiness(ReadinessExpectation::condition(1_000).expect("bounded readiness")); + + let report = block_on(provider.launch(&ticket)).expect("ready launch"); + assert_eq!(report.phase, ProcessPhaseClass::Ready); + assert_eq!( + provider.port().calls(), + vec![PortCall::Launch, PortCall::Observe] + ); +} + +#[test] +fn a_readiness_timeout_stops_the_exact_launched_identity() { + let provider = launching(); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_readiness(ReadinessExpectation::condition(1_000).expect("bounded readiness")); + + assert_eq!( + block_on(provider.launch(&ticket)).unwrap_err(), + ProcessConformanceError::DeadlineExceeded + ); + assert_eq!( + provider.port().calls(), + vec![ + PortCall::Launch, + PortCall::Observe, + PortCall::Stop(StopClass::Terminate) + ] + ); +} + +#[test] +fn an_adoption_identity_seal_mismatch_is_quarantined_before_pidfd_open() { + let provider = provider( + ScriptedEffectPort::launching(required(), WaitReapOwner::ServiceManager) + .with_candidate(required(), WaitReapOwner::ServiceManager), + ); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_expected_identity_digest(ProcessIdentityDigest::from_bytes([0x22; 32])) + .expect("nonzero identity seal"); + + assert!(matches!( + block_on(provider.adopt(&ticket)).expect("adoption result"), + AdoptionOutcome::Quarantined(report) + if report.phase == ProcessPhaseClass::Unknown + )); + assert_eq!(provider.port().calls(), vec![PortCall::Observe]); +} + +#[test] +fn a_launch_identity_seal_mismatch_fails_closed_without_signalling() { + let provider = launching(); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_expected_identity_digest(ProcessIdentityDigest::from_bytes([0x22; 32])) + .expect("nonzero identity seal"); + + assert_eq!( + block_on(provider.launch(&ticket)).unwrap_err(), + ProcessConformanceError::TerminalEvidenceMismatch + ); + assert_eq!(provider.port().calls(), vec![PortCall::Launch]); +} + +#[test] +fn stopping_a_zero_identity_is_rejected_without_an_effect() { + let provider = launching(); + let identity = ProcessIdentityDigest::from_bytes([0; 32]); + + assert_eq!( + block_on(provider.stop(&identity, StopClass::Terminate)).unwrap_err(), + ProcessConformanceError::IdentityUnverified + ); + assert!(provider.port().calls().is_empty()); +} + +#[test] +fn controller_authority_requires_a_committed_revision_before_launch() { + let controller_ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_controller_launch_binding( + ResourceGeneration::new(2).expect("provider generation"), + ReconnectGeneration::new(4).expect("session generation"), + ConfigurationDigest::from_bytes([1; 32]), + ConfigurationDigest::from_bytes([2; 32]), + ) + .expect("controller binding"); + let provider = launching(); + assert_eq!( + block_on(provider.launch(&controller_ticket)).unwrap_err(), + ProcessConformanceError::InvalidTicket + ); + assert!(provider.port().calls().is_empty()); + + let assignment_ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .build() + .expect("conformant ticket") + .with_assignment_binding( + ResourceGeneration::new(2).expect("provider generation"), + ReconnectGeneration::new(4).expect("session generation"), + 9, + ConfigurationDigest::from_bytes([3; 32]), + ) + .expect("assignment binding"); + let provider = launching(); + assert_eq!( + block_on(provider.launch(&assignment_ticket)).unwrap_err(), + ProcessConformanceError::InvalidTicket + ); + assert!(provider.port().calls().is_empty()); +} + #[test] fn a_user_domain_process_is_placed_in_a_verified_user_scope() { let provider = launching(); diff --git a/packages/d2b-provider-system-systemd/tests/controller.rs b/packages/d2b-provider-system-systemd/tests/controller.rs index a4e3bc9678..16ad27d950 100644 --- a/packages/d2b-provider-system-systemd/tests/controller.rs +++ b/packages/d2b-provider-system-systemd/tests/controller.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use d2b_process_conformance::testing::{PortCall, ScriptedEffectPort, block_on, fixtures}; use d2b_process_conformance::{ - AdoptionCandidate, IdentityBinding, LaunchTicket, LaunchedProcess, PidfdEvidence, - ProcessConformanceError, ProcessIdentityDigest, ProcessLaunchEffectPort, StopClass, - WaitReapOwner, + AdoptionCandidate, AdoptionOutcome, IdentityBinding, LaunchTicket, LaunchedProcess, + PidfdEvidence, ProcessConformanceError, ProcessIdentityDigest, ProcessLaunchEffectPort, + StopClass, WaitReapOwner, }; use d2b_provider_system_systemd::controller::{ SystemdProcessController, SystemdReconcileAction, SystemdReconcileResult, @@ -56,6 +56,51 @@ impl ProcessLaunchEffectPort for PendingEffectPort { } } +#[derive(Debug)] +struct LaunchPendingControlPort { + launch_entered: Arc, +} + +impl LaunchPendingControlPort { + fn new() -> Self { + Self { + launch_entered: Arc::new(tokio::sync::Notify::new()), + } + } +} + +impl ProcessLaunchEffectPort for LaunchPendingControlPort { + async fn launch( + &self, + _ticket: &LaunchTicket, + ) -> Result { + self.launch_entered.notify_one(); + std::future::pending().await + } + + async fn observe( + &self, + _ticket: &LaunchTicket, + ) -> Result, ProcessConformanceError> { + Ok(None) + } + + async fn open_pidfd( + &self, + _candidate: &AdoptionCandidate, + ) -> Result { + Ok(PidfdEvidence::held()) + } + + async fn stop( + &self, + _identity: &ProcessIdentityDigest, + _class: StopClass, + ) -> Result<(), ProcessConformanceError> { + Ok(()) + } +} + fn required() -> Vec { vec![ IdentityBinding::UnitInvocationId, @@ -149,6 +194,51 @@ fn controller_rejects_launches_when_the_bounded_permit_is_saturated() { }); } +#[test] +fn launch_slots_do_not_block_adoption_or_stop() { + let ticket = fixtures::ticket_builder() + .expected_identity(required()) + .build() + .expect("valid ticket"); + let identity = ProcessIdentityDigest::from_bytes([0x22; 32]); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .expect("runtime"); + runtime.block_on(async { + let port = LaunchPendingControlPort::new(); + let entered = Arc::clone(&port.launch_entered); + let controller = Arc::new(SystemdProcessController::new( + SystemdProcessProvider::new(port), + SystemdProviderConfig::new(1, 1, 1, 1).expect("bounded config"), + )); + let first_controller = Arc::clone(&controller); + let first_ticket = ticket.clone(); + let first = tokio::spawn(async move { + first_controller + .reconcile(SystemdReconcileAction::Start(&first_ticket)) + .await + }); + entered.notified().await; + + assert!(matches!( + controller + .reconcile(SystemdReconcileAction::Adopt(&ticket)) + .await + .expect("adoption remains admitted"), + SystemdReconcileResult::Adoption(AdoptionOutcome::Absent) + )); + assert!(matches!( + controller + .reconcile(SystemdReconcileAction::Stop(&identity, StopClass::Drain)) + .await + .expect("stop remains admitted"), + SystemdReconcileResult::Stopped + )); + first.abort(); + }); +} + #[test] fn controller_times_out_pending_start_adopt_and_stop_operations() { let ticket = fixtures::ticket_builder() diff --git a/packages/d2b-provider-system-systemd/tests/execution_parents.rs b/packages/d2b-provider-system-systemd/tests/execution_parents.rs index 6a04ea7c64..8f0fb9a9d2 100644 --- a/packages/d2b-provider-system-systemd/tests/execution_parents.rs +++ b/packages/d2b-provider-system-systemd/tests/execution_parents.rs @@ -9,10 +9,13 @@ use d2b_contracts_resource::v3::ResourceRef; use d2b_contracts_resource::v3::execution_policy::ExecutionDomain; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; +use d2b_contracts_resource::v3::{ResourceGeneration, ZoneRevision}; use d2b_process_conformance::testing::{PortCall, ScriptedEffectPort, block_on, fixtures}; use d2b_process_conformance::{ - AdoptionCondition, AdoptionOutcome, IdentityBinding, ProcessConformanceError, ProcessProvider, - WaitReapOwner, + AdoptionCondition, AdoptionOutcome, ConfigurationDigest, IdentityBinding, + ProcessConformanceError, ProcessIdentityDigest, ProcessPhaseClass, ProcessProvider, + ReadinessExpectation, WaitReapOwner, }; use d2b_provider_system_systemd::{PROVIDER_NAME, SystemdProcessProvider}; @@ -161,3 +164,67 @@ fn a_candidate_whose_wait_owner_disagrees_is_quarantined() { )); assert!(!provider.port().calls().contains(&PortCall::OpenPidfd)); } + +#[test] +fn readiness_and_identity_seals_are_parent_neutral() { + for execution_ref in [host_ref(), guest_ref()] { + let provider = provider( + ScriptedEffectPort::launching(required(), WaitReapOwner::ServiceManager) + .with_candidate(required(), WaitReapOwner::ServiceManager), + ); + let ticket = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .expected_identity(required()) + .execution_ref(execution_ref.clone()) + .build() + .expect("conformant ticket") + .with_readiness(ReadinessExpectation::condition(1_000).expect("bounded readiness")) + .with_expected_identity_digest(ProcessIdentityDigest::from_bytes([0x11; 32])) + .expect("identity seal"); + + let report = block_on(provider.launch(&ticket)).expect("ready launch"); + assert_eq!(report.execution_ref, execution_ref); + assert_eq!(report.phase, ProcessPhaseClass::Ready); + assert_eq!( + provider.port().calls(), + vec![PortCall::Launch, PortCall::Observe] + ); + } +} + +#[test] +fn controller_launch_and_assignment_authority_remain_disjoint_on_each_parent() { + for execution_ref in [host_ref(), guest_ref()] { + let launch = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .execution_ref(execution_ref.clone()) + .build() + .expect("conformant ticket") + .with_resource_revision(ZoneRevision::new(1)) + .expect("revision binding") + .with_controller_launch_binding( + ResourceGeneration::new(2).expect("provider generation"), + ReconnectGeneration::new(4).expect("session generation"), + ConfigurationDigest::from_bytes([1; 32]), + ConfigurationDigest::from_bytes([2; 32]), + ) + .expect("controller launch binding"); + d2b_process_conformance::suite::assert_controller_launch_has_no_resource_client(&launch); + + let assignment = fixtures::ticket_builder() + .selected_provider(PROVIDER_NAME) + .execution_ref(execution_ref) + .build() + .expect("conformant ticket") + .with_resource_revision(ZoneRevision::new(1)) + .expect("revision binding") + .with_assignment_binding( + ResourceGeneration::new(2).expect("provider generation"), + ReconnectGeneration::new(4).expect("session generation"), + 9, + ConfigurationDigest::from_bytes([3; 32]), + ) + .expect("assignment binding"); + d2b_process_conformance::suite::assert_assignment_is_session_fenced(&assignment, 4, 9); + } +} diff --git a/packages/d2b-provider-toolkit/src/agent.rs b/packages/d2b-provider-toolkit/src/agent.rs index 5182ba113e..41f9895fe9 100644 --- a/packages/d2b-provider-toolkit/src/agent.rs +++ b/packages/d2b-provider-toolkit/src/agent.rs @@ -9,19 +9,24 @@ use std::sync::Mutex; use d2b_contracts_zone_session::v3::{ component_session::RequestId, - zone_routing::ZonePath, + zone_routing::{ZoneLabelId, ZonePath}, }; use d2b_contracts_resource::v3::{ CanonicalJsonObject, ResourceRef, execution_policy::BoundedToken, }; -use d2b_session::{Cancellation, ComponentSessionDriver}; +use d2b_session::{ + AuthenticatedSessionRouteBinding, + Cancellation, + ComponentSessionDriver, +}; use crate::{ DispatchLimiter, ProviderAgentAuditEvent, ProviderAgentAuditLog, ProviderAgentAuditOutcome, ProviderToolkitError, }; +use crate::runtime::same_controller_identity; /// Validate the strict attachment-index sequence carried by a Provider /// adapter. Descriptors are numbered from zero and may not repeat, reorder, @@ -123,6 +128,7 @@ pub struct ProviderAgentAdapter { service: S, dispatch: DispatchLimiter, audit: Mutex, + authenticated_route: Mutex>, } impl ProviderAgentAdapter { @@ -132,6 +138,7 @@ impl ProviderAgentAdapter { service, dispatch: DispatchLimiter::new(), audit: Mutex::new(ProviderAgentAuditLog::new()), + authenticated_route: Mutex::new(None), } } @@ -152,6 +159,51 @@ impl ProviderAgentAdapter { .map(|audit| audit.len()) .unwrap_or_default() } + + /// Bind the adapter to one authenticated controller route. + /// + /// A route is admission evidence only. It does not grant a ResourceClient + /// or effect capability, and a second route cannot replace the first one + /// while this adapter is alive. + pub fn bind_authenticated_route( + &self, + route: AuthenticatedSessionRouteBinding, + ) -> Result<(), ProviderToolkitError> { + if route.provider_ref().is_none() + || route.provider_generation().is_none() + || route.controller_generation().is_none() + || route.reconnect_generation().get() == 0 + { + return Err(ProviderToolkitError::SessionUnauthenticated); + } + let mut bound = self + .authenticated_route + .lock() + .map_err(|_| ProviderToolkitError::SessionUnauthenticated)?; + match bound.as_ref() { + None => { + *bound = Some(route); + Ok(()) + } + Some(existing) if existing == &route => Ok(()), + Some(existing) + if same_controller_identity(existing, &route) + && route.reconnect_generation() > existing.reconnect_generation() => + { + *bound = Some(route); + Ok(()) + } + Some(_) => Err(ProviderToolkitError::SessionUnauthenticated), + } + } + + /// Whether an authenticated controller route has been bound. + pub fn has_authenticated_route(&self) -> bool { + self.authenticated_route + .lock() + .ok() + .is_some_and(|route| route.is_some()) + } } impl ProviderAgentAdapter @@ -165,6 +217,45 @@ where provider_ref: ResourceRef, method: BoundedToken, payload: CanonicalJsonObject, + ) -> Result { + let bound = self + .authenticated_route + .lock() + .map_err(|_| ProviderToolkitError::SessionUnauthenticated)?; + if let Some(route) = bound.as_ref() { + Self::validate_bound_request(route, &zone, &provider_ref)?; + } + self.dispatch_inner(zone, provider_ref, method, payload) + } + + fn dispatch_for_route( + &self, + route: &AuthenticatedSessionRouteBinding, + zone: ZonePath, + provider_ref: ResourceRef, + method: BoundedToken, + payload: CanonicalJsonObject, + ) -> Result { + let bound = self + .authenticated_route + .lock() + .map_err(|_| ProviderToolkitError::SessionUnauthenticated)?; + let current_route = bound + .as_ref() + .ok_or(ProviderToolkitError::SessionUnauthenticated)?; + if current_route != route { + return Err(ProviderToolkitError::SessionUnauthenticated); + } + Self::validate_bound_request(route, &zone, &provider_ref)?; + self.dispatch_inner(zone, provider_ref, method, payload) + } + + fn dispatch_inner( + &self, + zone: ZonePath, + provider_ref: ResourceRef, + method: BoundedToken, + payload: CanonicalJsonObject, ) -> Result { let _permit = self.dispatch.acquire()?; let result = self.service.dispatch(&method, &payload); @@ -187,6 +278,9 @@ where /// Serve decoded Provider RPC frames from one authenticated /// ComponentSession until cancellation or transport close. /// + /// Callers must bind the authenticated route before entering this loop. + /// Every decoded target is checked against that binding. + /// /// Session authentication, generation binding, attachment policy, and /// stream fairness remain owned by `d2b-session`; this loop only bridges /// the generated frame codec to the bounded Provider service adapter. @@ -200,10 +294,25 @@ where D: ComponentSessionDriver, C: ProviderFrameCodec, { + let route = self + .authenticated_route + .lock() + .map_err(|_| ProviderToolkitError::SessionUnauthenticated)? + .clone() + .ok_or(ProviderToolkitError::SessionUnauthenticated)?; loop { if cancellation.is_cancelled() { return Ok(()); } + let current_route = self + .authenticated_route + .lock() + .map_err(|_| ProviderToolkitError::SessionUnauthenticated)? + .clone() + .ok_or(ProviderToolkitError::SessionUnauthenticated)?; + if current_route != route { + return Err(ProviderToolkitError::SessionUnauthenticated); + } let frame = driver .receive_ttrpc() .await @@ -211,7 +320,8 @@ where let request = codec .decode_request(&frame) .map_err(|_| ProviderToolkitError::WireInvalid)?; - let response = self.dispatch( + let response = self.dispatch_for_route( + &route, request.zone().clone(), request.provider_ref().clone(), request.method().clone(), @@ -226,6 +336,25 @@ where .map_err(|_| ProviderToolkitError::SessionClosed)?; } } + + fn validate_bound_request( + route: &AuthenticatedSessionRouteBinding, + zone: &ZonePath, + provider_ref: &ResourceRef, + ) -> Result<(), ProviderToolkitError> { + let expected_zone = ZonePath::new(vec![ + ZoneLabelId::parse(route.zone().as_str()) + .map_err(|_| ProviderToolkitError::SessionUnauthenticated)?, + ]) + .map_err(|_| ProviderToolkitError::SessionUnauthenticated)?; + let expected_provider = route + .provider_ref() + .ok_or(ProviderToolkitError::SessionUnauthenticated)?; + if zone != &expected_zone || provider_ref != expected_provider { + return Err(ProviderToolkitError::SessionUnauthenticated); + } + Ok(()) + } } /// Generated-service registration facade over a Provider agent adapter. @@ -245,6 +374,14 @@ impl GeneratedProviderServiceServer { pub const fn adapter(&self) -> &ProviderAgentAdapter { &self.adapter } + + /// Bind the generated server to one authenticated controller route. + pub fn bind_authenticated_route( + &self, + route: AuthenticatedSessionRouteBinding, + ) -> Result<(), ProviderToolkitError> { + self.adapter.bind_authenticated_route(route) + } } impl GeneratedProviderServiceServer @@ -324,7 +461,6 @@ mod tests { ResourceName, ResourceTypeName, }; - struct Echo; impl ProviderService for Echo { @@ -386,4 +522,58 @@ mod tests { Err(ProviderToolkitError::NonMonotoneAttachmentIndexes) ); } + + #[test] + fn component_session_serving_requires_controller_route_admission() { + use crate::Fixture; + use d2b_provider::ProviderClass; + use d2b_session::AuthenticatedSessionRouteBinding; + + let fixture = Fixture::new(ProviderClass::Runtime, 0).expect("fixture"); + let adapter = ProviderAgentAdapter::new(Echo); + let missing_generation = AuthenticatedSessionRouteBinding::for_test( + Some(fixture.descriptor.provider_ref().clone()), + "d2b.provider.v3", + 1, + Some(1), + None, + ); + assert_eq!( + adapter.bind_authenticated_route(missing_generation), + Err(ProviderToolkitError::SessionUnauthenticated) + ); + assert!(!adapter.has_authenticated_route()); + + let route = AuthenticatedSessionRouteBinding::for_test( + Some(fixture.descriptor.provider_ref().clone()), + "d2b.provider.v3", + 1, + Some(1), + Some(1), + ); + assert!(adapter.bind_authenticated_route(route.clone()).is_ok()); + assert!(adapter.has_authenticated_route()); + assert!(adapter.bind_authenticated_route(route).is_ok()); + let reconnect = AuthenticatedSessionRouteBinding::for_test( + Some(fixture.descriptor.provider_ref().clone()), + "d2b.provider.v3", + 2, + Some(1), + Some(1), + ); + assert!(adapter.bind_authenticated_route(reconnect).is_ok()); + assert_eq!( + adapter.dispatch( + d2b_contracts_zone_session::v3::zone_routing::ZonePath::new(vec![ + d2b_contracts_zone_session::v3::zone_routing::ZoneLabelId::parse("dev") + .unwrap(), + ]) + .unwrap(), + d2b_contracts_resource::v3::ResourceRef::parse("Provider/other").unwrap(), + BoundedToken::parse("inspect").unwrap(), + CanonicalJsonObject::empty(), + ), + Err(ProviderToolkitError::SessionUnauthenticated) + ); + } } diff --git a/packages/d2b-provider-toolkit/src/runtime.rs b/packages/d2b-provider-toolkit/src/runtime.rs index 9f6e19aba6..f406784e4f 100644 --- a/packages/d2b-provider-toolkit/src/runtime.rs +++ b/packages/d2b-provider-toolkit/src/runtime.rs @@ -14,7 +14,12 @@ use std::{ time::Duration, }; -use d2b_contracts_resource::v3::ResourceRef; +use d2b_contracts_provider::v3::{ + ComponentDescriptor, ComponentExecution, ComponentType, ControllerTargetKind, EffectPortClass, +}; +use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceRef, +}; use d2b_session::{AuthenticatedComponentSession, AuthenticatedSessionRouteBinding}; const STARTING: u8 = 0; @@ -59,6 +64,9 @@ pub enum ProviderRuntimeError { SessionUnauthenticated, /// The generated Provider service loop failed after readiness. SessionLoopFailed, + /// A signed controller descriptor is not launchable on the requested + /// target. + ControllerDescriptorInvalid, } impl fmt::Display for ProviderRuntimeError { @@ -69,6 +77,7 @@ impl fmt::Display for ProviderRuntimeError { Self::ReadinessIo => "provider-runtime-readiness-io", Self::SessionUnauthenticated => "provider-runtime-session-unauthenticated", Self::SessionLoopFailed => "provider-runtime-session-loop-failed", + Self::ControllerDescriptorInvalid => "provider-runtime-controller-descriptor-invalid", }) } } @@ -77,6 +86,7 @@ impl std::error::Error for ProviderRuntimeError {} struct RuntimeState { admitted: usize, + ready_route: Option, } /// A non-cloneable process lifecycle owner. @@ -87,6 +97,11 @@ struct RuntimeState { pub struct ProviderEntrypoint { name: &'static str, provider_ref: Option, + execution_ref: Option, + process_ref: Option, + target_kind: Option, + provider_generation: Option, + controller_generation: Option, service: Option<&'static str>, lifecycle: AtomicU8, state: Arc<(Mutex, Condvar)>, @@ -102,6 +117,34 @@ pub struct ProviderSessionAdmission { route: AuthenticatedSessionRouteBinding, } +impl ProviderSessionAdmission { + /// Borrow the authenticated routing metadata. + pub const fn route(&self) -> &AuthenticatedSessionRouteBinding { + &self.route + } + + /// Return the authenticated Provider generation. + pub const fn provider_generation( + &self, + ) -> Option { + self.route.provider_generation() + } + + /// Return the authenticated controller generation. + pub const fn controller_generation( + &self, + ) -> Option { + self.route.controller_generation() + } + + /// Return the authenticated ComponentSession generation. + pub const fn reconnect_generation( + &self, + ) -> d2b_contracts_resource::v3::identity::ReconnectGeneration { + self.route.reconnect_generation() + } +} + impl fmt::Debug for ProviderSessionAdmission { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("ProviderSessionAdmission(REDACTED)") @@ -132,12 +175,65 @@ impl ProviderEntrypoint { Ok(Self { name, provider_ref: None, + execution_ref: None, + process_ref: None, + target_kind: None, + provider_generation: None, + controller_generation: None, service: None, lifecycle: AtomicU8::new(STARTING), - state: Arc::new((Mutex::new(RuntimeState { admitted: 0 }), Condvar::new())), + state: Arc::new(( + Mutex::new(RuntimeState { + admitted: 0, + ready_route: None, + }), + Condvar::new(), + )), }) } + /// Construct a target-local controller lifecycle from a signed component + /// descriptor. + /// + /// The descriptor is an admission input only. This method never resolves + /// an executable or starts a process; the fixed Process Provider and its + /// effect adapter own that boundary. + pub fn from_signed_controller( + name: &'static str, + provider_ref: ResourceRef, + service: &'static str, + descriptor: &ComponentDescriptor, + target: ControllerTargetKind, + ) -> Result { + if descriptor.component_type() != ComponentType::Controller + || !matches!( + descriptor.execution(), + ComponentExecution::Launchable { .. } + ) + || !matches!( + target, + ControllerTargetKind::Host | ControllerTargetKind::Guest + ) + || !descriptor.supported_target_kinds().contains(&target) + { + return Err(ProviderRuntimeError::ControllerDescriptorInvalid); + } + let Some(capability) = descriptor.target_capability(target) else { + return Err(ProviderRuntimeError::ControllerDescriptorInvalid); + }; + if !capability + .required_effect_classes() + .contains(&EffectPortClass::Process) + || is_zero_digest(descriptor.config_digest().as_str()) + || is_zero_digest(capability.artifact_digest().as_str()) + { + return Err(ProviderRuntimeError::ControllerDescriptorInvalid); + } + let mut runtime = Self::with_provider(name, provider_ref, service)?; + runtime.target_kind = Some(target); + Ok(runtime) + } + /// Construct a lifecycle owner bound to one compiled Provider identity /// and service package. pub fn with_provider( @@ -154,6 +250,55 @@ impl ProviderEntrypoint { Ok(runtime) } + /// Bind this lifecycle owner to one exact Host or Guest execution target. + pub fn with_execution_target( + mut self, + execution_ref: ResourceRef, + ) -> Result { + let target_kind = match execution_ref.resource_type().as_str() { + "Host" => ControllerTargetKind::Host, + "Guest" => ControllerTargetKind::Guest, + _ => return Err(ProviderRuntimeError::InvalidName), + }; + if self.execution_ref.is_some() + || self + .target_kind + .is_some_and(|expected| expected != target_kind) + { + return Err(ProviderRuntimeError::InvalidName); + } + self.execution_ref = Some(execution_ref); + self.target_kind = Some(target_kind); + Ok(self) + } + + /// Bind this lifecycle owner to the exact controller Process identity. + pub fn with_controller_process( + mut self, + process_ref: ResourceRef, + ) -> Result { + if process_ref.resource_type().as_str() != "Process" || self.process_ref.is_some() { + return Err(ProviderRuntimeError::InvalidName); + } + self.process_ref = Some(process_ref); + Ok(self) + } + + /// Bind this lifecycle owner to one exact Provider and controller + /// generation. + pub fn with_generations( + mut self, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + ) -> Result { + if self.provider_generation.is_some() || self.controller_generation.is_some() { + return Err(ProviderRuntimeError::InvalidName); + } + self.provider_generation = Some(provider_generation); + self.controller_generation = Some(controller_generation); + Ok(self) + } + /// Return the current process lifecycle. pub fn lifecycle(&self) -> ProviderLifecycle { ProviderLifecycle::from_u8(self.lifecycle.load(Ordering::Acquire)) @@ -193,6 +338,9 @@ impl ProviderEntrypoint { &self, session: &AuthenticatedComponentSession, ) -> Result { + if self.lifecycle() != ProviderLifecycle::Starting { + return Err(ProviderRuntimeError::NotAccepting); + } let Some(expected_provider) = &self.provider_ref else { return Err(ProviderRuntimeError::SessionUnauthenticated); }; @@ -202,8 +350,40 @@ impl ProviderEntrypoint { let route = session.route_binding(); if route.provider_ref() != Some(expected_provider) || route.service().as_str() != expected_service - || route.provider_generation().is_none() - || route.reconnect_generation().get() == 0 + { + return Err(ProviderRuntimeError::SessionUnauthenticated); + } + self.validate_authenticated_route(&route)?; + Ok(ProviderSessionAdmission { route }) + } + + /// Validate redacted route evidence before assignment or readiness. + pub fn validate_authenticated_route( + &self, + route: &AuthenticatedSessionRouteBinding, + ) -> Result<(), ProviderRuntimeError> { + if !self.route_matches_expected(route) { + return Err(ProviderRuntimeError::SessionUnauthenticated); + } + Ok(()) + } + + /// Admit a reconnecting controller session without widening its + /// Provider, subject, target, or controller-generation identity. + pub fn admit_authenticated_reconnect( + &self, + session: &AuthenticatedComponentSession, + ) -> Result { + if self.lifecycle() != ProviderLifecycle::Ready { + return Err(ProviderRuntimeError::NotAccepting); + } + let route = session.route_binding(); + let previous = self + .ready_route() + .ok_or(ProviderRuntimeError::SessionUnauthenticated)?; + if !self.route_matches_expected(&route) + || !same_controller_identity(&previous, &route) + || route.reconnect_generation() <= previous.reconnect_generation() { return Err(ProviderRuntimeError::SessionUnauthenticated); } @@ -220,9 +400,79 @@ impl ProviderEntrypoint { ) -> Result<(), ProviderRuntimeError> { let live_route = live_session.route_binding(); self.validate_authenticated_ready(registration, &session, &live_route)?; + let route = session.route.clone(); drop(session); let mut stdout = io::stdout().lock(); - self.publish_ready_to(&mut stdout) + self.publish_ready_to(&mut stdout)?; + let (lock, _) = &*self.state; + let mut state = lock + .lock() + .map_err(|_| ProviderRuntimeError::NotAccepting)?; + state.ready_route = Some(route); + Ok(()) + } + + /// Return whether this controller completed authenticated readiness. + pub fn is_controller_ready(&self) -> bool { + self.lifecycle() == ProviderLifecycle::Ready + && self + .state + .0 + .lock() + .ok() + .is_some_and(|state| state.ready_route.is_some()) + } + + /// Check that assignment authority is bound to the exact ready session. + pub fn is_ready_for_route(&self, route: &AuthenticatedSessionRouteBinding) -> bool { + if self.lifecycle() != ProviderLifecycle::Ready { + return false; + } + self.state + .0 + .lock() + .ok() + .and_then(|state| state.ready_route.clone()) + .is_some_and(|ready| ready == *route) + } + + /// Return redacted routing metadata for the current ready session. + pub fn ready_route(&self) -> Option { + if self.lifecycle() != ProviderLifecycle::Ready { + return None; + } + self.state + .0 + .lock() + .ok() + .and_then(|state| state.ready_route.clone()) + } + + /// Replace the retained route after Core has revoked the prior + /// generation and admitted an authenticated reconnect. + pub fn rebind_authenticated_route( + &self, + admission: ProviderSessionAdmission, + ) -> Result<(), ProviderRuntimeError> { + if self.lifecycle() != ProviderLifecycle::Ready + || !self.route_matches_expected(&admission.route) + { + return Err(ProviderRuntimeError::SessionUnauthenticated); + } + let (lock, _) = &*self.state; + let mut state = lock + .lock() + .map_err(|_| ProviderRuntimeError::NotAccepting)?; + let Some(previous) = state.ready_route.as_ref() else { + return Err(ProviderRuntimeError::SessionUnauthenticated); + }; + if !same_controller_identity(previous, &admission.route) + || admission.route.reconnect_generation() <= previous.reconnect_generation() + { + return Err(ProviderRuntimeError::SessionUnauthenticated); + } + state.ready_route = Some(admission.route); + Ok(()) } fn publish_ready_to(&self, writer: &mut W) -> Result<(), ProviderRuntimeError> { @@ -251,7 +501,10 @@ impl ProviderEntrypoint { .map_err(|_| ProviderRuntimeError::NotAccepting)?; if !Arc::ptr_eq(®istration.state, &self.state) || state.admitted == 0 + || self.lifecycle() != ProviderLifecycle::Starting + || !self.route_matches_expected(live_route) || session.route.reconnect_generation().get() == 0 + || session.route.controller_generation().is_none() || session.route != *live_route { return Err(ProviderRuntimeError::SessionUnauthenticated); @@ -259,6 +512,48 @@ impl ProviderEntrypoint { Ok(()) } + fn route_matches_expected(&self, route: &AuthenticatedSessionRouteBinding) -> bool { + self.provider_ref + .as_ref() + .is_some_and(|expected| route.provider_ref() == Some(expected)) + && self + .service + .is_some_and(|expected| route.service().as_str() == expected) + && route.provider_generation().is_some() + && route.controller_generation().is_some() + && route.reconnect_generation().get() != 0 + && self + .provider_generation + .is_none_or(|expected| route.provider_generation() == Some(expected)) + && self + .controller_generation + .is_none_or(|expected| route.controller_generation() == Some(expected)) + && self + .execution_ref + .as_ref() + .is_none_or(|expected| route.context().execution_ref() == Some(expected)) + && self + .target_kind + .is_none_or(|expected| { + route + .context() + .execution_ref() + .is_some_and(|reference| match expected { + ControllerTargetKind::Host => { + reference.resource_type().as_str() == "Host" + } + ControllerTargetKind::Guest => { + reference.resource_type().as_str() == "Guest" + } + ControllerTargetKind::Zone => false, + }) + }) + && self + .process_ref + .as_ref() + .is_none_or(|expected| route.context().process_ref() == Some(expected)) + } + fn transition_ready(&self) -> Result<(), ProviderRuntimeError> { if self .lifecycle @@ -291,11 +586,39 @@ impl ProviderEntrypoint { let drained = state.admitted == 0 && !wait.timed_out(); if drained { self.lifecycle.store(STOPPED, Ordering::Release); + state.ready_route = None; } drained } } +pub(crate) fn same_controller_identity( + left: &AuthenticatedSessionRouteBinding, + right: &AuthenticatedSessionRouteBinding, +) -> bool { + left.zone() == right.zone() + && left.subject_ref() == right.subject_ref() + && left.subject_uid() == right.subject_uid() + && left.evidence_class() == right.evidence_class() + && left.locality() == right.locality() + && left.service() == right.service() + && left.schema() == right.schema() + && left.provider_ref() == right.provider_ref() + && left.provider_generation() == right.provider_generation() + && left.controller_generation() == right.controller_generation() + && left.context().zone_ref() == right.context().zone_ref() + && left.context().session_purpose() == right.context().session_purpose() + && left.context().transport_binding() == right.context().transport_binding() + && left.context().execution_ref() == right.context().execution_ref() + && left.context().process_ref() == right.context().process_ref() +} + +fn is_zero_digest(value: &str) -> bool { + value + .strip_prefix("sha256:") + .is_some_and(|hex| !hex.is_empty() && hex.bytes().all(|byte| byte == b'0')) +} + /// One local registration held until its service is fully drained. pub struct ProviderAdmission { state: Arc<(Mutex, Condvar)>, @@ -322,7 +645,51 @@ impl Drop for ProviderAdmission { #[cfg(test)] mod tests { use super::*; + use d2b_contracts_provider::v3::{ + ArtifactDigest, BinaryRef, ComponentTargetCapability, EffectPortClass, + }; use d2b_contracts_resource::v3::ResourceRef; + use d2b_contracts_resource::v3::execution_policy::{BoundedToken, ExecutionDomain}; + use d2b_contracts_resource::v3::identity::ResourceTypeName; + + fn signed_controller_descriptor() -> ComponentDescriptor { + let digest = ArtifactDigest::parse(format!("sha256:{}", "a".repeat(64))).unwrap(); + ComponentDescriptor::new( + BoundedToken::parse("controller").unwrap(), + ComponentType::Controller, + [ResourceTypeName::parse("Volume").unwrap()], + [BoundedToken::parse("reconcile").unwrap()], + [ExecutionDomain::System], + 1, + digest.clone(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("controller").unwrap(), + }) + .with_controller_placement( + d2b_contracts_provider::v3::ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest.clone(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest, + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap() + } #[test] fn readiness_is_not_published_before_registration() { @@ -331,6 +698,7 @@ mod tests { let admission = runtime.admit().unwrap(); assert!(runtime.publish_ready().is_ok()); assert_eq!(runtime.lifecycle(), ProviderLifecycle::Ready); + assert!(!runtime.is_controller_ready()); drop(admission); assert!(runtime.drain(Duration::from_millis(10))); assert_eq!(runtime.lifecycle(), ProviderLifecycle::Stopped); @@ -417,4 +785,205 @@ mod tests { Err(ProviderRuntimeError::SessionUnauthenticated) ); } + + #[test] + fn controller_readiness_requires_a_controller_generation() { + let runtime = ProviderEntrypoint::with_provider( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + ) + .unwrap(); + let registration = runtime.admit().unwrap(); + let route = AuthenticatedSessionRouteBinding::for_test( + Some(ResourceRef::parse("Provider/test").unwrap()), + "d2b.provider.v3", + 1, + Some(1), + None, + ); + let admission = ProviderSessionAdmission { + route: route.clone(), + }; + assert_eq!( + runtime.validate_authenticated_route(&route), + Err(ProviderRuntimeError::SessionUnauthenticated) + ); + assert_eq!( + runtime.validate_authenticated_ready(®istration, &admission, &route), + Err(ProviderRuntimeError::SessionUnauthenticated) + ); + } + + #[test] + fn a_foreign_session_admission_cannot_publish_this_entrypoint_ready() { + let runtime = ProviderEntrypoint::with_provider( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + ) + .unwrap(); + let foreign = ProviderEntrypoint::with_provider( + "Provider/foreign", + ResourceRef::parse("Provider/foreign").unwrap(), + "d2b.provider.v3", + ) + .unwrap(); + let registration = runtime.admit().unwrap(); + let foreign_registration = foreign.admit().unwrap(); + let foreign_route = AuthenticatedSessionRouteBinding::for_test( + Some(ResourceRef::parse("Provider/foreign").unwrap()), + "d2b.provider.v3", + 1, + Some(1), + Some(1), + ); + let foreign_admission = ProviderSessionAdmission { + route: foreign_route.clone(), + }; + assert_eq!( + runtime + .validate_authenticated_ready(®istration, &foreign_admission, &foreign_route,), + Err(ProviderRuntimeError::SessionUnauthenticated) + ); + drop(foreign_registration); + } + + #[test] + fn reconnect_rebind_requires_the_same_controller_identity_and_a_new_generation() { + let runtime = ProviderEntrypoint::with_provider( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + ) + .unwrap(); + let first = AuthenticatedSessionRouteBinding::for_test( + Some(ResourceRef::parse("Provider/test").unwrap()), + "d2b.provider.v3", + 1, + Some(1), + Some(1), + ); + runtime.transition_ready().unwrap(); + runtime.state.0.lock().unwrap().ready_route = Some(first.clone()); + + let next = AuthenticatedSessionRouteBinding::for_test( + Some(ResourceRef::parse("Provider/test").unwrap()), + "d2b.provider.v3", + 2, + Some(1), + Some(1), + ); + runtime + .rebind_authenticated_route(ProviderSessionAdmission { + route: next.clone(), + }) + .expect("new reconnect generation"); + assert!(runtime.is_ready_for_route(&next)); + assert_eq!( + runtime.rebind_authenticated_route(ProviderSessionAdmission { route: first }), + Err(ProviderRuntimeError::SessionUnauthenticated) + ); + } + + #[test] + fn signed_controller_construction_rejects_non_launchable_or_unsupported_roles() { + let descriptor = signed_controller_descriptor(); + let runtime = ProviderEntrypoint::from_signed_controller( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + &descriptor, + ControllerTargetKind::Guest, + ) + .expect("signed Guest controller"); + assert_eq!(runtime.lifecycle(), ProviderLifecycle::Starting); + + let targeted = ProviderEntrypoint::with_provider( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + ) + .unwrap() + .with_execution_target(ResourceRef::parse("Guest/dev-vm").unwrap()) + .unwrap(); + let host_route = AuthenticatedSessionRouteBinding::for_test( + Some(ResourceRef::parse("Provider/test").unwrap()), + "d2b.provider.v3", + 1, + Some(1), + Some(1), + ); + assert_eq!( + runtime.validate_authenticated_route(&host_route), + Err(ProviderRuntimeError::SessionUnauthenticated) + ); + assert_eq!( + targeted.validate_authenticated_route(&host_route), + Err(ProviderRuntimeError::SessionUnauthenticated) + ); + let generation_bound = ProviderEntrypoint::with_provider( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + ) + .unwrap() + .with_generations( + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(2).unwrap(), + ) + .unwrap(); + assert_eq!( + generation_bound.validate_authenticated_route(&host_route), + Err(ProviderRuntimeError::SessionUnauthenticated) + ); + let process_bound = ProviderEntrypoint::with_provider( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + ) + .unwrap() + .with_controller_process(ResourceRef::parse("Process/controller").unwrap()) + .unwrap(); + assert_eq!( + process_bound.validate_authenticated_route(&host_route), + Err(ProviderRuntimeError::SessionUnauthenticated) + ); + + let invalid_target = ProviderEntrypoint::from_signed_controller( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + &descriptor, + ControllerTargetKind::Zone, + ); + assert!(matches!( + invalid_target, + Err(ProviderRuntimeError::ControllerDescriptorInvalid) + )); + + let in_process = ComponentDescriptor::new( + BoundedToken::parse("controller").unwrap(), + ComponentType::Controller, + [ResourceTypeName::parse("Volume").unwrap()], + [BoundedToken::parse("reconcile").unwrap()], + [ExecutionDomain::System], + 1, + ArtifactDigest::parse(format!("sha256:{}", "b".repeat(64))).unwrap(), + [], + false, + ) + .unwrap(); + assert!(matches!( + ProviderEntrypoint::from_signed_controller( + "Provider/test", + ResourceRef::parse("Provider/test").unwrap(), + "d2b.provider.v3", + &in_process, + ControllerTargetKind::Guest, + ), + Err(ProviderRuntimeError::ControllerDescriptorInvalid) + )); + } + } diff --git a/packages/d2b-provider-toolkit/src/server.rs b/packages/d2b-provider-toolkit/src/server.rs index 53d5fbabec..4a4dc627be 100644 --- a/packages/d2b-provider-toolkit/src/server.rs +++ b/packages/d2b-provider-toolkit/src/server.rs @@ -25,7 +25,11 @@ use d2b_contracts_resource::v3::{ execution_policy::BoundedToken, }; use d2b_contracts_resource::v3::identity::ServiceName; -use d2b_session::{Cancellation, ComponentSessionDriver}; +use d2b_session::{ + AuthenticatedSessionRouteBinding, + Cancellation, + ComponentSessionDriver, +}; use tokio::sync::Notify; use crate::{ProviderAgentAdapter, ProviderFrameCodec, ProviderService, ProviderToolkitError}; @@ -162,6 +166,17 @@ impl GeneratedProviderServiceServer { &self.adapter } + /// Bind the generated server to one authenticated controller route. + pub fn bind_authenticated_route( + &self, + route: AuthenticatedSessionRouteBinding, + ) -> Result<(), ProviderToolkitError> { + if route.service() != self.generated.package() { + return Err(ProviderToolkitError::SessionUnauthenticated); + } + self.adapter.bind_authenticated_route(route) + } + /// Borrow the generated service descriptor. pub const fn generated_service(&self) -> &GeneratedServiceDescriptor { &self.generated @@ -249,6 +264,9 @@ where } /// Serve frames from an authenticated ComponentSession. + /// + /// The server must first be bound with [`Self::bind_authenticated_route`]; + /// an unbound server refuses the loop before reading transport data. pub async fn serve_component_session( &self, driver: &D, @@ -321,4 +339,31 @@ mod tests { assert!(response.get("state").is_some()); assert_eq!(server.generated_services().len(), 1); } + + #[test] + fn generated_server_requires_an_authenticated_controller_route_for_session_serving() { + let fixture = Fixture::new(ProviderClass::Runtime, 0).expect("fixture"); + let server = GeneratedProviderServiceServer::new(FakeProvider::new(fixture.clone())); + let route = AuthenticatedSessionRouteBinding::for_test( + Some(fixture.descriptor.provider_ref().clone()), + "d2b.provider.v3", + 1, + Some(1), + Some(1), + ); + assert!(server.bind_authenticated_route(route).is_ok()); + assert!(server.adapter().has_authenticated_route()); + + let wrong_service = AuthenticatedSessionRouteBinding::for_test( + Some(fixture.descriptor.provider_ref().clone()), + "d2b.other.v3", + 1, + Some(1), + Some(1), + ); + assert_eq!( + server.bind_authenticated_route(wrong_service), + Err(ProviderToolkitError::SessionUnauthenticated) + ); + } } diff --git a/packages/d2bd-runtime/src/target_runtime.rs b/packages/d2bd-runtime/src/target_runtime.rs index d2460ae8e1..abfbd67d23 100644 --- a/packages/d2bd-runtime/src/target_runtime.rs +++ b/packages/d2bd-runtime/src/target_runtime.rs @@ -6,7 +6,7 @@ //! controller assignments, and revocation on session loss. use std::{ - collections::{BTreeMap, VecDeque}, + collections::{BTreeMap, BTreeSet, VecDeque}, sync::{ Arc, Mutex, atomic::{AtomicBool, AtomicUsize, Ordering}, @@ -15,7 +15,17 @@ use std::{ }; use d2b_contracts_broker::broker_wire::BrokerProfile; -use d2b_contracts_resource::v3::{ResourceRef, ZoneId}; +use d2b_contracts_provider::v3::{ + ArtifactDigest, ComponentDescriptor, ComponentExecution, ComponentType, + ControllerInstanceScope, ControllerTargetKind, EffectPortClass, +}; +use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceName, ResourceRef, ResourceTypeName, + ResourceUid, SchemaFingerprint, ZoneId, ZoneRevision, + identity::ReconnectGeneration, + process::{ExecutionSpec, ProcessClass, ProcessSpec}, +}; +use sha2::{Digest, Sha256}; /// The only daemon modes. A mode is selected before the runtime starts and is /// never read from a request or changed on a live instance. @@ -384,6 +394,456 @@ fn permit(kind: AdmissionKind, counters: Arc) -> AdmissionPer } } +/// Finalizer retained on every target-local controller Process until the +/// controller's children have been adopted or quarantined and terminally +/// accounted for. +pub const CONTROLLER_PROCESS_FINALIZER: &str = "provider-controller.d2bus.org/children"; + +/// Resource verbs a controller ResourceClient may use. Spec writes and +/// arbitrary deletes remain outside the controller capability. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ControllerResourceVerb { + Get, + List, + Watch, + Create, + UpdateStatus, + UpdateFinalizers, +} + +/// Closed mutation/read set granted to every admitted controller client. +pub const CONTROLLER_ALLOWED_RESOURCE_VERBS: &[ControllerResourceVerb; 6] = &[ + ControllerResourceVerb::Get, + ControllerResourceVerb::List, + ControllerResourceVerb::Watch, + ControllerResourceVerb::Create, + ControllerResourceVerb::UpdateStatus, + ControllerResourceVerb::UpdateFinalizers, +]; + +/// Lifecycle of a signed target-local controller Process. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ControllerProcessPhase { + Pending, + Launching, + Running, + Ready, + Assigned, + Draining, + Revoked, + Quarantined, + Released, +} + +/// The immutable Process resource Core creates for one signed controller +/// component and one exact execution target. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControllerProcessResource { + process_ref: ResourceRef, + controller_role_ref: ResourceRef, + uid: ResourceUid, + zone: ZoneId, + provider_ref: ResourceRef, + component_id: d2b_contracts_resource::v3::execution_policy::BoundedToken, + target: ResourceRef, + process_provider_ref: ResourceRef, + process_spec: ProcessSpec, + resource_generation: ResourceGeneration, + resource_revision: ZoneRevision, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + target_session_generation: ReconnectGeneration, + signed_descriptor_digest: ArtifactDigest, + artifact_digest: ArtifactDigest, + required_effect_classes: BTreeSet, + owned_resource_types: BTreeSet, + repair_owner: ResourceRef, +} + +impl ControllerProcessResource { + /// Borrow the generated Process resource reference. + pub const fn process_ref(&self) -> &ResourceRef { + &self.process_ref + } + + /// Borrow the signed controller role reference represented by this + /// Process instance. + pub const fn controller_role_ref(&self) -> &ResourceRef { + &self.controller_role_ref + } + + /// Borrow the generated Process resource UID. + pub const fn uid(&self) -> &ResourceUid { + &self.uid + } + + /// Borrow the containing Zone. + pub const fn zone(&self) -> &ZoneId { + &self.zone + } + + /// Borrow the semantic Provider that owns this controller role. + pub const fn provider_ref(&self) -> &ResourceRef { + &self.provider_ref + } + + /// Borrow the signed component identifier. + pub const fn component_id( + &self, + ) -> &d2b_contracts_resource::v3::execution_policy::BoundedToken { + &self.component_id + } + + /// Borrow the exact Host or Guest execution target. + pub const fn target(&self) -> &ResourceRef { + &self.target + } + + /// Borrow the selected fixed Process Provider. + pub const fn process_provider_ref(&self) -> &ResourceRef { + &self.process_provider_ref + } + + /// Borrow the generated controller Process spec. + pub const fn process_spec(&self) -> &ProcessSpec { + &self.process_spec + } + + /// Return the Process resource generation. + pub const fn resource_generation(&self) -> ResourceGeneration { + self.resource_generation + } + + /// Return the committed Process resource revision. + pub const fn resource_revision(&self) -> ZoneRevision { + self.resource_revision + } + + /// Return the installed semantic Provider generation. + pub const fn provider_generation(&self) -> ResourceGeneration { + self.provider_generation + } + + /// Return the signed controller generation. + pub const fn controller_generation(&self) -> ControllerGeneration { + self.controller_generation + } + + /// Return the parent target ComponentSession generation. + pub const fn target_session_generation(&self) -> ReconnectGeneration { + self.target_session_generation + } + + /// Borrow the signed descriptor commitment. + pub const fn signed_descriptor_digest(&self) -> &ArtifactDigest { + &self.signed_descriptor_digest + } + + /// Borrow the concrete target artifact commitment. + pub const fn artifact_digest(&self) -> &ArtifactDigest { + &self.artifact_digest + } + + /// Borrow the signed EffectPort class set. + pub fn required_effect_classes(&self) -> &BTreeSet { + &self.required_effect_classes + } + + /// Borrow the ResourceTypes owned by this signed controller role. + pub fn owned_resource_types(&self) -> &BTreeSet { + &self.owned_resource_types + } + + /// Return the finalizer Core retains while this controller owns children. + pub const fn finalizer(&self) -> &'static str { + CONTROLLER_PROCESS_FINALIZER + } + + /// Borrow the single repair owner for this Process resource. + pub const fn repair_owner(&self) -> &ResourceRef { + &self.repair_owner + } +} + +/// Authenticated readiness evidence for a controller's own ComponentSession. +/// +/// This is separate from the target session generation carried by the static +/// launch ticket. It is the only evidence accepted before a ResourceClient +/// assignment is minted. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControllerSessionBinding { + process_ref: ResourceRef, + zone: ZoneId, + provider_ref: ResourceRef, + target: ResourceRef, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + target_session_generation: ReconnectGeneration, + session_generation: ReconnectGeneration, + readiness_digest: SchemaFingerprint, +} + +impl ControllerSessionBinding { + /// Construct authenticated controller-session evidence. + #[allow(clippy::too_many_arguments)] + pub fn new( + process_ref: ResourceRef, + zone: ZoneId, + provider_ref: ResourceRef, + target: ResourceRef, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + target_session_generation: ReconnectGeneration, + session_generation: ReconnectGeneration, + readiness_digest: SchemaFingerprint, + ) -> Self { + Self { + process_ref, + zone, + provider_ref, + target, + provider_generation, + controller_generation, + target_session_generation, + session_generation, + readiness_digest, + } + } + + /// Borrow the controller Process reference. + pub const fn process_ref(&self) -> &ResourceRef { + &self.process_ref + } + + /// Borrow the Zone identity. + pub const fn zone(&self) -> &ZoneId { + &self.zone + } + + /// Borrow the semantic Provider identity. + pub const fn provider_ref(&self) -> &ResourceRef { + &self.provider_ref + } + + /// Borrow the exact execution target. + pub const fn target(&self) -> &ResourceRef { + &self.target + } + + /// Return the Provider generation. + pub const fn provider_generation(&self) -> ResourceGeneration { + self.provider_generation + } + + /// Return the controller generation. + pub const fn controller_generation(&self) -> ControllerGeneration { + self.controller_generation + } + + /// Return the target session generation. + pub const fn target_session_generation(&self) -> ReconnectGeneration { + self.target_session_generation + } + + /// Return the controller's authenticated session generation. + pub const fn session_generation(&self) -> ReconnectGeneration { + self.session_generation + } + + /// Borrow the controller readiness commitment. + pub const fn readiness_digest(&self) -> &SchemaFingerprint { + &self.readiness_digest + } +} + +/// A ResourceClient assignment request after controller readiness succeeded. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControllerAssignmentRequest { + process_ref: ResourceRef, + resource_ref: ResourceRef, + resource_uid: ResourceUid, + resource_generation: ResourceGeneration, + resource_revision: ZoneRevision, + provider_ref: ResourceRef, + target: ResourceRef, + session_generation: ReconnectGeneration, +} + +impl ControllerAssignmentRequest { + /// Construct one exact resource-to-controller assignment request. + #[allow(clippy::too_many_arguments)] + pub fn new( + process_ref: ResourceRef, + resource_ref: ResourceRef, + resource_uid: ResourceUid, + resource_generation: ResourceGeneration, + resource_revision: ZoneRevision, + provider_ref: ResourceRef, + target: ResourceRef, + session_generation: ReconnectGeneration, + ) -> Self { + Self { + process_ref, + resource_ref, + resource_uid, + resource_generation, + resource_revision, + provider_ref, + target, + session_generation, + } + } +} + +/// The exact identity bound to a controller's scoped ResourceClient. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ControllerAssignmentIdentity { + process_ref: ResourceRef, + resource_ref: ResourceRef, + resource_uid: ResourceUid, + resource_generation: ResourceGeneration, + resource_revision: ZoneRevision, + provider_ref: ResourceRef, + target: ResourceRef, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + session_generation: ReconnectGeneration, + assignment_epoch: u64, +} + +impl ControllerAssignmentIdentity { + /// Borrow the controller Process identity. + pub const fn process_ref(&self) -> &ResourceRef { + &self.process_ref + } + + /// Borrow the assigned resource. + pub const fn resource_ref(&self) -> &ResourceRef { + &self.resource_ref + } + + /// Borrow the assigned resource UID. + pub const fn resource_uid(&self) -> &ResourceUid { + &self.resource_uid + } + + /// Return the assigned resource generation. + pub const fn resource_generation(&self) -> ResourceGeneration { + self.resource_generation + } + + /// Return the assigned resource revision. + pub const fn resource_revision(&self) -> ZoneRevision { + self.resource_revision + } + + /// Borrow the semantic Provider identity. + pub const fn provider_ref(&self) -> &ResourceRef { + &self.provider_ref + } + + /// Borrow the exact execution target. + pub const fn target(&self) -> &ResourceRef { + &self.target + } + + /// Return the Provider generation. + pub const fn provider_generation(&self) -> ResourceGeneration { + self.provider_generation + } + + /// Return the controller generation. + pub const fn controller_generation(&self) -> ControllerGeneration { + self.controller_generation + } + + /// Return the authenticated controller session generation. + pub const fn session_generation(&self) -> ReconnectGeneration { + self.session_generation + } + + /// Return the monotonically increasing assignment epoch. + pub const fn assignment_epoch(&self) -> u64 { + self.assignment_epoch + } +} + +/// A controller child observed during restart adoption. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControllerChildObservation { + child_ref: ResourceRef, + uid: ResourceUid, + identity_verified: bool, +} + +impl ControllerChildObservation { + /// Construct a fully verified child observation. + pub fn verified(child_ref: ResourceRef, uid: ResourceUid) -> Self { + Self { + child_ref, + uid, + identity_verified: true, + } + } + + /// Construct an ambiguous observation that must be quarantined. + pub fn quarantined(child_ref: ResourceRef, uid: ResourceUid) -> Self { + Self { + child_ref, + uid, + identity_verified: false, + } + } +} + +/// Result of adopting or quarantining the observed controller children. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ControllerChildAdoption { + pub adopted: usize, + pub quarantined: usize, +} + +/// Context passed from ProviderDeployment to the fixed Process adapter for a +/// static controller launch. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControllerLaunchContext { + resource: ControllerProcessResource, + target_readiness_digest: SchemaFingerprint, +} + +impl ControllerLaunchContext { + /// Borrow the generated Process resource. + pub const fn resource(&self) -> &ControllerProcessResource { + &self.resource + } + + /// Borrow the target readiness commitment bound into the LaunchTicket. + pub const fn target_readiness_digest(&self) -> &SchemaFingerprint { + &self.target_readiness_digest + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ChildDisposition { + Adopted, + Quarantined, +} + +#[derive(Debug)] +struct ControllerRecord { + resource: ControllerProcessResource, + phase: ControllerProcessPhase, + controller_permit: Option, + target_ready: bool, + launch_identity: Option<[u8; 32]>, + target_readiness_digest: Option, + ready_session: Option, + last_session_generation: Option, + assignments: BTreeMap>, + children: BTreeMap, + finalizer_held: bool, +} + /// The immutable identity of one controller assignment. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ControllerAssignmentKey { @@ -485,6 +945,135 @@ impl Drop for AssignmentLease { } } +/// A live authenticated controller session. +/// +/// Dropping the lease revokes every assignment for this exact controller +/// session generation. A later reconnect must authenticate again and receives +/// a new assignment epoch. +#[derive(Debug)] +pub struct ControllerSessionLease { + process_ref: ResourceRef, + binding: ControllerSessionBinding, + deployment: ProviderDeployment, +} + +impl ControllerSessionLease { + /// Borrow the authenticated session binding. + pub const fn binding(&self) -> &ControllerSessionBinding { + &self.binding + } + + /// Return the authenticated controller session generation. + pub const fn generation(&self) -> ReconnectGeneration { + self.binding.session_generation + } + + /// Borrow the controller Process reference. + pub const fn process_ref(&self) -> &ResourceRef { + &self.process_ref + } + + /// Return whether this exact controller session still admits work. + pub fn is_active(&self) -> bool { + self.deployment + .controller_session_is_active(&self.process_ref, self.binding.session_generation()) + } +} + +impl Drop for ControllerSessionLease { + fn drop(&mut self) { + let _ = self + .deployment + .revoke_controller_session(&self.process_ref, self.binding.session_generation); + } +} + +/// A scoped ResourceClient assignment held by one ready controller session. +#[derive(Debug)] +pub struct ControllerAssignmentLease { + identity: ControllerAssignmentIdentity, + state: Arc, + assignments: Arc>>>, + controllers: Arc>>>>, + resource_types: BTreeSet, + _controller_permit: AdmissionPermit, +} + +impl ControllerAssignmentLease { + /// Borrow the complete assignment identity. + pub const fn identity(&self) -> &ControllerAssignmentIdentity { + &self.identity + } + + /// Return whether this assignment may still mutate or observe. + pub fn is_active(&self) -> bool { + self.state.active.load(Ordering::Acquire) + } + + /// Borrow the ResourceTypes this controller client may query. + pub fn resource_types(&self) -> &BTreeSet { + &self.resource_types + } + + /// Return whether a closed controller verb is in scope for this client. + pub fn allows(&self, verb: ControllerResourceVerb) -> bool { + self.is_active() && CONTROLLER_ALLOWED_RESOURCE_VERBS.contains(&verb) + } + + /// Revoke this exact assignment. + pub fn revoke(&self) { + self.state.revoke(); + unregister_controller_assignment( + &self.assignments, + &self.controllers, + &self.identity, + &self.state, + ); + } +} + +impl Drop for ControllerAssignmentLease { + fn drop(&mut self) { + self.state.revoke(); + unregister_controller_assignment( + &self.assignments, + &self.controllers, + &self.identity, + &self.state, + ); + } +} + +fn unregister_controller_assignment( + assignments: &Mutex>>, + controllers: &Mutex>>>, + identity: &ControllerAssignmentIdentity, + state: &Arc, +) { + if let Ok(mut current) = assignments.lock() + && current + .get(identity) + .is_some_and(|candidate| Arc::ptr_eq(candidate, state)) + { + current.remove(identity); + } + if let Ok(current) = controllers.lock() + && let Some(record) = current.get(identity.process_ref()) + && let Ok(mut record) = record.lock() + { + record.assignments.retain(|candidate, candidate_state| { + candidate != identity || !Arc::ptr_eq(candidate_state, state) + }); + if record.assignments.is_empty() && record.phase == ControllerProcessPhase::Assigned { + record.phase = if record.ready_session.is_some() { + ControllerProcessPhase::Ready + } else { + ControllerProcessPhase::Revoked + }; + } + } +} + fn unregister_assignment( registry: &Mutex>>, key: &ControllerAssignmentKey, @@ -505,6 +1094,10 @@ pub struct ProviderDeployment { mode: DaemonMode, admission: AdmissionBudget, assignments: Arc>>>, + controllers: Arc>>>>, + controller_assignments: + Arc>>>, + next_assignment_epoch: Arc, } impl ProviderDeployment { @@ -513,6 +1106,9 @@ impl ProviderDeployment { mode, admission: AdmissionBudget::new(limits)?, assignments: Arc::new(Mutex::new(BTreeMap::new())), + controllers: Arc::new(Mutex::new(BTreeMap::new())), + controller_assignments: Arc::new(Mutex::new(BTreeMap::new())), + next_assignment_epoch: Arc::new(std::sync::atomic::AtomicU64::new(0)), }) } @@ -564,6 +1160,7 @@ impl ProviderDeployment { if session_generation == 0 { return Err(DeploymentError::GenerationZero); } + let mut count = 0usize; let mut assignments = self .assignments .lock() @@ -576,9 +1173,39 @@ impl ProviderDeployment { for key in &keys { if let Some(state) = assignments.remove(key) { state.revoke(); + count = count.saturating_add(1); + } + } + drop(assignments); + let records = self + .controllers + .lock() + .map_err(|_| DeploymentError::StateUnavailable)? + .values() + .cloned() + .collect::>(); + for record in records { + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + let matches = record + .ready_session + .as_ref() + .is_some_and(|binding| binding.session_generation().get() == session_generation); + if matches { + count = count.saturating_add(revoke_record_assignments( + &self.controller_assignments, + &mut record, + ReconnectGeneration::new(session_generation) + .map_err(|_| DeploymentError::GenerationZero)?, + )?); + record.ready_session = None; + if record.phase != ControllerProcessPhase::Released { + record.phase = ControllerProcessPhase::Revoked; + } } } - Ok(keys.len()) + Ok(count) } /// Revoke every assignment before replacing the target's generation. @@ -597,7 +1224,86 @@ impl ProviderDeployment { state.revoke(); } } - Ok(keys.len()) + let mut count = keys.len(); + drop(assignments); + let records = self + .controllers + .lock() + .map_err(|_| DeploymentError::StateUnavailable)? + .values() + .cloned() + .collect::>(); + for record in records { + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if record.resource.target() == target { + count = count.saturating_add(revoke_record_assignments_all( + &self.controller_assignments, + &mut record, + )); + record.ready_session = None; + if record.phase != ControllerProcessPhase::Released { + record.phase = ControllerProcessPhase::Revoked; + } + } + } + Ok(count) + } + + /// Revoke every assignment and controller instance for one Provider + /// generation before a Provider replacement is admitted. + pub fn revoke_provider( + &self, + provider: &ResourceRef, + provider_generation: u64, + ) -> Result { + if provider_generation == 0 { + return Err(DeploymentError::GenerationZero); + } + let mut assignments = self + .assignments + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + let keys = assignments + .keys() + .filter(|key| { + &key.provider == provider && key.provider_generation == provider_generation + }) + .cloned() + .collect::>(); + let mut count = keys.len(); + for key in keys { + if let Some(state) = assignments.remove(&key) { + state.revoke(); + } + } + drop(assignments); + let records = self + .controllers + .lock() + .map_err(|_| DeploymentError::StateUnavailable)? + .values() + .cloned() + .collect::>(); + for record in records { + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if &record.resource.provider_ref == provider + && record.resource.provider_generation.get() == provider_generation + { + count = count.saturating_add(revoke_record_assignments_all( + &self.controller_assignments, + &mut record, + )); + record.ready_session = None; + if record.phase != ControllerProcessPhase::Released { + record.phase = ControllerProcessPhase::Revoked; + } + } + } + Ok(count) } pub fn active_assignments(&self) -> Result { @@ -605,33 +1311,987 @@ impl ProviderDeployment { .assignments .lock() .map_err(|_| DeploymentError::StateUnavailable)?; + let legacy = assignments + .values() + .filter(|state| state.active.load(Ordering::Acquire)) + .count(); + drop(assignments); + Ok(legacy.saturating_add(self.active_controller_assignments()?)) + } + + /// Return the number of active controller ResourceClient assignments. + pub fn active_controller_assignments(&self) -> Result { + let assignments = self + .controller_assignments + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; Ok(assignments .values() .filter(|state| state.active.load(Ordering::Acquire)) .count()) } -} -/// ProviderDeployment refusal. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DeploymentError { - Admission(AdmissionError), - GenerationZero, - TargetWrongKind, - ProviderWrongKind, - AssignmentAlreadyActive, - StateUnavailable, -} - -impl std::fmt::Display for DeploymentError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(match self { - Self::Admission(error) => return error.fmt(formatter), - Self::GenerationZero => "provider-deployment-generation-zero", + /// Create one signed target-local controller Process resource. + /// + /// The returned resource is only an intent. No executable is resolved and + /// no process is spawned until [`Self::begin_controller_launch`] is + /// admitted by the fixed Process adapter. + #[allow(clippy::too_many_arguments)] + pub fn create_controller_process( + &self, + zone: ZoneId, + provider_ref: ResourceRef, + descriptor: &ComponentDescriptor, + resource_generation: ResourceGeneration, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + target_session_generation: ReconnectGeneration, + resource_revision: ZoneRevision, + target: ResourceRef, + process_provider_ref: ResourceRef, + target_ready: bool, + ) -> Result { + validate_controller_descriptor( + self.mode, + &provider_ref, + descriptor, + &target, + &process_provider_ref, + resource_generation, + provider_generation, + controller_generation, + target_session_generation, + resource_revision, + )?; + let process_name = controller_process_name( + &zone, + &provider_ref, + descriptor.component_id(), + &target, + provider_generation, + controller_generation, + target_session_generation, + ); + let process_ref = ResourceRef::new( + ResourceTypeName::parse("Process") + .map_err(|_| DeploymentError::ControllerDescriptorInvalid)?, + ResourceName::parse(process_name) + .map_err(|_| DeploymentError::ControllerDescriptorInvalid)?, + ); + let controller_role_ref = ResourceRef::new( + ResourceTypeName::parse("Process") + .map_err(|_| DeploymentError::ControllerDescriptorInvalid)?, + ResourceName::parse(descriptor.component_id().as_str()) + .map_err(|_| DeploymentError::ControllerDescriptorInvalid)?, + ); + let uid = controller_process_uid( + &zone, + &provider_ref, + descriptor.component_id(), + &target, + provider_generation, + controller_generation, + target_session_generation, + )?; + let process_spec = ProcessSpec::minimal( + ExecutionSpec::minimal( + target.clone(), + ProcessClass::Controller, + descriptor.component_id().clone(), + ) + .map_err(|_| DeploymentError::ControllerDescriptorInvalid)?, + ); + let target_kind = controller_target_kind(&target)?; + let capability = descriptor + .target_capability(target_kind) + .ok_or(DeploymentError::ControllerDescriptorInvalid)?; + let resource = ControllerProcessResource { + process_ref: process_ref.clone(), + controller_role_ref, + uid, + zone, + provider_ref, + component_id: descriptor.component_id().clone(), + target, + process_provider_ref, + process_spec, + resource_generation, + resource_revision, + provider_generation, + controller_generation, + target_session_generation, + signed_descriptor_digest: descriptor.config_digest().clone(), + artifact_digest: capability.artifact_digest().clone(), + required_effect_classes: capability.required_effect_classes().clone(), + owned_resource_types: descriptor.exported_resource_types().clone(), + repair_owner: process_ref.clone(), + }; + let controller_permit = self + .admission + .try_admit(AdmissionKind::Controller) + .map_err(DeploymentError::Admission)?; + let mut controllers = self + .controllers + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if controllers.contains_key(&resource.process_ref) { + return Err(DeploymentError::ControllerAlreadyDeployed); + } + controllers.insert( + resource.process_ref.clone(), + Arc::new(Mutex::new(ControllerRecord { + resource: resource.clone(), + phase: ControllerProcessPhase::Pending, + controller_permit: Some(controller_permit), + target_ready, + launch_identity: None, + target_readiness_digest: None, + ready_session: None, + last_session_generation: None, + assignments: BTreeMap::new(), + children: BTreeMap::new(), + finalizer_held: true, + })), + ); + Ok(resource) + } + + /// Return the current lifecycle phase for one controller Process. + pub fn controller_phase(&self, process_ref: &ResourceRef) -> Option { + self.controllers + .lock() + .ok() + .and_then(|controllers| controllers.get(process_ref).cloned()) + .and_then(|record| record.lock().ok().map(|record| record.phase)) + } + + fn controller_session_is_active( + &self, + process_ref: &ResourceRef, + session_generation: ReconnectGeneration, + ) -> bool { + self.controllers + .lock() + .ok() + .and_then(|controllers| controllers.get(process_ref).cloned()) + .and_then(|record| { + record.lock().ok().map(|record| { + matches!( + record.phase, + ControllerProcessPhase::Ready | ControllerProcessPhase::Assigned + ) && record + .ready_session + .as_ref() + .is_some_and(|binding| binding.session_generation() == session_generation) + }) + }) + .unwrap_or(false) + } + + /// Return the signed target-local controller Process resources currently + /// owned by this deployment. + pub fn controller_processes(&self) -> Result, DeploymentError> { + let records = self + .controllers + .lock() + .map_err(|_| DeploymentError::StateUnavailable)? + .values() + .cloned() + .collect::>(); + records + .into_iter() + .map(|record| { + record + .lock() + .map_err(|_| DeploymentError::StateUnavailable) + .map(|record| record.resource.clone()) + }) + .collect() + } + + /// Borrow one signed target-local controller Process resource. + pub fn controller_process( + &self, + process_ref: &ResourceRef, + ) -> Result { + self.controller_record(process_ref)? + .lock() + .map_err(|_| DeploymentError::StateUnavailable) + .map(|record| record.resource.clone()) + } + + /// Begin one target-local controller launch after target readiness passed. + pub fn begin_controller_launch( + &self, + process_ref: &ResourceRef, + target_readiness_digest: SchemaFingerprint, + ) -> Result { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if !record.target_ready { + return Err(DeploymentError::ControllerTargetNotReady); + } + if digest_is_zero(target_readiness_digest.as_str()) { + return Err(DeploymentError::ControllerIdentityInvalid); + } + if record.phase != ControllerProcessPhase::Pending { + return Err(DeploymentError::ControllerLaunchInvalid); + } + record.phase = ControllerProcessPhase::Launching; + record.target_readiness_digest = Some(target_readiness_digest.clone()); + Ok(ControllerLaunchContext { + resource: record.resource.clone(), + target_readiness_digest, + }) + } + + /// Record a fixed Process adapter launch receipt. + pub fn controller_launch_succeeded( + &self, + process_ref: &ResourceRef, + identity: [u8; 32], + ) -> Result<(), DeploymentError> { + if identity == [0; 32] { + return Err(DeploymentError::ControllerIdentityInvalid); + } + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if record.phase != ControllerProcessPhase::Launching { + return Err(DeploymentError::ControllerLaunchInvalid); + } + if record + .launch_identity + .is_some_and(|existing| existing != identity) + { + return Err(DeploymentError::ControllerIdentityChanged); + } + record.launch_identity = Some(identity); + record.phase = ControllerProcessPhase::Running; + Ok(()) + } + + /// Record a restart adoption receipt for an existing controller Process. + pub fn controller_adopted( + &self, + process_ref: &ResourceRef, + identity: [u8; 32], + ) -> Result<(), DeploymentError> { + if identity == [0; 32] { + return Err(DeploymentError::ControllerIdentityInvalid); + } + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if !matches!( + record.phase, + ControllerProcessPhase::Pending + | ControllerProcessPhase::Launching + | ControllerProcessPhase::Running + | ControllerProcessPhase::Revoked + ) { + return Err(DeploymentError::ControllerLaunchInvalid); + } + if record + .launch_identity + .is_some_and(|existing| existing != identity) + { + return Err(DeploymentError::ControllerIdentityChanged); + } + record.launch_identity = Some(identity); + record.phase = ControllerProcessPhase::Running; + Ok(()) + } + + /// Return a failed controller launch to Pending, or quarantine it when + /// the Process adapter cannot establish an unambiguous identity. + pub fn controller_launch_failed( + &self, + process_ref: &ResourceRef, + quarantine: bool, + ) -> Result<(), DeploymentError> { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if record.phase != ControllerProcessPhase::Launching { + return Err(DeploymentError::ControllerLaunchInvalid); + } + record.phase = if quarantine { + ControllerProcessPhase::Quarantined + } else { + ControllerProcessPhase::Pending + }; + if !quarantine { + record.target_readiness_digest = None; + } + Ok(()) + } + + /// Quarantine a controller whose restart adoption found ambiguous + /// process identity. Quarantine retains the finalizer and blocks cleanup. + pub fn quarantine_controller(&self, process_ref: &ResourceRef) -> Result<(), DeploymentError> { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + record.phase = ControllerProcessPhase::Quarantined; + record.ready_session = None; + revoke_record_assignments_all(&self.controller_assignments, &mut record); + Ok(()) + } + + /// Whether the controller Process finalizer is still retained. + pub fn controller_finalizer_held(&self, process_ref: &ResourceRef) -> Option { + self.controllers + .lock() + .ok() + .and_then(|controllers| controllers.get(process_ref).cloned()) + .and_then(|record| record.lock().ok().map(|record| record.finalizer_held)) + } + + /// Admit a controller's separate authenticated ComponentSession. + pub fn admit_controller_session( + &self, + binding: ControllerSessionBinding, + ) -> Result { + let record = self.controller_record(binding.process_ref())?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + validate_controller_session(&record.resource, &binding)?; + if record + .target_readiness_digest + .as_ref() + .is_some_and(|expected| expected != binding.readiness_digest()) + { + return Err(DeploymentError::ControllerSessionMismatch); + } + if record.phase == ControllerProcessPhase::Released + || record.phase == ControllerProcessPhase::Quarantined + || record.phase == ControllerProcessPhase::Launching + || record.phase == ControllerProcessPhase::Pending + { + return Err(DeploymentError::ControllerNotReady); + } + if let Some(previous) = record.ready_session.as_ref() { + if binding.session_generation() <= previous.session_generation() { + return Err(DeploymentError::ControllerSessionStale); + } + let previous_generation = previous.session_generation(); + revoke_record_assignments( + &self.controller_assignments, + &mut record, + previous_generation, + )?; + record.phase = ControllerProcessPhase::Revoked; + } else if let Some(previous) = record.last_session_generation + && binding.session_generation() <= previous + { + return Err(DeploymentError::ControllerSessionStale); + } + record.last_session_generation = Some(binding.session_generation()); + record.ready_session = Some(binding.clone()); + record.phase = if record.assignments.is_empty() { + ControllerProcessPhase::Ready + } else { + ControllerProcessPhase::Assigned + }; + Ok(ControllerSessionLease { + process_ref: binding.process_ref().clone(), + binding, + deployment: self.clone(), + }) + } + + /// Admit one ResourceClient only after the controller session is ready. + pub fn admit_controller_assignment( + &self, + request: ControllerAssignmentRequest, + ) -> Result { + let record = self.controller_record(&request.process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + let ready = record + .ready_session + .as_ref() + .ok_or(DeploymentError::ControllerNotReady)?; + if record.phase != ControllerProcessPhase::Ready + && record.phase != ControllerProcessPhase::Assigned + { + return Err(DeploymentError::ControllerNotReady); + } + if request.provider_ref != record.resource.provider_ref + || request.target != record.resource.target + || request.session_generation != ready.session_generation() + || request.session_generation + != record + .last_session_generation + .ok_or(DeploymentError::ControllerSessionMismatch)? + || request.resource_ref.resource_type().as_str() == "Provider" + { + return Err(DeploymentError::ControllerSessionMismatch); + } + if !record + .resource + .owned_resource_types + .contains(request.resource_ref.resource_type()) + { + return Err(DeploymentError::ControllerResourceTypeUnowned); + } + if request.resource_generation.get() == 0 + || request.resource_revision.get() == 0 + || request.resource_uid.as_str().is_empty() + { + return Err(DeploymentError::ControllerAssignmentInvalid); + } + if record.assignments.keys().any(|identity| { + identity.resource_ref == request.resource_ref + && identity.resource_uid == request.resource_uid + && identity.resource_generation == request.resource_generation + }) { + return Err(DeploymentError::ControllerAssignmentConflict); + } + let permit = self + .admission + .try_admit(AdmissionKind::Controller) + .map_err(DeploymentError::Admission)?; + let epoch = self + .next_assignment_epoch + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |value| { + value.checked_add(1) + }) + .map_err(|_| DeploymentError::AssignmentEpochExhausted)? + .saturating_add(1); + let identity = ControllerAssignmentIdentity { + process_ref: request.process_ref, + resource_ref: request.resource_ref, + resource_uid: request.resource_uid, + resource_generation: request.resource_generation, + resource_revision: request.resource_revision, + provider_ref: request.provider_ref, + target: request.target, + provider_generation: record.resource.provider_generation, + controller_generation: record.resource.controller_generation, + session_generation: request.session_generation, + assignment_epoch: epoch, + }; + let state = Arc::new(AssignmentState::new()); + let mut assignments = self + .controller_assignments + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if assignments.contains_key(&identity) { + return Err(DeploymentError::ControllerAssignmentConflict); + } + state.install_permit(permit.clone())?; + assignments.insert(identity.clone(), Arc::clone(&state)); + record + .assignments + .insert(identity.clone(), Arc::clone(&state)); + record.phase = ControllerProcessPhase::Assigned; + Ok(ControllerAssignmentLease { + identity, + state, + assignments: Arc::clone(&self.controller_assignments), + controllers: Arc::clone(&self.controllers), + resource_types: record.resource.owned_resource_types.clone(), + _controller_permit: permit, + }) + } + + /// Revoke every controller assignment and session bound to one session. + pub fn revoke_controller_session( + &self, + process_ref: &ResourceRef, + session_generation: ReconnectGeneration, + ) -> Result { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if record + .ready_session + .as_ref() + .is_none_or(|session| session.session_generation() != session_generation) + { + return Ok(0); + } + let count = revoke_record_assignments( + &self.controller_assignments, + &mut record, + session_generation, + )?; + record.ready_session = None; + if record.phase != ControllerProcessPhase::Released { + record.phase = ControllerProcessPhase::Revoked; + } + Ok(count) + } + + /// Record one child resource under the controller's finalizer owner. + pub fn record_controller_child( + &self, + process_ref: &ResourceRef, + child_ref: ResourceRef, + child_uid: ResourceUid, + ) -> Result<(), DeploymentError> { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if !record.finalizer_held { + return Err(DeploymentError::ControllerReleased); + } + if child_ref == *process_ref { + return Err(DeploymentError::ControllerChildAmbiguous); + } + if let Some((existing_uid, _)) = record.children.get(&child_ref) + && existing_uid != &child_uid + { + record.phase = ControllerProcessPhase::Quarantined; + return Err(DeploymentError::ControllerChildAmbiguous); + } + record + .children + .insert(child_ref, (child_uid, ChildDisposition::Quarantined)); + Ok(()) + } + + /// Adopt verified children or quarantine ambiguous observations. + pub fn adopt_controller_children( + &self, + process_ref: &ResourceRef, + observations: impl IntoIterator, + ) -> Result { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if !record.finalizer_held { + return Err(DeploymentError::ControllerReleased); + } + let mut result = ControllerChildAdoption { + adopted: 0, + quarantined: 0, + }; + for observation in observations { + let Some((expected_uid, disposition)) = record.children.get_mut(&observation.child_ref) + else { + record.phase = ControllerProcessPhase::Quarantined; + return Err(DeploymentError::ControllerChildAmbiguous); + }; + if expected_uid != &observation.uid || !observation.identity_verified { + *disposition = ChildDisposition::Quarantined; + result.quarantined = result.quarantined.saturating_add(1); + record.phase = ControllerProcessPhase::Quarantined; + } else { + *disposition = ChildDisposition::Adopted; + result.adopted = result.adopted.saturating_add(1); + } + } + Ok(result) + } + + /// Remove one child only after exact adopted identity and terminal state. + pub fn remove_controller_child( + &self, + process_ref: &ResourceRef, + child_ref: ResourceRef, + child_uid: &ResourceUid, + ) -> Result<(), DeploymentError> { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + let Some((expected_uid, disposition)) = record.children.get(&child_ref) else { + return Err(DeploymentError::ControllerChildMissing); + }; + if expected_uid != child_uid { + return Err(DeploymentError::ControllerChildAmbiguous); + } + if *disposition != ChildDisposition::Adopted { + return Err(DeploymentError::ControllerChildQuarantined); + } + record.children.remove(&child_ref); + Ok(()) + } + + /// Prepare controller cleanup without releasing its finalizer. + pub fn prepare_controller_cleanup( + &self, + process_ref: &ResourceRef, + repair_owner: &ResourceRef, + ) -> Result<(), DeploymentError> { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if repair_owner != &record.resource.repair_owner { + return Err(DeploymentError::ControllerRepairOwnerMismatch); + } + if !record.finalizer_held { + return Err(DeploymentError::ControllerFinalizerMissing); + } + if !record.assignments.is_empty() { + return Err(DeploymentError::ControllerAssignmentActive); + } + if !record.children.is_empty() { + return Err(DeploymentError::ControllerChildrenUnresolved); + } + if record.phase == ControllerProcessPhase::Quarantined { + return Err(DeploymentError::ControllerChildQuarantined); + } + if !matches!( + record.phase, + ControllerProcessPhase::Revoked | ControllerProcessPhase::Draining + ) { + return Err(DeploymentError::ControllerCleanupInvalid); + } + record.phase = ControllerProcessPhase::Draining; + Ok(()) + } + + /// Complete cleanup and clear the finalizer only for the named owner. + pub fn complete_controller_cleanup( + &self, + process_ref: &ResourceRef, + repair_owner: &ResourceRef, + ) -> Result<(), DeploymentError> { + let record = self.controller_record(process_ref)?; + let mut record = record + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + if repair_owner != &record.resource.repair_owner { + return Err(DeploymentError::ControllerRepairOwnerMismatch); + } + if record.phase != ControllerProcessPhase::Draining + || !record.children.is_empty() + || !record.assignments.is_empty() + { + return Err(DeploymentError::ControllerCleanupInvalid); + } + record.finalizer_held = false; + if let Some(permit) = record.controller_permit.take() { + permit.release(); + } + record.phase = ControllerProcessPhase::Released; + Ok(()) + } + + fn controller_record( + &self, + process_ref: &ResourceRef, + ) -> Result>, DeploymentError> { + self.controllers + .lock() + .map_err(|_| DeploymentError::StateUnavailable)? + .get(process_ref) + .cloned() + .ok_or(DeploymentError::ControllerNotFound) + } +} + +fn controller_target_kind(target: &ResourceRef) -> Result { + match target.resource_type().as_str() { + "Host" => Ok(ControllerTargetKind::Host), + "Guest" => Ok(ControllerTargetKind::Guest), + _ => Err(DeploymentError::TargetWrongKind), + } +} + +#[allow(clippy::too_many_arguments)] +fn validate_controller_descriptor( + mode: DaemonMode, + provider_ref: &ResourceRef, + descriptor: &ComponentDescriptor, + target: &ResourceRef, + process_provider_ref: &ResourceRef, + resource_generation: ResourceGeneration, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + target_session_generation: ReconnectGeneration, + resource_revision: ZoneRevision, +) -> Result<(), DeploymentError> { + if provider_ref.resource_type().as_str() != "Provider" + || process_provider_ref.resource_type().as_str() != "Provider" + || !matches!( + descriptor.execution(), + ComponentExecution::Launchable { .. } + ) + || descriptor.component_type() != ComponentType::Controller + || resource_generation.get() == 0 + || provider_generation.get() == 0 + || controller_generation.get() == 0 + || target_session_generation.get() == 0 + || resource_revision.get() == 0 + { + return Err(DeploymentError::ControllerDescriptorInvalid); + } + let target_kind = controller_target_kind(target)?; + let expected_target = match mode { + DaemonMode::Host => ControllerTargetKind::Host, + DaemonMode::Guest => ControllerTargetKind::Guest, + }; + if target_kind != expected_target + || !descriptor.supported_target_kinds().contains(&target_kind) + || !matches!( + descriptor.instance_scope(), + Some( + ControllerInstanceScope::FixedExecutionTarget + | ControllerInstanceScope::PerResourceTarget + ) + ) + { + return Err(DeploymentError::TargetWrongKind); + } + let Some(capability) = descriptor.target_capability(target_kind) else { + return Err(DeploymentError::ControllerDescriptorInvalid); + }; + if digest_is_zero(capability.artifact_digest().as_str()) + || digest_is_zero(descriptor.config_digest().as_str()) + || !capability + .required_effect_classes() + .contains(&EffectPortClass::Process) + { + return Err(DeploymentError::ControllerDescriptorInvalid); + } + Ok(()) +} + +fn controller_process_name( + zone: &ZoneId, + provider_ref: &ResourceRef, + component_id: &d2b_contracts_resource::v3::execution_policy::BoundedToken, + target: &ResourceRef, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + target_session_generation: ReconnectGeneration, +) -> String { + let mut digest = Sha256::new(); + digest.update(b"d2b-controller-process-v1\0"); + digest.update(zone.as_str().as_bytes()); + digest.update([0]); + digest.update(provider_ref.to_canonical_string().as_bytes()); + digest.update([0]); + digest.update(component_id.as_str().as_bytes()); + digest.update([0]); + digest.update(target.to_canonical_string().as_bytes()); + digest.update(provider_generation.get().to_be_bytes()); + digest.update(controller_generation.get().to_be_bytes()); + digest.update(target_session_generation.get().to_be_bytes()); + let digest = digest.finalize(); + format!( + "controller-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7] + ) +} + +fn controller_process_uid( + zone: &ZoneId, + provider_ref: &ResourceRef, + component_id: &d2b_contracts_resource::v3::execution_policy::BoundedToken, + target: &ResourceRef, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + target_session_generation: ReconnectGeneration, +) -> Result { + let mut digest = Sha256::new(); + digest.update(b"d2b-controller-process-uid-v1\0"); + digest.update(zone.as_str().as_bytes()); + digest.update([0]); + digest.update(provider_ref.to_canonical_string().as_bytes()); + digest.update([0]); + digest.update(component_id.as_str().as_bytes()); + digest.update([0]); + digest.update(target.to_canonical_string().as_bytes()); + digest.update(provider_generation.get().to_be_bytes()); + digest.update(controller_generation.get().to_be_bytes()); + digest.update(target_session_generation.get().to_be_bytes()); + let digest: [u8; 32] = digest.finalize().into(); + let mut bytes = [0_u8; 16]; + bytes.copy_from_slice(&digest[..16]); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + let rendered = format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + bytes[4], + bytes[5], + bytes[6], + bytes[7], + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15], + ); + ResourceUid::parse(rendered).map_err(|_| DeploymentError::ControllerDescriptorInvalid) +} + +fn validate_controller_session( + resource: &ControllerProcessResource, + binding: &ControllerSessionBinding, +) -> Result<(), DeploymentError> { + if binding.process_ref() != resource.process_ref() + || binding.zone() != resource.zone() + || binding.provider_ref() != resource.provider_ref() + || binding.target() != resource.target() + || binding.provider_generation() != resource.provider_generation() + || binding.controller_generation() != resource.controller_generation() + || binding.target_session_generation() != resource.target_session_generation() + || binding.session_generation().get() == 0 + || digest_is_zero(binding.readiness_digest().as_str()) + { + return Err(DeploymentError::ControllerSessionMismatch); + } + Ok(()) +} + +fn digest_is_zero(value: &str) -> bool { + value + .strip_prefix("sha256:") + .is_some_and(|digest| digest.bytes().all(|byte| byte == b'0')) +} + +fn revoke_record_assignments( + assignments: &Mutex>>, + record: &mut ControllerRecord, + session_generation: ReconnectGeneration, +) -> Result { + let keys = record + .assignments + .keys() + .filter(|identity| identity.session_generation == session_generation) + .cloned() + .collect::>(); + let count = keys.len(); + for key in &keys { + if let Some(state) = record.assignments.get(key) { + state.revoke(); + } + } + let mut assignments = assignments + .lock() + .map_err(|_| DeploymentError::StateUnavailable)?; + for key in keys { + assignments.remove(&key); + record.assignments.remove(&key); + } + Ok(count) +} + +fn revoke_record_assignments_all( + assignments: &Mutex>>, + record: &mut ControllerRecord, +) -> usize { + let keys = record.assignments.keys().cloned().collect::>(); + let count = keys.len(); + for key in &keys { + if let Some(state) = record.assignments.get(key) { + state.revoke(); + } + } + if let Ok(mut assignments) = assignments.lock() { + for key in &keys { + assignments.remove(key); + } + } + for key in keys { + record.assignments.remove(&key); + } + count +} + +/// ProviderDeployment refusal. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeploymentError { + Admission(AdmissionError), + GenerationZero, + TargetWrongKind, + ProviderWrongKind, + AssignmentAlreadyActive, + StateUnavailable, + ControllerDescriptorInvalid, + ControllerTargetNotReady, + ControllerAlreadyDeployed, + ControllerLaunchInvalid, + ControllerIdentityInvalid, + ControllerIdentityChanged, + ControllerNotFound, + ControllerNotReady, + ControllerSessionMismatch, + ControllerSessionStale, + ControllerAssignmentInvalid, + ControllerResourceTypeUnowned, + ControllerAssignmentConflict, + ControllerAssignmentActive, + AssignmentEpochExhausted, + ControllerReleased, + ControllerChildAmbiguous, + ControllerChildMissing, + ControllerChildQuarantined, + ControllerChildrenUnresolved, + ControllerRepairOwnerMismatch, + ControllerFinalizerMissing, + ControllerCleanupInvalid, +} + +impl std::fmt::Display for DeploymentError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Admission(error) => return error.fmt(formatter), + Self::GenerationZero => "provider-deployment-generation-zero", Self::TargetWrongKind => "provider-deployment-target-wrong-kind", Self::ProviderWrongKind => "provider-deployment-provider-wrong-kind", Self::AssignmentAlreadyActive => "provider-deployment-assignment-active", Self::StateUnavailable => "provider-deployment-state-unavailable", + Self::ControllerDescriptorInvalid => { + "provider-deployment-controller-descriptor-invalid" + } + Self::ControllerTargetNotReady => "provider-deployment-controller-target-not-ready", + Self::ControllerAlreadyDeployed => "provider-deployment-controller-already-deployed", + Self::ControllerLaunchInvalid => "provider-deployment-controller-launch-invalid", + Self::ControllerIdentityInvalid => "provider-deployment-controller-identity-invalid", + Self::ControllerIdentityChanged => "provider-deployment-controller-identity-changed", + Self::ControllerNotFound => "provider-deployment-controller-not-found", + Self::ControllerNotReady => "provider-deployment-controller-not-ready", + Self::ControllerSessionMismatch => "provider-deployment-controller-session-mismatch", + Self::ControllerSessionStale => "provider-deployment-controller-session-stale", + Self::ControllerAssignmentInvalid => { + "provider-deployment-controller-assignment-invalid" + } + Self::ControllerResourceTypeUnowned => { + "provider-deployment-controller-resource-type-unowned" + } + Self::ControllerAssignmentConflict => { + "provider-deployment-controller-assignment-conflict" + } + Self::ControllerAssignmentActive => "provider-deployment-controller-assignment-active", + Self::AssignmentEpochExhausted => "provider-deployment-assignment-epoch-exhausted", + Self::ControllerReleased => "provider-deployment-controller-released", + Self::ControllerChildAmbiguous => "provider-deployment-controller-child-ambiguous", + Self::ControllerChildMissing => "provider-deployment-controller-child-missing", + Self::ControllerChildQuarantined => "provider-deployment-controller-child-quarantined", + Self::ControllerChildrenUnresolved => { + "provider-deployment-controller-children-unresolved" + } + Self::ControllerRepairOwnerMismatch => { + "provider-deployment-controller-repair-owner-mismatch" + } + Self::ControllerFinalizerMissing => "provider-deployment-controller-finalizer-missing", + Self::ControllerCleanupInvalid => "provider-deployment-controller-cleanup-invalid", }) } } @@ -741,4 +2401,376 @@ mod tests { Err(DeploymentError::ProviderWrongKind) )); } + + fn signed_controller_descriptor() -> d2b_contracts_provider::v3::ComponentDescriptor { + use d2b_contracts_provider::v3::{ + ArtifactDigest, BinaryRef, ComponentTargetCapability, ComponentType, + ControllerInstanceScope, ControllerTargetKind, EffectPortClass, + }; + use d2b_contracts_resource::v3::execution_policy::{BoundedToken, ExecutionDomain}; + + let digest = ArtifactDigest::parse(format!("sha256:{}", "a".repeat(64))).unwrap(); + ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [ResourceTypeName::parse("Process").unwrap()], + [BoundedToken::parse("reconcile").unwrap()], + [ExecutionDomain::System], + 8, + digest.clone(), + [], + false, + ) + .unwrap() + .with_execution(d2b_contracts_provider::v3::ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest, + [EffectPortClass::Process], + ) + .unwrap()]) + .unwrap() + } + + #[test] + fn target_local_controller_launch_requires_ready_session_before_assignment() { + use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceUid, ZoneRevision, + identity::ReconnectGeneration, + }; + + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()) + .expect("deployment"); + let provider = resource("Provider", "runtime"); + let process_provider = resource("Provider", "system-systemd"); + let target = resource("Guest", "workload"); + let process = deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + provider.clone(), + &signed_controller_descriptor(), + ResourceGeneration::new(3).unwrap(), + ResourceGeneration::new(7).unwrap(), + ControllerGeneration::new(4).unwrap(), + ReconnectGeneration::new(2).unwrap(), + ZoneRevision::new(11), + target.clone(), + process_provider, + true, + ) + .expect("controller process"); + + assert_eq!( + process.process_spec().execution().process_class(), + ProcessClass::Controller + ); + assert_eq!( + process.controller_role_ref(), + &resource("Process", "process-controller") + ); + let readiness = d2b_contracts_resource::v3::SchemaFingerprint::parse(format!( + "sha256:{}", + "b".repeat(64) + )) + .unwrap(); + let launch = deployment + .begin_controller_launch(process.process_ref(), readiness.clone()) + .expect("launch admission"); + assert_eq!(launch.resource().process_ref(), process.process_ref()); + assert!(matches!( + deployment.admit_controller_assignment(ControllerAssignmentRequest::new( + process.process_ref().clone(), + resource("Process", "owned"), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ResourceGeneration::new(1).unwrap(), + ZoneRevision::new(12), + provider.clone(), + target.clone(), + ReconnectGeneration::new(2).unwrap(), + )), + Err(DeploymentError::ControllerNotReady) + )); + + deployment + .controller_launch_succeeded(process.process_ref(), [9; 32]) + .expect("launch success"); + let session = deployment + .admit_controller_session(ControllerSessionBinding::new( + process.process_ref().clone(), + process.zone().clone(), + provider.clone(), + target.clone(), + process.provider_generation(), + process.controller_generation(), + process.target_session_generation(), + ReconnectGeneration::new(5).unwrap(), + readiness, + )) + .expect("controller session"); + let assignment = deployment + .admit_controller_assignment(ControllerAssignmentRequest::new( + process.process_ref().clone(), + resource("Process", "owned"), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ResourceGeneration::new(1).unwrap(), + ZoneRevision::new(12), + provider, + target, + session.generation(), + )) + .expect("scoped assignment"); + assert!(assignment.is_active()); + assert_eq!(assignment.identity().resource_generation().get(), 1); + assert_eq!(assignment.identity().assignment_epoch(), 1); + assert_eq!(deployment.active_controller_assignments().unwrap(), 1); + drop(session); + assert!(!assignment.is_active()); + assert_eq!(deployment.active_controller_assignments().unwrap(), 0); + let reconnect = deployment + .admit_controller_session(ControllerSessionBinding::new( + process.process_ref().clone(), + process.zone().clone(), + process.provider_ref().clone(), + process.target().clone(), + process.provider_generation(), + process.controller_generation(), + process.target_session_generation(), + ReconnectGeneration::new(6).unwrap(), + d2b_contracts_resource::v3::SchemaFingerprint::parse(format!( + "sha256:{}", + "b".repeat(64) + )) + .unwrap(), + )) + .expect("reconnect"); + let replacement = deployment + .admit_controller_assignment(ControllerAssignmentRequest::new( + process.process_ref().clone(), + resource("Process", "owned"), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ResourceGeneration::new(1).unwrap(), + ZoneRevision::new(12), + process.provider_ref().clone(), + process.target().clone(), + reconnect.generation(), + )) + .expect("reconnected assignment"); + assert_eq!(replacement.identity().assignment_epoch(), 2); + assert!(replacement.is_active()); + drop(reconnect); + assert!(!replacement.is_active()); + } + + #[test] + fn controller_cleanup_requires_verified_children_and_exact_repair_owner() { + use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceUid, ZoneRevision, + identity::ReconnectGeneration, + }; + + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()) + .expect("deployment"); + let process = deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + resource("Provider", "runtime"), + &signed_controller_descriptor(), + ResourceGeneration::new(3).unwrap(), + ResourceGeneration::new(7).unwrap(), + ControllerGeneration::new(4).unwrap(), + ReconnectGeneration::new(2).unwrap(), + ZoneRevision::new(11), + resource("Guest", "workload"), + resource("Provider", "system-systemd"), + true, + ) + .expect("controller process"); + deployment + .begin_controller_launch( + process.process_ref(), + d2b_contracts_resource::v3::SchemaFingerprint::parse(format!( + "sha256:{}", + "b".repeat(64) + )) + .unwrap(), + ) + .unwrap(); + deployment + .controller_launch_succeeded(process.process_ref(), [9; 32]) + .unwrap(); + let session = deployment + .admit_controller_session(ControllerSessionBinding::new( + process.process_ref().clone(), + process.zone().clone(), + process.provider_ref().clone(), + process.target().clone(), + process.provider_generation(), + process.controller_generation(), + process.target_session_generation(), + ReconnectGeneration::new(5).unwrap(), + d2b_contracts_resource::v3::SchemaFingerprint::parse(format!( + "sha256:{}", + "b".repeat(64) + )) + .unwrap(), + )) + .unwrap(); + let child_uid = ResourceUid::parse("223e4567-e89b-42d3-a456-426614174001").unwrap(); + deployment + .record_controller_child( + process.process_ref(), + resource("Process", "child"), + child_uid.clone(), + ) + .unwrap(); + deployment + .revoke_session(session.generation().get()) + .expect("revoke session"); + assert!( + deployment + .prepare_controller_cleanup(process.process_ref(), process.process_ref()) + .is_err() + ); + deployment + .adopt_controller_children( + process.process_ref(), + [ControllerChildObservation::verified( + resource("Process", "child"), + child_uid.clone(), + )], + ) + .unwrap(); + deployment + .remove_controller_child( + process.process_ref(), + resource("Process", "child"), + &child_uid, + ) + .unwrap(); + deployment + .prepare_controller_cleanup(process.process_ref(), process.process_ref()) + .expect("cleanup owner"); + deployment + .complete_controller_cleanup(process.process_ref(), process.process_ref()) + .expect("cleanup complete"); + assert_eq!( + deployment.controller_phase(process.process_ref()), + Some(ControllerProcessPhase::Released) + ); + } + + #[test] + fn ambiguous_controller_child_stays_quarantined_and_keeps_finalizer() { + use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceUid, ZoneRevision, + identity::ReconnectGeneration, + }; + + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()) + .expect("deployment"); + let process = deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + resource("Provider", "runtime"), + &signed_controller_descriptor(), + ResourceGeneration::new(3).unwrap(), + ResourceGeneration::new(7).unwrap(), + ControllerGeneration::new(4).unwrap(), + ReconnectGeneration::new(2).unwrap(), + ZoneRevision::new(11), + resource("Guest", "workload"), + resource("Provider", "system-systemd"), + true, + ) + .expect("controller process"); + deployment + .record_controller_child( + process.process_ref(), + resource("Process", "child"), + ResourceUid::parse("223e4567-e89b-42d3-a456-426614174001").unwrap(), + ) + .unwrap(); + deployment + .adopt_controller_children( + process.process_ref(), + [ControllerChildObservation::quarantined( + resource("Process", "child"), + ResourceUid::parse("223e4567-e89b-42d3-a456-426614174001").unwrap(), + )], + ) + .unwrap(); + assert_eq!( + deployment.controller_phase(process.process_ref()), + Some(ControllerProcessPhase::Quarantined) + ); + assert!(matches!( + deployment.remove_controller_child( + process.process_ref(), + resource("Process", "child"), + &ResourceUid::parse("223e4567-e89b-42d3-a456-426614174001").unwrap(), + ), + Err(DeploymentError::ControllerChildQuarantined) + )); + assert!( + deployment + .controller_finalizer_held(process.process_ref()) + .unwrap() + ); + } + + #[test] + fn target_session_reconnect_gets_a_new_controller_process_identity() { + use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ZoneRevision, identity::ReconnectGeneration, + }; + + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()) + .expect("deployment"); + let provider = resource("Provider", "runtime"); + let target = resource("Guest", "workload"); + let first = deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + provider.clone(), + &signed_controller_descriptor(), + ResourceGeneration::new(1).unwrap(), + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + ZoneRevision::new(5), + target.clone(), + resource("Provider", "system-systemd"), + true, + ) + .unwrap(); + let second = deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + provider, + &signed_controller_descriptor(), + ResourceGeneration::new(1).unwrap(), + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(5).unwrap(), + ZoneRevision::new(6), + target, + resource("Provider", "system-systemd"), + true, + ) + .unwrap(); + assert_ne!(first.process_ref(), second.process_ref()); + assert_ne!(first.uid(), second.uid()); + assert_eq!(first.controller_role_ref(), second.controller_role_ref()); + } } diff --git a/packages/d2bd/src/process_provider_runtime.rs b/packages/d2bd/src/process_provider_runtime.rs index 026ba6885d..4cdf7b57d8 100644 --- a/packages/d2bd/src/process_provider_runtime.rs +++ b/packages/d2bd/src/process_provider_runtime.rs @@ -14,14 +14,9 @@ use std::{ }; use d2b_contracts_broker::broker_wire::BrokerCallerRole; +use d2b_contracts_resource::v3::execution_policy::{BoundedToken, ExecutionDomain}; use d2b_contracts_resource::v3::{ - execution_policy::{BoundedToken, ExecutionDomain}, -}; -use d2b_contracts_resource::v3::{ - ControllerGeneration, - ResourceGeneration, - ResourceRef, - ResourceUid, + ControllerGeneration, ResourceGeneration, ResourceRef, ResourceUid, process::{EphemeralProcessSpec, ProcessSpec}, }; use d2b_core::{ @@ -39,8 +34,11 @@ use d2b_provider_supervisor::{ }; use d2b_provider_system_minijail::{MinijailProcessProvider, launch::PlatformGate}; use d2b_provider_system_systemd::SystemdProcessProvider; +use d2bd_runtime::target_runtime::{ControllerProcessResource, DaemonMode}; use sha2::{Digest, Sha256}; +use crate::provider_effects::FixedEffectAdapter; + /// The fixed process Provider names wired by the daemon. pub const FIXED_PROCESS_PROVIDER_NAMES: [&str; 2] = ["system-minijail", "system-systemd"]; @@ -231,6 +229,8 @@ pub struct ProductionProcessProviders { minijail: MinijailProcessProvider, systemd: SystemdProcessProvider, bundle: BundleResolver, + mode: DaemonMode, + fixed_effect: FixedEffectAdapter, managed: Mutex>, managed_resources: Mutex>, } @@ -250,25 +250,43 @@ impl ProductionProcessProviders { bundle: BundleResolver, broker_socket: impl Into, caller_role: BrokerCallerRole, + ) -> Self { + Self::new_for_mode(bundle, broker_socket, caller_role, DaemonMode::Host) + } + + /// Construct both fixed process Providers over a mode-bound broker. + /// + /// The mode is selected once at construction. It is passed to both + /// concrete broker adapters and cannot be widened by a Process ticket. + pub fn new_for_mode( + bundle: BundleResolver, + broker_socket: impl Into, + caller_role: BrokerCallerRole, + mode: DaemonMode, ) -> Self { let broker_socket = broker_socket.into(); + let fixed_socket = broker_socket.clone(); + let daemon_uid = caller_uid(&caller_role); let resolver = BundleBackedLaunchResolver::new(bundle.clone()).with_observation_socket( broker_socket.clone(), Duration::from_secs(10), caller_role.clone(), ); - let minijail_backend = BrokerProcessBackend::with_socket_and_role( + let minijail_backend = BrokerProcessBackend::with_socket_profile_and_role( resolver.clone(), broker_socket.clone(), Duration::from_secs(10), + mode.broker_profile(), caller_role.clone(), ); - let systemd_owner = BrokerSystemdEffectOwner::with_socket( + let systemd_owner = BrokerSystemdEffectOwner::with_socket_profile_and_role( resolver, broker_socket, Duration::from_secs(10), + mode.broker_profile(), caller_role, ); + let fixed_effect = FixedEffectAdapter::for_mode(mode, fixed_socket, daemon_uid); let platform_gate = detect_minijail_platform_gate(); Self { minijail: MinijailProcessProvider::with_platform_gate( @@ -279,11 +297,23 @@ impl ProductionProcessProviders { SystemdProcessBackend::new(systemd_owner), )), bundle, + mode, + fixed_effect, managed: Mutex::new(BTreeMap::new()), managed_resources: Mutex::new(BTreeMap::new()), } } + /// Return the fixed daemon mode bound to these Process Providers. + pub const fn mode(&self) -> DaemonMode { + self.mode + } + + /// Return the broker profile sealed into both concrete Process adapters. + pub const fn broker_profile(&self) -> d2b_contracts_broker::broker_wire::BrokerProfile { + self.mode.broker_profile() + } + /// Borrow the daemon-owned minijail Provider. pub const fn minijail(&self) -> &MinijailProcessProvider { &self.minijail @@ -299,6 +329,12 @@ impl ProductionProcessProviders { &FIXED_PROCESS_PROVIDER_NAMES } + /// Borrow the fixed mode-bound effect adapter used by controller + /// launches. + pub const fn fixed_effect(&self) -> &FixedEffectAdapter { + &self.fixed_effect + } + /// Return whether this node is a daemon-owned Provider process. pub fn supports_node(node: &ProcessNode) -> bool { matches!( @@ -498,6 +534,110 @@ impl ProductionProcessProviders { }) } + /// Launch a signed target-local controller Process. + /// + /// The controller is represented by a normal Process ticket. The fixed + /// adapter validates the target mode first, then the selected Process + /// Provider delivers the ticket through its mode-bound broker backend. + pub(crate) async fn launch_controller( + &self, + resource: &ControllerProcessResource, + target_readiness_digest: ConfigurationDigest, + timeout: Duration, + ) -> Result { + self.validate_controller_target(resource)?; + let provider = managed_provider_from_ref(resource.process_provider_ref())?; + let ticket = + controller_launch_ticket(resource, provider, target_readiness_digest, timeout)?; + self.fixed_effect + .validate_controller_ticket(&ticket) + .map_err(|error| error.to_string())?; + let report = match provider { + ManagedProvider::Minijail => self + .minijail + .launch(&ticket) + .await + .map_err(provider_error)?, + ManagedProvider::Systemd => { + self.systemd.launch(&ticket).await.map_err(provider_error)? + } + }; + self.remember_resource( + resource.process_ref(), + resource.uid(), + resource.resource_generation(), + provider, + report.identity, + )?; + Ok(ProviderLaunch { + identity: report.identity, + }) + } + + /// Adopt a signed target-local controller Process after daemon restart. + pub(crate) async fn adopt_controller( + &self, + resource: &ControllerProcessResource, + target_readiness_digest: ConfigurationDigest, + ) -> Result { + self.validate_controller_target(resource)?; + let provider = managed_provider_from_ref(resource.process_provider_ref())?; + let ticket = controller_launch_ticket( + resource, + provider, + target_readiness_digest, + Duration::from_secs(30), + )?; + self.fixed_effect + .validate_controller_ticket(&ticket) + .map_err(|error| error.to_string())?; + let outcome = match provider { + ManagedProvider::Minijail => { + self.minijail.adopt(&ticket).await.map_err(provider_error)? + } + ManagedProvider::Systemd => { + self.systemd.adopt(&ticket).await.map_err(provider_error)? + } + }; + match outcome { + AdoptionOutcome::Absent => Ok(ProviderAdoption::Absent), + AdoptionOutcome::Adopted(report) => { + self.remember_resource( + resource.process_ref(), + resource.uid(), + resource.resource_generation(), + provider, + report.identity, + )?; + Ok(ProviderAdoption::Adopted(report)) + } + AdoptionOutcome::Quarantined(report) => { + self.forget_resource(resource.process_ref()); + Ok(ProviderAdoption::Quarantined(report)) + } + } + } + + fn validate_controller_target( + &self, + resource: &ControllerProcessResource, + ) -> Result<(), String> { + let expected = match self.mode { + DaemonMode::Host => "Host", + DaemonMode::Guest => "Guest", + }; + if resource.target().resource_type().as_str() != expected { + return Err("provider-controller-target-denied".to_owned()); + } + if !resource + .required_effect_classes() + .contains(&d2b_contracts_provider::v3::EffectPortClass::Process) + { + return Err("provider-controller-effect-class-denied".to_owned()); + } + Ok(()) + } + /// Adopt one durable Process resource with the controller generation /// rehydrated from the owning Zone store. pub(crate) async fn adopt_resource( @@ -1177,6 +1317,15 @@ fn provider_error(error: ProcessConformanceError) -> String { error.code().to_owned() } +fn caller_uid(caller: &BrokerCallerRole) -> u32 { + match caller { + BrokerCallerRole::AdminUid { uid } + | BrokerCallerRole::LauncherUid { uid } + | BrokerCallerRole::RootUid { uid } => *uid, + BrokerCallerRole::CutoverRunner { .. } | BrokerCallerRole::NotAuthorized => 0, + } +} + fn managed_provider_from_ref(provider_ref: &ResourceRef) -> Result { match provider_ref.name().as_str() { "system-minijail" => Ok(ManagedProvider::Minijail), @@ -1185,6 +1334,120 @@ fn managed_provider_from_ref(provider_ref: &ResourceRef) -> Result Result { + let owner_provider = BoundedToken::parse(resource.provider_ref().name().as_str()) + .map_err(|_| "provider-ticket:invalid-owner-provider")?; + let selected_provider = BoundedToken::parse(resource.process_provider_ref().name().as_str()) + .map_err(|_| "provider-ticket:invalid-process-provider")?; + let component = resource.component_id().clone(); + let operation_scope = format!( + "{}:{}:{}:{}", + component.as_str(), + resource.provider_generation().get(), + resource.controller_generation().get(), + resource.target_session_generation().get(), + ); + let operation_uid = stable_uid( + "controller-launch", + &resource.process_ref().to_canonical_string(), + &operation_scope, + resource.resource_generation().get(), + ); + let deadline_ms = timeout.as_millis().clamp(1, 900_000) as u32; + let operation = OperationBinding::new(operation_uid, deadline_ms) + .map_err(|_| "provider-ticket:invalid-operation")?; + let mut ticket = LaunchTicket::new( + resource.process_ref().clone(), + resource.uid().clone(), + resource.resource_generation(), + resource.controller_generation(), + owner_provider, + component.clone(), + component, + resource.target().clone(), + ExecutionDomain::System, + None, + selected_provider, + compiled_controller_digests(resource, provider, &target_readiness_digest), + operation, + required_identity(provider), + ) + .map_err(|error| format!("provider-ticket:{}", error.code()))?; + let sandbox = SandboxCompiler + .compile_plan( + resource.process_spec().execution().sandbox(), + ExecutionDomain::System, + false, + ) + .map_err(|error| format!("provider-ticket:{}", error.code()))?; + let signed_descriptor_digest = configuration_digest( + "signed-descriptor", + resource.signed_descriptor_digest().as_str(), + ); + ticket = ticket + .with_resource_revision(resource.resource_revision()) + .map_err(|error| format!("provider-ticket:{}", error.code()))? + .with_controller_launch_binding( + resource.provider_generation(), + resource.target_session_generation(), + signed_descriptor_digest, + target_readiness_digest, + ) + .map_err(|error| format!("provider-ticket:{}", error.code()))? + .with_sandbox_plan(sandbox) + .with_readiness(ReadinessExpectation::None); + Ok(ticket) +} + +fn compiled_controller_digests( + resource: &ControllerProcessResource, + provider: ManagedProvider, + readiness: &ConfigurationDigest, +) -> CompiledDigests { + fn digest(label: &str, bytes: &[u8]) -> ConfigurationDigest { + let mut hasher = Sha256::new(); + hasher.update(b"d2bd-provider-controller-ticket-v1"); + hasher.update(label.as_bytes()); + hasher.update([0]); + hasher.update(bytes); + ConfigurationDigest::from_bytes(hasher.finalize().into()) + } + let context = format!( + "{}:{}:{}:{}", + resource.process_ref().to_canonical_string(), + resource.signed_descriptor_digest().as_str(), + resource.artifact_digest().as_str(), + match provider { + ManagedProvider::Minijail => "system-minijail", + ManagedProvider::Systemd => "system-systemd", + }, + ); + let bytes = format!("{context}:{}", readiness.to_hex()).into_bytes(); + CompiledDigests { + sandbox: digest("sandbox", &bytes), + budget: digest("budget", &bytes), + mounts: digest("mounts", &bytes), + devices: digest("devices", &bytes), + network: digest("network", &bytes), + endpoints: digest("endpoints", &bytes), + fd_table: digest("fd-table", &bytes), + } +} + +fn configuration_digest(label: &str, value: &str) -> ConfigurationDigest { + let mut hasher = Sha256::new(); + hasher.update(b"d2bd-provider-configuration-digest-v1"); + hasher.update(label.as_bytes()); + hasher.update([0]); + hasher.update(value.as_bytes()); + ConfigurationDigest::from_bytes(hasher.finalize().into()) +} + fn resource_ticket( bundle: &BundleResolver, context: ProcessResourceContext<'_>, @@ -1481,6 +1744,68 @@ fn stable_token(value: &str) -> String { #[cfg(test)] mod tests { use super::*; + use d2b_contracts_provider::v3::{ + ArtifactDigest, BinaryRef, ComponentDescriptor, ComponentExecution, + ComponentTargetCapability, ComponentType, ControllerInstanceScope, ControllerTargetKind, + EffectPortClass, + }; + use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceRef, ResourceTypeName, ZoneId, + ZoneRevision, + execution_policy::{BoundedToken, ExecutionDomain}, + identity::ReconnectGeneration, + }; + use d2bd_runtime::target_runtime::ProviderDeployment; + + fn controller_resource() -> ControllerProcessResource { + let digest = ArtifactDigest::parse(format!("sha256:{}", "a".repeat(64))).unwrap(); + let descriptor = ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [ResourceTypeName::parse("Process").unwrap()], + [BoundedToken::parse("reconcile").unwrap()], + [ExecutionDomain::System], + 8, + digest.clone(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest, + [EffectPortClass::Process], + ) + .unwrap()]) + .unwrap(); + ProviderDeployment::new( + DaemonMode::Guest, + d2bd_runtime::target_runtime::AdmissionLimits::guest_default(), + ) + .unwrap() + .create_controller_process( + ZoneId::parse("work").unwrap(), + ResourceRef::parse("Provider/runtime").unwrap(), + &descriptor, + ResourceGeneration::new(1).unwrap(), + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + ZoneRevision::new(5), + ResourceRef::parse("Guest/workload").unwrap(), + ResourceRef::parse("Provider/system-systemd").unwrap(), + true, + ) + .unwrap() + } #[test] fn stable_uids_are_uuid_v4_shaped_and_repeatable() { @@ -1542,4 +1867,35 @@ mod tests { assert!(!retryable_stop_error("identity-mismatch")); assert!(!retryable_stop_error("permission-denied")); } + + #[test] + fn controller_launch_ticket_binds_target_descriptor_and_session_without_assignment() { + let resource = controller_resource(); + let readiness = ConfigurationDigest::from_bytes([7; 32]); + let ticket = controller_launch_ticket( + &resource, + ManagedProvider::Systemd, + readiness, + Duration::from_secs(5), + ) + .expect("controller ticket"); + assert!(ticket.validate_controller_launch().is_ok()); + assert_eq!(ticket.process_ref(), resource.process_ref()); + assert_eq!(ticket.execution_ref(), resource.target()); + assert_eq!( + ticket.provider_generation(), + Some(resource.provider_generation()) + ); + assert_eq!( + ticket.target_session_generation(), + Some(resource.target_session_generation()) + ); + assert_eq!( + ticket.resource_revision(), + Some(resource.resource_revision()) + ); + assert!(ticket.signed_descriptor_digest().is_some()); + assert!(!ticket.has_assignment_binding()); + assert!(ticket.resource_client_binding().is_none()); + } } diff --git a/packages/d2bd/src/provider_effects.rs b/packages/d2bd/src/provider_effects.rs index a5b9b3a942..7fc3b6c946 100644 --- a/packages/d2bd/src/provider_effects.rs +++ b/packages/d2bd/src/provider_effects.rs @@ -20,10 +20,8 @@ use std::{ use d2b_contracts_broker::broker_wire::BrokerCallerRole; use d2b_contracts_broker::broker_wire::{BrokerRequest, BrokerResponse}; -use d2b_contracts_resource::v3::{ - ResourceRef, - ZoneId, -}; +use d2b_contracts_resource::v3::{ResourceRef, ZoneId}; +use d2b_process_conformance::LaunchTicket; use d2bd_runtime::{ broker_transport::{ModeBoundBrokerAdapter, ModeBoundBrokerError}, target_runtime::DaemonMode, @@ -58,6 +56,14 @@ pub struct FixedEffectAdapter { } impl FixedEffectAdapter { + /// Construct the adapter from one closed daemon mode. + pub fn for_mode(mode: DaemonMode, socket_path: PathBuf, daemon_uid: u32) -> Self { + match mode { + DaemonMode::Host => Self::host(socket_path, daemon_uid), + DaemonMode::Guest => Self::guest(socket_path, daemon_uid), + } + } + pub fn host(socket_path: PathBuf, daemon_uid: u32) -> Self { Self { mode: DaemonMode::Host, @@ -80,6 +86,13 @@ impl FixedEffectAdapter { self.broker.profile() } + /// Validate the fixed socket instance before any ticket is delivered. + pub fn validate_instance(&self) -> Result<(), FixedEffectError> { + self.broker + .validate_instance() + .map_err(FixedEffectError::Broker) + } + pub fn allows(&self, class: ProviderEffectClass) -> bool { match self.mode { DaemonMode::Host => true, @@ -106,12 +119,43 @@ impl FixedEffectAdapter { .dispatch(request, timeout) .map_err(FixedEffectError::Broker) } + + /// Validate a signed target-local controller LaunchTicket against this + /// adapter's fixed daemon mode before it reaches a Process Provider. + /// + /// The ticket carries target-session evidence only. ResourceClient + /// assignment is admitted separately by ProviderDeployment after the + /// controller authenticates and publishes readiness. + pub fn validate_controller_ticket( + &self, + ticket: &LaunchTicket, + ) -> Result<(), FixedEffectError> { + self.validate_instance()?; + ticket + .validate_controller_launch() + .map_err(|_| FixedEffectError::ControllerTicketInvalid)?; + let expected_target = match self.mode { + DaemonMode::Host => "Host", + DaemonMode::Guest => "Guest", + }; + if ticket.execution_ref().resource_type().as_str() != expected_target { + return Err(FixedEffectError::ControllerTargetDenied); + } + if !self.allows(ProviderEffectClass::ProcessLaunch) { + return Err(FixedEffectError::EffectClassDenied( + ProviderEffectClass::ProcessLaunch, + )); + } + Ok(()) + } } /// Refusal at the fixed EffectPort boundary. #[derive(Debug)] pub enum FixedEffectError { EffectClassDenied(ProviderEffectClass), + ControllerTicketInvalid, + ControllerTargetDenied, Broker(ModeBoundBrokerError), } @@ -121,6 +165,12 @@ impl std::fmt::Display for FixedEffectError { Self::EffectClassDenied(class) => { write!(formatter, "provider-effect-class-denied:{}", class.as_str()) } + Self::ControllerTicketInvalid => { + formatter.write_str("provider-controller-ticket-invalid") + } + Self::ControllerTargetDenied => { + formatter.write_str("provider-controller-target-denied") + } Self::Broker(error) => error.fmt(formatter), } } @@ -907,12 +957,53 @@ fn now_ms() -> u64 { #[cfg(test)] mod tests { use super::*; + use d2b_contracts_resource::v3::{ + ResourceGeneration, ResourceRef, ZoneRevision, identity::ReconnectGeneration, + }; + use d2b_process_conformance::ConfigurationDigest; use std::sync::{ Arc, Barrier, atomic::{AtomicBool, AtomicUsize, Ordering}, mpsc::{Sender, channel}, }; + #[test] + fn fixed_effect_adapter_binds_controller_ticket_to_its_mode() { + let ticket = d2b_process_conformance::testing::fixtures::ticket_builder() + .execution_ref(ResourceRef::parse("Guest/dev-vm").unwrap()) + .build() + .unwrap() + .with_resource_revision(ZoneRevision::new(1)) + .unwrap() + .with_controller_launch_binding( + ResourceGeneration::new(2).unwrap(), + ReconnectGeneration::new(3).unwrap(), + ConfigurationDigest::from_bytes([1; 32]), + ConfigurationDigest::from_bytes([2; 32]), + ) + .unwrap(); + let guest = FixedEffectAdapter::guest("/run/d2b/guest-broker.sock".into(), 997); + assert!(guest.validate_controller_ticket(&ticket).is_ok()); + let host = FixedEffectAdapter::host("/run/d2b/broker.sock".into(), 997); + assert!(matches!( + host.validate_controller_ticket(&ticket), + Err(FixedEffectError::ControllerTargetDenied) + )); + } + + #[test] + fn malformed_controller_ticket_is_rejected_before_broker_io() { + let ticket = d2b_process_conformance::testing::fixtures::ticket_builder() + .execution_ref(ResourceRef::parse("Guest/dev-vm").unwrap()) + .build() + .unwrap(); + let guest = FixedEffectAdapter::guest("/run/d2b/guest-broker.sock".into(), 997); + assert!(matches!( + guest.validate_controller_ticket(&ticket), + Err(FixedEffectError::ControllerTicketInvalid) + )); + } + struct RecordingEffect { calls: Arc, reject: AtomicBool, diff --git a/packages/d2bd/src/provider_registry.rs b/packages/d2bd/src/provider_registry.rs index 939560c945..b82443392a 100644 --- a/packages/d2bd/src/provider_registry.rs +++ b/packages/d2bd/src/provider_registry.rs @@ -13,18 +13,13 @@ use std::{ }; use d2b_contracts_broker::broker_wire::BrokerCallerRole; -use d2b_contracts_zone_session::v3::{ - zone_routing::{ZoneLabelId, ZonePath}, -}; +use d2b_contracts_provider::v3::{ComponentType, ControllerInstanceScope, ProviderManifest}; +use d2b_contracts_resource::v3::identity::ServiceName; use d2b_contracts_resource::v3::{ - ResourceRef, - ConfigurationGeneration, - ResourceGeneration, - ResourceName, - SchemaFingerprint, - ZoneId, + ConfigurationGeneration, ControllerGeneration, ResourceGeneration, ResourceName, ResourceRef, + SchemaFingerprint, ZoneId, ZoneRevision, identity::ReconnectGeneration, }; -use d2b_contracts_resource::v3::identity::ServiceName; +use d2b_contracts_zone_session::v3::zone_routing::{ZoneLabelId, ZonePath}; use d2b_core::host::HostJson; use d2b_provider::instance::ProviderInstance; use d2b_provider::{ @@ -34,11 +29,17 @@ use d2b_provider::{ }; use sha2::{Digest, Sha256}; -use crate::process_provider_runtime::ProductionProcessProviders; +use crate::process_provider_runtime::{ + ProductionProcessProviders, ProviderAdoption, ProviderLaunch, +}; use crate::provider_effects::{ EffectDispatch, GuestLifecycleOperation, GuestLifecycleRequest, ProviderEffectError, ProviderLifecycleDispatch, ProviderLifecycleEffectPort, }; +use d2b_process_conformance::ConfigurationDigest; +use d2bd_runtime::target_runtime::{ + ControllerProcessResource, DaemonMode, DeploymentError, ProviderDeployment, +}; /// Version of the v3 Provider bundle artifact. pub const PROVIDER_BUNDLE_VERSION: u32 = 3; @@ -88,6 +89,15 @@ pub enum ProviderCompositionError { CatalogFingerprintInvalid, /// The registry state lock was poisoned. StateUnavailable, + /// A signed controller manifest could not be admitted for deployment. + ControllerManifestInvalid, + /// A target-local controller Process could not be created. + ControllerDeployment(DeploymentError), + /// The fixed Process adapter or broker rejected a controller effect. + ControllerEffectRejected, + /// The Process adapter could not prove an unambiguous controller + /// identity; reuse is forbidden until repair. + ControllerEffectAmbiguous, /// The shared Provider registry rejected a descriptor or instance. RegistryBuild(RegistryBuildError), } @@ -107,6 +117,10 @@ impl ProviderCompositionError { Self::ZonePathInvalid => "provider-zone-path-invalid", Self::CatalogFingerprintInvalid => "provider-catalog-fingerprint-invalid", Self::StateUnavailable => "provider-registry-state-unavailable", + Self::ControllerManifestInvalid => "provider-controller-manifest-invalid", + Self::ControllerDeployment(_) => "provider-controller-deployment-rejected", + Self::ControllerEffectRejected => "provider-controller-effect-rejected", + Self::ControllerEffectAmbiguous => "provider-controller-effect-ambiguous", Self::RegistryBuild(_) => "provider-registry-build-rejected", } } @@ -275,6 +289,222 @@ pub fn validate_provider_bundle_version( Ok(()) } +/// Create the target-local controller Process resources advertised by one +/// signed Provider manifest. +/// +/// This function is deliberately an intent-only step. It never resolves a +/// binary or starts a child. Launches are admitted later through the fixed +/// Process Provider and the mode-bound broker. +#[allow(clippy::too_many_arguments)] +pub fn deploy_target_local_controllers( + deployment: &ProviderDeployment, + zone: ZoneId, + provider_ref: ResourceRef, + manifest: &ProviderManifest, + resource_generation: ResourceGeneration, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + target_session_generation: ReconnectGeneration, + resource_revision: ZoneRevision, + target: ResourceRef, + process_provider_ref: ResourceRef, + target_ready: bool, +) -> Result, ProviderCompositionError> { + manifest + .validate_installation_contract() + .map_err(|_| ProviderCompositionError::ControllerManifestInvalid)?; + if provider_ref.resource_type().as_str() != "Provider" + || provider_ref.name().as_str() != manifest.artifact_id().as_str() + { + return Err(ProviderCompositionError::ControllerManifestInvalid); + } + let target_kind = match target.resource_type().as_str() { + "Host" => d2b_contracts_provider::v3::ControllerTargetKind::Host, + "Guest" => d2b_contracts_provider::v3::ControllerTargetKind::Guest, + _ => return Err(ProviderCompositionError::ControllerManifestInvalid), + }; + let expected_kind = match deployment.mode() { + DaemonMode::Host => d2b_contracts_provider::v3::ControllerTargetKind::Host, + DaemonMode::Guest => d2b_contracts_provider::v3::ControllerTargetKind::Guest, + }; + if target_kind != expected_kind { + return Err(ProviderCompositionError::ControllerManifestInvalid); + } + let mut resources = Vec::new(); + for descriptor in manifest.components() { + if descriptor.component_type() != ComponentType::Controller + || matches!( + descriptor.instance_scope(), + Some(ControllerInstanceScope::ZoneSingleton) + ) + || !descriptor.supported_target_kinds().contains(&target_kind) + { + continue; + } + resources.push( + deployment + .create_controller_process( + zone.clone(), + provider_ref.clone(), + descriptor, + resource_generation, + provider_generation, + controller_generation, + target_session_generation, + resource_revision, + target.clone(), + process_provider_ref.clone(), + target_ready, + ) + .map_err(ProviderCompositionError::ControllerDeployment)?, + ); + } + Ok(resources) +} + +/// Launch one previously created target-local controller Process through the +/// fixed, mode-bound Process Provider adapter. +pub(crate) async fn launch_target_local_controller( + providers: &ProductionProcessProviders, + resource: &ControllerProcessResource, + target_readiness_digest: ConfigurationDigest, + timeout: std::time::Duration, +) -> Result { + providers + .launch_controller(resource, target_readiness_digest, timeout) + .await + .map_err(|error| { + if error.contains("ambiguous") + || error.contains("identity") + || error.contains("deadline") + || error.contains("fate") + { + ProviderCompositionError::ControllerEffectAmbiguous + } else { + ProviderCompositionError::ControllerEffectRejected + } + }) +} + +/// Adopt one target-local controller Process through the same fixed adapter +/// used for its launch. +pub(crate) async fn adopt_target_local_controller( + providers: &ProductionProcessProviders, + resource: &ControllerProcessResource, + target_readiness_digest: ConfigurationDigest, +) -> Result { + providers + .adopt_controller(resource, target_readiness_digest) + .await + .map_err(|error| { + if error.contains("ambiguous") + || error.contains("identity") + || error.contains("deadline") + || error.contains("fate") + { + ProviderCompositionError::ControllerEffectAmbiguous + } else { + ProviderCompositionError::ControllerEffectRejected + } + }) +} + +/// Launch one deployed controller and commit its running identity back to the +/// ProviderDeployment state machine. +pub async fn launch_deployed_controller( + deployment: &ProviderDeployment, + providers: &ProductionProcessProviders, + process_ref: &ResourceRef, + target_readiness: SchemaFingerprint, + timeout: std::time::Duration, +) -> Result { + let context = deployment + .begin_controller_launch(process_ref, target_readiness.clone()) + .map_err(ProviderCompositionError::ControllerDeployment)?; + let readiness = configuration_digest(&target_readiness); + let result = + launch_target_local_controller(providers, context.resource(), readiness, timeout).await; + match result { + Ok(launch) => { + let identity = identity_commitment(launch.identity); + match deployment.controller_launch_succeeded(process_ref, identity) { + Ok(()) => Ok(launch), + Err(error) => { + let _ = deployment.controller_launch_failed(process_ref, true); + Err(ProviderCompositionError::ControllerDeployment(error)) + } + } + } + Err(error) => { + let _ = deployment.controller_launch_failed( + process_ref, + matches!(error, ProviderCompositionError::ControllerEffectAmbiguous), + ); + Err(error) + } + } +} + +/// Adopt one deployed controller after restart, quarantining an ambiguous +/// child rather than handing it to a replacement controller. +pub async fn adopt_deployed_controller( + deployment: &ProviderDeployment, + providers: &ProductionProcessProviders, + process_ref: &ResourceRef, + target_readiness: SchemaFingerprint, +) -> Result { + let resource = deployment + .controller_process(process_ref) + .map_err(ProviderCompositionError::ControllerDeployment)?; + let adoption = match adopt_target_local_controller( + providers, + &resource, + configuration_digest(&target_readiness), + ) + .await + { + Ok(adoption) => adoption, + Err(error) => { + if matches!(error, ProviderCompositionError::ControllerEffectAmbiguous) { + let _ = deployment.quarantine_controller(process_ref); + } + return Err(error); + } + }; + match adoption { + ProviderAdoption::Adopted(report) => { + match deployment.controller_adopted(process_ref, identity_commitment(report.identity)) { + Ok(()) => Ok(ProviderAdoption::Adopted(report)), + Err(error) => { + let _ = deployment.quarantine_controller(process_ref); + Err(ProviderCompositionError::ControllerDeployment(error)) + } + } + } + ProviderAdoption::Quarantined(report) => { + deployment + .quarantine_controller(process_ref) + .map_err(ProviderCompositionError::ControllerDeployment)?; + Ok(ProviderAdoption::Quarantined(report)) + } + ProviderAdoption::Absent => Ok(ProviderAdoption::Absent), + } +} + +fn configuration_digest(value: &SchemaFingerprint) -> ConfigurationDigest { + let mut digest = Sha256::new(); + digest.update(b"d2bd-controller-readiness-v1\0"); + digest.update(value.as_str().as_bytes()); + ConfigurationDigest::from_bytes(digest.finalize().into()) +} + +fn identity_commitment(identity: d2b_process_conformance::ProcessIdentityDigest) -> [u8; 32] { + let mut digest = Sha256::new(); + digest.update(b"d2bd-controller-process-identity-v1\0"); + digest.update(identity.to_hex().as_bytes()); + digest.finalize().into() +} + /// Result of routing a lifecycle request through the configured Provider /// runtime. #[derive(Debug, PartialEq, Eq)] @@ -612,6 +842,17 @@ pub fn provider_resource_type() -> d2b_contracts_resource::v3::identity::Resourc mod tests { use super::*; use crate::provider_effects::{EffectDispatch, ProviderLifecycleEffectPort}; + use d2b_contracts_provider::v3::{ + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, ResourceApiBinding, + RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, UpgradeDisposition, + UpgradePolicy, + }; + use d2b_contracts_resource::v3::{ + ResourceTypeName, SchemaVersion, + execution_policy::{BoundedToken, ExecutionDomain}, + }; const FINGERPRINT: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000001"; @@ -630,6 +871,107 @@ mod tests { .expect("binding") } + fn controller_manifest() -> ProviderManifest { + let digest = ArtifactDigest::parse(format!("sha256:{}", "a".repeat(64))).unwrap(); + let fingerprint = SchemaFingerprint::parse(format!("sha256:{}", "b".repeat(64))).unwrap(); + let process = ResourceTypeName::parse("Process").unwrap(); + let component = ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [process.clone()], + [BoundedToken::parse("reconcile").unwrap()], + [ExecutionDomain::System], + 8, + digest.clone(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest.clone(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest.clone(), + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap(); + let binding = ResourceApiBinding::new_with_placement( + process, + SchemaVersion::new(1, 0).unwrap(), + fingerprint.clone(), + SchemaVersion::new(1, 0).unwrap(), + fingerprint.clone(), + Default::default(), + None, + None, + d2b_contracts_resource::v3::PlacementAnchor::ExecutionRef, + ) + .unwrap(); + let trust = TrustEvidence { + publisher: BoundedToken::parse("trusted").unwrap(), + root_epoch: 1, + publisher_trusted: true, + signature: SignatureState::Valid, + revocation: RevocationState::Clear, + emergency_deny: false, + provenance: PolicyEvaluation::Accepted, + sbom: PolicyEvaluation::Accepted, + license: PolicyEvaluation::Accepted, + vulnerability: PolicyEvaluation::Accepted, + conformance: PolicyEvaluation::Accepted, + support_channel: BoundedToken::parse("stable").unwrap(), + }; + ProviderManifest::new( + d2b_contracts_resource::v3::ArtifactId::parse("provider-runtime").unwrap(), + ArtifactDigestSet { + package: digest.clone(), + executable: digest.clone(), + manifest: digest.clone(), + config: digest.clone(), + schema: digest.clone(), + service: digest.clone(), + }, + trust, + CompatibilityRange { + api_major: 3, + api_minor: 0, + descriptor_fingerprint: fingerprint, + state_schema_version: SchemaVersion::new(1, 0).unwrap(), + }, + [component], + [binding], + [], + UpgradePolicy { + drain_before_upgrade: true, + max_automatic_disposition: UpgradeDisposition::InPlace, + preserves_durable_state: true, + }, + ) + .unwrap() + .with_target_runtime_artifacts([ + TargetRuntimeArtifacts::new(ControllerTargetKind::Host, digest.clone(), digest.clone()) + .unwrap(), + TargetRuntimeArtifacts::new(ControllerTargetKind::Guest, digest.clone(), digest) + .unwrap(), + ]) + .unwrap() + } + struct RecordingEffect; impl ProviderLifecycleEffectPort for RecordingEffect { @@ -718,4 +1060,37 @@ mod tests { Err(ProviderEffectError::CallerRoleDenied) ); } + + #[test] + fn signed_manifest_deployment_creates_one_target_local_controller_process() { + let deployment = ProviderDeployment::new( + DaemonMode::Guest, + d2bd_runtime::target_runtime::AdmissionLimits::guest_default(), + ) + .expect("deployment"); + let resources = deploy_target_local_controllers( + &deployment, + zone(), + ResourceRef::parse("Provider/provider-runtime").unwrap(), + &controller_manifest(), + ResourceGeneration::new(1).unwrap(), + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + ZoneRevision::new(5), + ResourceRef::parse("Guest/workload").unwrap(), + ResourceRef::parse("Provider/system-systemd").unwrap(), + true, + ) + .expect("target-local controller deployment"); + assert_eq!(resources.len(), 1); + assert_eq!( + resources[0].process_spec().execution().process_class(), + d2b_contracts_resource::v3::process::ProcessClass::Controller + ); + assert_eq!( + deployment.controller_phase(resources[0].process_ref()), + Some(d2bd_runtime::target_runtime::ControllerProcessPhase::Pending) + ); + } } diff --git a/packages/d2bd/tests/zone_provider_acceptance.rs b/packages/d2bd/tests/zone_provider_acceptance.rs index 2f1b1ca5f3..53d677ba3e 100644 --- a/packages/d2bd/tests/zone_provider_acceptance.rs +++ b/packages/d2bd/tests/zone_provider_acceptance.rs @@ -14,15 +14,21 @@ use std::{ }; use async_trait::async_trait; +use d2b_contracts_provider::v3::{ + ArtifactDigest, BinaryRef, ComponentDescriptor, ComponentExecution, ComponentTargetCapability, + ComponentType, ControllerInstanceScope, ControllerTargetKind, EffectPortClass, +}; use d2b_contracts_resource::v3::{ - ResourceBundleGenerationId, - ResourceGeneration, - ResourceRef, - ResourceUid, + ControllerGeneration, ResourceBundleGenerationId, ResourceGeneration, ResourceRef, + ResourceTypeName, ResourceUid, SchemaFingerprint, ZoneId, ZoneRevision, execution_policy::BoundedToken, guest::GuestSpec, - network::{AttachmentGenerationFence, AttachmentHandle, DhcpSpec, DnsSpec, Ipv4Cidr, IsolationSpec, MdnsSpec, NetworkSpec, RoutingSpec}, - process::ProcessSpec, + identity::ReconnectGeneration, + network::{ + AttachmentGenerationFence, AttachmentHandle, DhcpSpec, DnsSpec, Ipv4Cidr, IsolationSpec, + MdnsSpec, NetworkSpec, RoutingSpec, + }, + process::{ProcessClass, ProcessSpec}, volume::{EntryType, VolumeSpec}, }; use d2b_provider_device_tpm::{ @@ -53,6 +59,11 @@ use d2bd_runtime::resource_operator_activation::{ Wave6BoundaryError, Wave6Dependencies, Wave6ProviderBoundary, Wave6ReconcileResult, Wave6Resource, Wave6ResourceSet, }; +use d2bd_runtime::target_runtime::{ + AdmissionLimits, ControllerAssignmentRequest, ControllerChildObservation, + ControllerProcessPhase, ControllerResourceVerb, ControllerSessionBinding, DaemonMode, + ProviderDeployment, +}; use serde_json::json; fn block_on(future: F) -> F::Output { @@ -1359,3 +1370,230 @@ impl Wave6ProviderBoundary for Wave6RealBoundary { Ok(()) } } + +fn u4_controller_descriptor() -> ComponentDescriptor { + let digest = ArtifactDigest::parse(format!("sha256:{}", "a".repeat(64))).unwrap(); + ComponentDescriptor::new( + BoundedToken::parse("process-controller").unwrap(), + ComponentType::Controller, + [ResourceTypeName::parse("Process").unwrap()], + [BoundedToken::parse("reconcile").unwrap()], + [d2b_contracts_resource::v3::execution_policy::ExecutionDomain::System], + 8, + digest.clone(), + [], + false, + ) + .unwrap() + .with_execution(ComponentExecution::Launchable { + binary_ref: BinaryRef::parse("process-controller").unwrap(), + }) + .with_controller_placement( + ControllerInstanceScope::PerResourceTarget, + [ControllerTargetKind::Host, ControllerTargetKind::Guest], + ) + .unwrap() + .with_target_capabilities([ + ComponentTargetCapability::new( + ControllerTargetKind::Host, + digest.clone(), + [EffectPortClass::Process], + ) + .unwrap(), + ComponentTargetCapability::new( + ControllerTargetKind::Guest, + digest, + [EffectPortClass::Process], + ) + .unwrap(), + ]) + .unwrap() +} + +#[test] +fn controller_process_acceptance_fences_assignment_and_cleanup() { + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()).unwrap(); + let provider = ResourceRef::parse("Provider/runtime").unwrap(); + let target = ResourceRef::parse("Guest/workload").unwrap(); + let process = deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + provider.clone(), + &u4_controller_descriptor(), + ResourceGeneration::new(1).unwrap(), + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + ZoneRevision::new(5), + target.clone(), + ResourceRef::parse("Provider/system-systemd").unwrap(), + true, + ) + .unwrap(); + assert_eq!( + process.process_spec().execution().process_class(), + ProcessClass::Controller + ); + assert_eq!( + process.finalizer(), + "provider-controller.d2bus.org/children" + ); + assert!( + process + .owned_resource_types() + .contains(&ResourceTypeName::parse("Process").expect("Process type")) + ); + + let readiness = SchemaFingerprint::parse(format!("sha256:{}", "b".repeat(64))).unwrap(); + deployment + .begin_controller_launch(process.process_ref(), readiness.clone()) + .unwrap(); + assert!(matches!( + deployment.admit_controller_assignment(ControllerAssignmentRequest::new( + process.process_ref().clone(), + ResourceRef::parse("Process/child").unwrap(), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ResourceGeneration::new(1).unwrap(), + ZoneRevision::new(6), + provider.clone(), + target.clone(), + ReconnectGeneration::new(7).unwrap(), + )), + Err(d2bd_runtime::target_runtime::DeploymentError::ControllerNotReady) + )); + deployment + .controller_launch_succeeded(process.process_ref(), [1; 32]) + .unwrap(); + let session = deployment + .admit_controller_session(ControllerSessionBinding::new( + process.process_ref().clone(), + process.zone().clone(), + provider.clone(), + target.clone(), + process.provider_generation(), + process.controller_generation(), + process.target_session_generation(), + ReconnectGeneration::new(7).unwrap(), + readiness, + )) + .unwrap(); + let assignment = deployment + .admit_controller_assignment(ControllerAssignmentRequest::new( + process.process_ref().clone(), + ResourceRef::parse("Process/child").unwrap(), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ResourceGeneration::new(1).unwrap(), + ZoneRevision::new(6), + provider, + target, + session.generation(), + )) + .unwrap(); + assert!(assignment.is_active()); + assert!( + assignment + .resource_types() + .contains(&ResourceTypeName::parse("Process").expect("Process type")) + ); + assert!(assignment.allows(ControllerResourceVerb::UpdateStatus)); + assert!(assignment.allows(ControllerResourceVerb::Watch)); + drop(session); + assert!(!assignment.is_active()); + assert_eq!( + deployment.controller_phase(process.process_ref()), + Some(ControllerProcessPhase::Revoked) + ); + + deployment + .record_controller_child( + process.process_ref(), + ResourceRef::parse("Process/child").unwrap(), + ResourceUid::parse("223e4567-e89b-42d3-a456-426614174001").unwrap(), + ) + .unwrap(); + let child_uid = ResourceUid::parse("223e4567-e89b-42d3-a456-426614174001").unwrap(); + deployment + .adopt_controller_children( + process.process_ref(), + [ControllerChildObservation::verified( + ResourceRef::parse("Process/child").unwrap(), + child_uid.clone(), + )], + ) + .unwrap(); + deployment + .remove_controller_child( + process.process_ref(), + ResourceRef::parse("Process/child").unwrap(), + &child_uid, + ) + .unwrap(); + deployment + .prepare_controller_cleanup(process.process_ref(), process.process_ref()) + .unwrap(); + assert!( + deployment + .controller_finalizer_held(process.process_ref()) + .unwrap() + ); + deployment + .complete_controller_cleanup(process.process_ref(), process.process_ref()) + .unwrap(); + assert!( + !deployment + .controller_finalizer_held(process.process_ref()) + .unwrap() + ); +} + +#[test] +fn host_and_guest_controller_process_resources_keep_one_process_shape() { + let descriptor = u4_controller_descriptor(); + let host_deployment = + ProviderDeployment::new(DaemonMode::Host, AdmissionLimits::host_default()).unwrap(); + let guest_deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()).unwrap(); + let provider = ResourceRef::parse("Provider/runtime").unwrap(); + let process_provider = ResourceRef::parse("Provider/system-systemd").unwrap(); + let host = host_deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + provider.clone(), + &descriptor, + ResourceGeneration::new(1).unwrap(), + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + ZoneRevision::new(5), + ResourceRef::parse("Host/host-system").unwrap(), + process_provider.clone(), + true, + ) + .unwrap(); + let guest = guest_deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + provider, + &descriptor, + ResourceGeneration::new(1).unwrap(), + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(3).unwrap(), + ReconnectGeneration::new(4).unwrap(), + ZoneRevision::new(5), + ResourceRef::parse("Guest/workload").unwrap(), + process_provider, + true, + ) + .unwrap(); + assert_ne!(host.process_ref(), guest.process_ref()); + assert_eq!( + host.process_spec().execution().process_class(), + guest.process_spec().execution().process_class() + ); + assert_eq!(host.process_provider_ref(), guest.process_provider_ref()); + assert_eq!( + host.required_effect_classes(), + guest.required_effect_classes() + ); +} From 6144833b184b8d9a6979e66c3051d48210acb13c Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 21:39:55 -0700 Subject: [PATCH 08/56] fix(runtime): revoke target controller sessions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/d2bd-runtime/src/target_runtime.rs | 129 +++++++++++++++++--- 1 file changed, 111 insertions(+), 18 deletions(-) diff --git a/packages/d2bd-runtime/src/target_runtime.rs b/packages/d2bd-runtime/src/target_runtime.rs index abfbd67d23..50b8654eba 100644 --- a/packages/d2bd-runtime/src/target_runtime.rs +++ b/packages/d2bd-runtime/src/target_runtime.rs @@ -1155,7 +1155,10 @@ impl ProviderDeployment { }) } - /// Revoke every assignment bound to one ComponentSession generation. + /// Revoke every assignment bound to one target ComponentSession generation. + /// + /// A controller's authenticated session has a separate generation, so + /// controller records must be selected by their target generation. pub fn revoke_session(&self, session_generation: u64) -> Result { if session_generation == 0 { return Err(DeploymentError::GenerationZero); @@ -1188,19 +1191,21 @@ impl ProviderDeployment { let mut record = record .lock() .map_err(|_| DeploymentError::StateUnavailable)?; - let matches = record - .ready_session - .as_ref() - .is_some_and(|binding| binding.session_generation().get() == session_generation); + let matches = record.resource.target_session_generation().get() == session_generation; if matches { - count = count.saturating_add(revoke_record_assignments( + count = count.saturating_add(revoke_record_assignments_all( &self.controller_assignments, &mut record, - ReconnectGeneration::new(session_generation) - .map_err(|_| DeploymentError::GenerationZero)?, - )?); + )); record.ready_session = None; - if record.phase != ControllerProcessPhase::Released { + if matches!( + record.phase, + ControllerProcessPhase::Pending + | ControllerProcessPhase::Launching + | ControllerProcessPhase::Running + | ControllerProcessPhase::Ready + | ControllerProcessPhase::Assigned + ) { record.phase = ControllerProcessPhase::Revoked; } } @@ -1671,11 +1676,14 @@ impl ProviderDeployment { { return Err(DeploymentError::ControllerSessionMismatch); } - if record.phase == ControllerProcessPhase::Released - || record.phase == ControllerProcessPhase::Quarantined - || record.phase == ControllerProcessPhase::Launching - || record.phase == ControllerProcessPhase::Pending - { + if matches!( + record.phase, + ControllerProcessPhase::Released + | ControllerProcessPhase::Quarantined + | ControllerProcessPhase::Draining + | ControllerProcessPhase::Launching + | ControllerProcessPhase::Pending + ) { return Err(DeploymentError::ControllerNotReady); } if let Some(previous) = record.ready_session.as_ref() { @@ -2531,9 +2539,20 @@ mod tests { assert_eq!(assignment.identity().resource_generation().get(), 1); assert_eq!(assignment.identity().assignment_epoch(), 1); assert_eq!(deployment.active_controller_assignments().unwrap(), 1); - drop(session); + assert_eq!( + deployment + .revoke_session(process.target_session_generation().get()) + .expect("revoke parent session"), + 1 + ); + assert!(!session.is_active()); assert!(!assignment.is_active()); assert_eq!(deployment.active_controller_assignments().unwrap(), 0); + assert_eq!( + deployment.controller_phase(process.process_ref()), + Some(ControllerProcessPhase::Revoked) + ); + drop(session); let reconnect = deployment .admit_controller_session(ControllerSessionBinding::new( process.process_ref().clone(), @@ -2569,6 +2588,58 @@ mod tests { assert!(!replacement.is_active()); } + #[test] + fn parent_session_revoke_revokes_running_controller_without_ready_session() { + use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ZoneRevision, identity::ReconnectGeneration, + }; + + let deployment = + ProviderDeployment::new(DaemonMode::Guest, AdmissionLimits::guest_default()) + .expect("deployment"); + let process = deployment + .create_controller_process( + ZoneId::parse("work").unwrap(), + resource("Provider", "runtime"), + &signed_controller_descriptor(), + ResourceGeneration::new(3).unwrap(), + ResourceGeneration::new(7).unwrap(), + ControllerGeneration::new(4).unwrap(), + ReconnectGeneration::new(2).unwrap(), + ZoneRevision::new(11), + resource("Guest", "workload"), + resource("Provider", "system-systemd"), + true, + ) + .expect("controller process"); + let readiness = d2b_contracts_resource::v3::SchemaFingerprint::parse(format!( + "sha256:{}", + "b".repeat(64) + )) + .unwrap(); + deployment + .begin_controller_launch(process.process_ref(), readiness) + .expect("launch admission"); + deployment + .controller_launch_succeeded(process.process_ref(), [9; 32]) + .expect("launch success"); + assert_eq!( + deployment.controller_phase(process.process_ref()), + Some(ControllerProcessPhase::Running) + ); + + assert_eq!( + deployment + .revoke_session(process.target_session_generation().get()) + .expect("revoke parent session"), + 0 + ); + assert_eq!( + deployment.controller_phase(process.process_ref()), + Some(ControllerProcessPhase::Revoked) + ); + } + #[test] fn controller_cleanup_requires_verified_children_and_exact_repair_owner() { use d2b_contracts_resource::v3::{ @@ -2607,7 +2678,7 @@ mod tests { deployment .controller_launch_succeeded(process.process_ref(), [9; 32]) .unwrap(); - let session = deployment + let _session = deployment .admit_controller_session(ControllerSessionBinding::new( process.process_ref().clone(), process.zone().clone(), @@ -2633,7 +2704,7 @@ mod tests { ) .unwrap(); deployment - .revoke_session(session.generation().get()) + .revoke_session(process.target_session_generation().get()) .expect("revoke session"); assert!( deployment @@ -2659,6 +2730,28 @@ mod tests { deployment .prepare_controller_cleanup(process.process_ref(), process.process_ref()) .expect("cleanup owner"); + assert!(matches!( + deployment.admit_controller_session(ControllerSessionBinding::new( + process.process_ref().clone(), + process.zone().clone(), + process.provider_ref().clone(), + process.target().clone(), + process.provider_generation(), + process.controller_generation(), + process.target_session_generation(), + ReconnectGeneration::new(6).unwrap(), + d2b_contracts_resource::v3::SchemaFingerprint::parse(format!( + "sha256:{}", + "b".repeat(64) + )) + .unwrap(), + )), + Err(DeploymentError::ControllerNotReady) + )); + assert_eq!( + deployment.controller_phase(process.process_ref()), + Some(ControllerProcessPhase::Draining) + ); deployment .complete_controller_cleanup(process.process_ref(), process.process_ref()) .expect("cleanup complete"); From 5613f2da6f742715a68bd6f3e57d963db5765f0b Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Fri, 21 Aug 2026 22:49:12 -0700 Subject: [PATCH 09/56] fix(provider): refresh acceptance controller manifest Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- changelog.d/phase-v3-guest-u4.md | 1 + tests/fixtures/provider-acceptance/provider-manifest.json | 2 +- tests/fixtures/provider-acceptance/provider-manifest.sig.b64 | 2 +- tests/host-integration/resource-operator-activation.nix | 2 +- .../runtime-cloud-hypervisor-guest-preflight.nix | 2 +- tests/host-integration/unsafe-local-helper.nix | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/changelog.d/phase-v3-guest-u4.md b/changelog.d/phase-v3-guest-u4.md index 6f951d524f..9fba32c581 100644 --- a/changelog.d/phase-v3-guest-u4.md +++ b/changelog.d/phase-v3-guest-u4.md @@ -1,3 +1,4 @@ ### Fixed - Process Provider controllers now validate launch tickets and exact process identity before readiness, adoption, and stop effects on Host and Guest targets. +- Regenerated the signed acceptance Provider manifest for the target-local controller contract. diff --git a/tests/fixtures/provider-acceptance/provider-manifest.json b/tests/fixtures/provider-acceptance/provider-manifest.json index f37b55b7ea..b7828d1069 100644 --- a/tests/fixtures/provider-acceptance/provider-manifest.json +++ b/tests/fixtures/provider-acceptance/provider-manifest.json @@ -1 +1 @@ -{"apiBindings":[],"artifactId":"acceptance-provider","compatibility":{"apiMajor":3,"apiMinor":0,"descriptorFingerprint":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","stateSchemaVersion":"1.0"},"components":[{"allowedDomains":["system"],"binaryRef":"acceptance-controller","cardinality":1,"componentId":"acceptance-controller","componentType":"controller","configDigest":"sha256:ccb5a9d66e068ea8f4e205788589675a48e9e3754a840d8ac10120d14238e914","declaresStateVolume":false,"dependencies":[],"exportedMethods":[],"exportedResourceTypes":["Device","Guest","Network","Volume"],"stateNamespaces":[]}],"digests":{"config":"sha256:ccb5a9d66e068ea8f4e205788589675a48e9e3754a840d8ac10120d14238e914","executable":"sha256:f84125779653dba770042fd2af2bd01299b05ae892c039c497e6b5ce45029d9c","manifest":"sha256:3333333333333333333333333333333333333333333333333333333333333333","package":"sha256:1111111111111111111111111111111111111111111111111111111111111111","schema":"sha256:5555555555555555555555555555555555555555555555555555555555555555","service":"sha256:6666666666666666666666666666666666666666666666666666666666666666"},"projectionFactories":[],"trust":{"conformance":"accepted","emergencyDeny":false,"license":"accepted","provenance":"accepted","publisher":"d2b-acceptance","publisherTrusted":true,"revocation":"clear","rootEpoch":1,"sbom":"accepted","signature":"valid","supportChannel":"stable","vulnerability":"accepted"},"upgradePolicy":{"drainBeforeUpgrade":true,"maxAutomaticDisposition":"in-place","preservesDurableState":true}} \ No newline at end of file +{"apiBindings":[{"baseSpecFingerprint":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseSpecVersion":"1.0","baseStatusFingerprint":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","baseStatusVersion":"1.0","capabilityMatrix":{},"placementAnchor":"execution-ref","resourceType":"Device"},{"baseSpecFingerprint":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseSpecVersion":"1.0","baseStatusFingerprint":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","baseStatusVersion":"1.0","capabilityMatrix":{},"placementAnchor":"execution-ref","resourceType":"Guest"},{"baseSpecFingerprint":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseSpecVersion":"1.0","baseStatusFingerprint":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","baseStatusVersion":"1.0","capabilityMatrix":{},"placementAnchor":"execution-ref","resourceType":"Network"},{"baseSpecFingerprint":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseSpecVersion":"1.0","baseStatusFingerprint":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","baseStatusVersion":"1.0","capabilityMatrix":{},"placementAnchor":"execution-ref","resourceType":"Volume"}],"artifactId":"acceptance-provider","compatibility":{"apiMajor":3,"apiMinor":0,"descriptorFingerprint":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","stateSchemaVersion":"1.0"},"components":[{"allowedDomains":["system"],"binaryRef":"acceptance-controller","cardinality":1,"componentId":"acceptance-controller","componentType":"controller","configDigest":"sha256:ccb5a9d66e068ea8f4e205788589675a48e9e3754a840d8ac10120d14238e914","declaresStateVolume":false,"dependencies":[],"exportedMethods":[],"exportedResourceTypes":["Device","Guest","Network","Volume"],"instanceScope":"per-resource-target","stateNamespaces":[],"supportedTargetKinds":["host","guest"],"targetCapabilities":[{"artifactDigest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","requiredEffectClasses":["process"],"targetKind":"host"},{"artifactDigest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","requiredEffectClasses":["process"],"targetKind":"guest"}]}],"digests":{"config":"sha256:ccb5a9d66e068ea8f4e205788589675a48e9e3754a840d8ac10120d14238e914","executable":"sha256:f84125779653dba770042fd2af2bd01299b05ae892c039c497e6b5ce45029d9c","manifest":"sha256:3333333333333333333333333333333333333333333333333333333333333333","package":"sha256:1111111111111111111111111111111111111111111111111111111111111111","schema":"sha256:5555555555555555555555555555555555555555555555555555555555555555","service":"sha256:6666666666666666666666666666666666666666666666666666666666666666"},"projectionFactories":[],"runtimeArtifacts":[{"brokerDigest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","d2bdDigest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","targetKind":"host"},{"brokerDigest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","d2bdDigest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","targetKind":"guest"}],"trust":{"conformance":"accepted","emergencyDeny":false,"license":"accepted","provenance":"accepted","publisher":"d2b-acceptance","publisherTrusted":true,"revocation":"clear","rootEpoch":1,"sbom":"accepted","signature":"valid","supportChannel":"stable","vulnerability":"accepted"},"upgradePolicy":{"drainBeforeUpgrade":true,"maxAutomaticDisposition":"in-place","preservesDurableState":true}} \ No newline at end of file diff --git a/tests/fixtures/provider-acceptance/provider-manifest.sig.b64 b/tests/fixtures/provider-acceptance/provider-manifest.sig.b64 index 46fff3b75f..af4cc42634 100644 --- a/tests/fixtures/provider-acceptance/provider-manifest.sig.b64 +++ b/tests/fixtures/provider-acceptance/provider-manifest.sig.b64 @@ -1 +1 @@ -jYHKdq6VAj7gR6ufoKKSKcPUzS85LePG9Syn7hXaNP4JaM49wn98MsJMDC0PFW9x6PI9yRHc+dttFWPSuPJUBw== \ No newline at end of file +X2eCQ46TNSYQuqbiVsxL4W+3yKthuRbMLZsZ1ea+unH0t9jjNPyr/E+1RbveSgDJFqgTJh+Q543PlmcaJH4wAQ== \ No newline at end of file diff --git a/tests/host-integration/resource-operator-activation.nix b/tests/host-integration/resource-operator-activation.nix index ee58c399d0..d243ba82aa 100644 --- a/tests/host-integration/resource-operator-activation.nix +++ b/tests/host-integration/resource-operator-activation.nix @@ -53,7 +53,7 @@ let publisher = "d2b-acceptance"; packageDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"; executableDigest = "sha256:f84125779653dba770042fd2af2bd01299b05ae892c039c497e6b5ce45029d9c"; - manifestDigest = "sha256:5f8d852ba3ecd89883afdcf2330f3f752eb1d68a572698035177bcd4b8595e6c"; + manifestDigest = "sha256:7a3ba1efbd15c544edf574087aed31053c69fb758108177bf028d3f7d62c0635"; componentDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; descriptorDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; configDigest = "sha256:ccb5a9d66e068ea8f4e205788589675a48e9e3754a840d8ac10120d14238e914"; diff --git a/tests/host-integration/runtime-cloud-hypervisor-guest-preflight.nix b/tests/host-integration/runtime-cloud-hypervisor-guest-preflight.nix index 20547639e0..769ffbe79e 100644 --- a/tests/host-integration/runtime-cloud-hypervisor-guest-preflight.nix +++ b/tests/host-integration/runtime-cloud-hypervisor-guest-preflight.nix @@ -53,7 +53,7 @@ let publisher = "d2b-acceptance"; packageDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"; executableDigest = "sha256:f84125779653dba770042fd2af2bd01299b05ae892c039c497e6b5ce45029d9c"; - manifestDigest = "sha256:5f8d852ba3ecd89883afdcf2330f3f752eb1d68a572698035177bcd4b8595e6c"; + manifestDigest = "sha256:7a3ba1efbd15c544edf574087aed31053c69fb758108177bf028d3f7d62c0635"; componentDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; descriptorDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; configDigest = "sha256:ccb5a9d66e068ea8f4e205788589675a48e9e3754a840d8ac10120d14238e914"; diff --git a/tests/host-integration/unsafe-local-helper.nix b/tests/host-integration/unsafe-local-helper.nix index 3d9fdf8ab3..b0205e2210 100644 --- a/tests/host-integration/unsafe-local-helper.nix +++ b/tests/host-integration/unsafe-local-helper.nix @@ -46,7 +46,7 @@ let publisher = "d2b-acceptance"; packageDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"; executableDigest = "sha256:f84125779653dba770042fd2af2bd01299b05ae892c039c497e6b5ce45029d9c"; - manifestDigest = "sha256:5f8d852ba3ecd89883afdcf2330f3f752eb1d68a572698035177bcd4b8595e6c"; + manifestDigest = "sha256:7a3ba1efbd15c544edf574087aed31053c69fb758108177bf028d3f7d62c0635"; componentDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; descriptorDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; configDigest = "sha256:ccb5a9d66e068ea8f4e205788589675a48e9e3754a840d8ac10120d14238e914"; From a094f0b6a960bcdd52752402bbe44ff176136ebc Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 08:42:51 -0700 Subject: [PATCH 10/56] exec: migrate guest sessions to resources Route guest exec and shell lifecycle through Process-family resources and authenticated named streams while preserving owner, auth, and disconnect boundaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 5 + .../process-shell-component-session.md | 6 + docs/reference/daemon-api.md | 120 +- packages/d2b-bus/src/router.rs | 42 +- .../d2b-contracts-control/src/public_wire.rs | 333 +- .../d2b-provider-shell-terminal/src/lib.rs | 3 +- .../src/resources/session.rs | 47 + .../src/service/controller.rs | 7 + .../src/service/mod.rs | 3 +- .../src/service/supervisor.rs | 345 +- .../tests/controller_reconcile.rs | 112 +- .../tests/supervisor_runtime.rs | 5 + .../d2b-provider-system-systemd/BUILD.bazel | 2 + .../d2b-provider-system-systemd/Cargo.toml | 2 + .../src/guest_exec.rs | 249 +- .../src/lifecycle.rs | 37 +- .../tests/lifecycle.rs | 26 +- packages/d2b-resource-client/BUILD.bazel | 1 + packages/d2b-resource-client/Cargo.toml | 2 + .../d2b-resource-client/src/process_attach.rs | 94 + packages/d2b-session/src/admission.rs | 10 + packages/d2b/src/context.rs | 387 +- packages/d2b/src/exec_client.rs | 233 +- packages/d2b/src/legacy.rs | 398 +- packages/d2bd-runtime/src/exec_detached.rs | 277 +- packages/d2bd-runtime/src/exec_session.rs | 1208 ++++- .../d2bd-runtime/src/exec_session_real.rs | 435 +- .../d2bd-runtime/src/guest_control_bridge.rs | 2 +- packages/d2bd-runtime/src/shell_backend.rs | 268 ++ packages/d2bd-runtime/src/terminal_session.rs | 7 + packages/d2bd/BUILD.bazel | 1 + packages/d2bd/Cargo.toml | 1 + packages/d2bd/src/composition.rs | 4120 +++++++++++++++-- packages/d2bd/src/interaction_composition.rs | 547 ++- packages/d2bd/src/resource_runtime.rs | 9 + 35 files changed, 8424 insertions(+), 920 deletions(-) create mode 100644 changelog.d/process-shell-component-session.md diff --git a/Cargo.lock b/Cargo.lock index cecdb991f2..6f28862c6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1667,11 +1667,13 @@ dependencies = [ name = "d2b-provider-system-systemd" version = "0.0.0-bootstrap" dependencies = [ + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-process-conformance", "d2b-provider-toolkit", "serde", + "serde_json", "tokio", ] @@ -1795,9 +1797,11 @@ name = "d2b-resource-client" version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core-controller", + "serde_json", "tokio", ] @@ -1974,6 +1978,7 @@ dependencies = [ "d2b-provider-notification-desktop", "d2b-provider-runtime-azure-container-apps", "d2b-provider-runtime-cloud-hypervisor", + "d2b-provider-shell-terminal", "d2b-provider-supervisor", "d2b-provider-system-core", "d2b-provider-system-minijail", diff --git a/changelog.d/process-shell-component-session.md b/changelog.d/process-shell-component-session.md new file mode 100644 index 0000000000..0a5c28dc65 --- /dev/null +++ b/changelog.d/process-shell-component-session.md @@ -0,0 +1,6 @@ +### Changed + +- Route guest execution and persistent shell lifecycle through Process-family resource intents and authenticated ComponentSession named streams while retaining bounded authorization, retention, and disconnect handling; unavailable target-session routes fail closed without a legacy fallback. +- Demultiplex concurrent named-stream responses, cancel/reset on owner disconnect, and use one correlated JSON frame codec across CLI, ResourceClient, Providers, and daemon shell owners. +- Reject the retired public `exec`/guest shell bridge in production composition and record target-local supervisor Process ownership for ShellSession resources. +- Resolve enrolled target-local ComponentSessions for real Process and ShellSession routes, and persist shell supervisor Process ownership through the daemon Resource API with exact owner and finalizer fencing. diff --git a/docs/reference/daemon-api.md b/docs/reference/daemon-api.md index c6f512d963..dc0f86af97 100644 --- a/docs/reference/daemon-api.md +++ b/docs/reference/daemon-api.md @@ -268,16 +268,17 @@ host reboot. | `UsbipUnbindCliRequest` | struct | [`UsbipUnbindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L572) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | | `StoreVerifyRequest` | struct | [`StoreVerifyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L581) | struct { `vm`: `String`; `repair`: `bool` } | | `ReadGuestConfigRequest` | struct | [`ReadGuestConfigRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L594) | struct { `vm`: `String` } | -| `MigrateRequest` | struct | [`MigrateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2024) | struct { `flags`: `MutationFlags` } | -| `HostPrepareRequest` | struct | [`HostPrepareRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2031) | struct { `flags`: `MutationFlags` } | -| `HostDestroyRequest` | struct | [`HostDestroyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2038) | struct { `flags`: `MutationFlags` } | -| `HostInstallRequest` | struct | [`HostInstallRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2045) | struct { `flags`: `MutationFlags`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool` } | -| `HostReconcileRequest` | struct | [`HostReconcileRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2063) | struct { `flags`: `MutationFlags`; `network`: `bool` } | -| `HostCutoverRequest` | struct | [`HostCutoverRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2074) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `candidate_id`: `Option`; `revision_plan_id`: `Option`; `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option`; `preview_digest`: `Option`; `recovery_digest`: `Option`; `operator_id`: `Option`; `consent_digest`: `Option`; `consent_json`: `Option`; `destructive_consent_digest`: `Option`; `destructive_consent_json`: `Option`; `destroy_durable_volumes`: `Option`; `recovery_attestation_json`: `Option`; `host_digest`: `Option`; `fresh_consent_digest`: `Option`; `reason`: `Option`; `reset_scope`: `Option`; `target`: `Option`; `zone`: `Option` } | -| `UsbSecurityKeyStatusRequest` | struct | [`UsbSecurityKeyStatusRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L3892) | empty struct | -| `UsbSecurityKeySessionsRequest` | struct | [`UsbSecurityKeySessionsRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L3978) | empty struct | -| `UsbSecurityKeyCancelRequest` | struct | [`UsbSecurityKeyCancelRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4032) | struct { `session_id`: `Option`; `current`: `bool` } | -| `UsbSecurityKeyTestRequest` | struct | [`UsbSecurityKeyTestRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4057) | struct { `vm`: `String` } | +| `NamedProcessStreamRequest` | enum | [`NamedProcessStreamRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L1273) | `Stdin` - struct { `offset`: `u64`; `chunk_base64`: `String`; `eof`: `bool` }; `Read` - struct { `stream`: `ExecStream`; `offset`: `u64`; `max_len`: `u64`; `wait`: `bool`; `timeout_ms`: `u64` }; `Signal` - struct { `control_seq`: `u64`; `signo`: `u32` }; `Resize` - struct { `control_seq`: `u64`; `rows`: `u32`; `cols`: `u32` }; `CloseStdin` - struct { `offset`: `u64` }; `Cancel`; `Close`; `Wait` - struct { `timeout_ms`: `u64` } | +| `MigrateRequest` | struct | [`MigrateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2276) | struct { `flags`: `MutationFlags` } | +| `HostPrepareRequest` | struct | [`HostPrepareRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2283) | struct { `flags`: `MutationFlags` } | +| `HostDestroyRequest` | struct | [`HostDestroyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2290) | struct { `flags`: `MutationFlags` } | +| `HostInstallRequest` | struct | [`HostInstallRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2297) | struct { `flags`: `MutationFlags`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool` } | +| `HostReconcileRequest` | struct | [`HostReconcileRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2315) | struct { `flags`: `MutationFlags`; `network`: `bool` } | +| `HostCutoverRequest` | struct | [`HostCutoverRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2326) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `candidate_id`: `Option`; `revision_plan_id`: `Option`; `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option`; `preview_digest`: `Option`; `recovery_digest`: `Option`; `operator_id`: `Option`; `consent_digest`: `Option`; `consent_json`: `Option`; `destructive_consent_digest`: `Option`; `destructive_consent_json`: `Option`; `destroy_durable_volumes`: `Option`; `recovery_attestation_json`: `Option`; `host_digest`: `Option`; `fresh_consent_digest`: `Option`; `reason`: `Option`; `reset_scope`: `Option`; `target`: `Option`; `zone`: `Option` } | +| `UsbSecurityKeyStatusRequest` | struct | [`UsbSecurityKeyStatusRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4223) | empty struct | +| `UsbSecurityKeySessionsRequest` | struct | [`UsbSecurityKeySessionsRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4309) | empty struct | +| `UsbSecurityKeyCancelRequest` | struct | [`UsbSecurityKeyCancelRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4363) | struct { `session_id`: `Option`; `current`: `bool` } | +| `UsbSecurityKeyTestRequest` | struct | [`UsbSecurityKeyTestRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4388) | struct { `vm`: `String` } | ### Broker socket request types @@ -402,23 +403,24 @@ see the auto-generated tables above for the committed Rust variants. | `GatewayDisplayOpResponse` | enum | [`GatewayDisplayOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L370) | `Start` - (GatewayDisplayStartResult); `Stop` - (GatewayDisplayStopResult); `Open` - (GatewayDisplayOpenResult); `Close` - (GatewayDisplayCloseResult); `List` - (GatewayDisplayListResult); `ListDetailed` - (GatewayDisplayListDetailedResult) | | `ReadGuestConfigResponse` | struct | [`ReadGuestConfigResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L606) | struct { `content_base64`: `String` } | | `ExecOpResponse` | enum | [`ExecOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1246) | `Start` - (ExecStartResult); `DetachedCreate` - (ExecDetachedCreateResult); `WriteStdin` - (ExecWriteStdinResult); `ReadOutput` - (ExecReadOutputResult); `Signal` - (ExecControlResult); `Resize` - (ExecControlResult); `Wait` - (ExecWaitResult); `Close` - (ExecCloseResult); `List` - (ExecDetachedListResult); `Logs` - (ExecDetachedLogsResult); `Status` - (ExecDetachedStatusResult); `Kill` - (ExecDetachedKillResult) | -| `ConsoleOpResponse` | enum | [`ConsoleOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1808) | `Attach` - (ConsoleAttachResult); `WriteStdin` - (ConsoleControlResult); `ReadOutput` - (ConsoleReadOutputResult); `Resize` - (ConsoleControlResult); `Wait` - (ConsoleWaitResult); `Close` - (ConsoleCloseResult) | -| `AudioOpResponse` | enum | [`AudioOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2014) | `Status` - (AudioStatusResult); `SetVolume` - (AudioSetResult); `Mute` - (AudioSetResult) | -| `HostCutoverResponse` | struct | [`HostCutoverResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2180) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `state`: `String`; `phase`: `u8`; `preview_digest`: `Option`; `summary`: `String`; `mutation_accepted`: `bool`; `inventory`: `Option` } | -| `MutatingVerbResponse` | struct | [`MutatingVerbResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2241) | struct { `verb`: `String`; `outcome`: `MutatingVerbOutcome`; `target_wave`: `Option`; `summary`: `Option`; `remediation`: `Option`; `api_ready`: `Option` } | -| `CapabilitiesResponse` | struct | [`CapabilitiesResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2267) | struct { `broker_socket`: `String`; `capabilities`: `Vec`; `public_socket`: `String`; `server_version`: `Version`; `selected_version`: `Version` } | -| `AuthStatusResponse` | struct | [`AuthStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2277) | struct { `allowed_subcommands`: `Vec`; `denied_subcommands`: `Vec`; `role`: `AuthRole`; `sockets`: `Vec` } | -| `ListResponse` | struct | [`ListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2286) | struct { `vms`: `Vec`; `read_model`: `Option` } | -| `StatusResponse` | struct | [`StatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2294) | struct { `entries`: `Vec`; `read_model`: `Option` } | -| `AuditResponse` | struct | [`AuditResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2314) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | -| `HostCheckResponse` | struct | [`HostCheckResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2364) | struct { `exit_code`: `u8`; `findings`: `Vec` } | -| `KeysListResponse` | struct | [`KeysListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2382) | struct { `entries`: `Vec` } | -| `KeysShowResponse` | struct | [`KeysShowResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2388) | struct { `vm`: `String`; `env`: `Option`; `managed_key_path`: `String`; `public_key`: `String`; `fingerprint`: `String`; `known_hosts_entry`: `Option` } | -| `UsbipProbeResponse` | struct | [`UsbipProbeResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2629) | struct { `entries`: `Vec` } | -| `UsbSecurityKeyStatusResponse` | struct | [`UsbSecurityKeyStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L3963) | struct { `host_proxy_enabled`: `bool`; `physical_keys`: `Vec`; `vm_devices`: `Vec`; `lease`: `UsbSkLeaseStatus` } | -| `UsbSecurityKeySessionsResponse` | struct | [`UsbSecurityKeySessionsResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4025) | struct { `sessions`: `Vec` } | -| `UsbSecurityKeyCancelResponse` | struct | [`UsbSecurityKeyCancelResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4043) | struct { `cancelled_session_id`: `Option`; `already_idle`: `bool` } | -| `UsbSecurityKeyTestResponse` | struct | [`UsbSecurityKeyTestResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4080) | struct { `vm`: `String`; `ok`: `bool`; `checks`: `Vec` } | +| `NamedProcessStreamResponse` | enum | [`NamedProcessStreamResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1373) | `Stdin` - (ExecWriteStdinResult); `Output` - (ExecReadOutputResult); `Delivered` - (ExecControlResult); `Wait` - (ExecWaitResult); `Closed` - (ExecCloseResult); `Terminal` - (ExecTerminalStatus); `Error` - (NamedProcessStreamError) | +| `ConsoleOpResponse` | enum | [`ConsoleOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2060) | `Attach` - (ConsoleAttachResult); `WriteStdin` - (ConsoleControlResult); `ReadOutput` - (ConsoleReadOutputResult); `Resize` - (ConsoleControlResult); `Wait` - (ConsoleWaitResult); `Close` - (ConsoleCloseResult) | +| `AudioOpResponse` | enum | [`AudioOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2266) | `Status` - (AudioStatusResult); `SetVolume` - (AudioSetResult); `Mute` - (AudioSetResult) | +| `HostCutoverResponse` | struct | [`HostCutoverResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2432) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `state`: `String`; `phase`: `u8`; `preview_digest`: `Option`; `summary`: `String`; `mutation_accepted`: `bool`; `inventory`: `Option` } | +| `MutatingVerbResponse` | struct | [`MutatingVerbResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2493) | struct { `verb`: `String`; `outcome`: `MutatingVerbOutcome`; `target_wave`: `Option`; `summary`: `Option`; `remediation`: `Option`; `api_ready`: `Option` } | +| `CapabilitiesResponse` | struct | [`CapabilitiesResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2519) | struct { `broker_socket`: `String`; `capabilities`: `Vec`; `public_socket`: `String`; `server_version`: `Version`; `selected_version`: `Version` } | +| `AuthStatusResponse` | struct | [`AuthStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2529) | struct { `allowed_subcommands`: `Vec`; `denied_subcommands`: `Vec`; `role`: `AuthRole`; `sockets`: `Vec` } | +| `ListResponse` | struct | [`ListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2538) | struct { `vms`: `Vec`; `read_model`: `Option` } | +| `StatusResponse` | struct | [`StatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2546) | struct { `entries`: `Vec`; `read_model`: `Option` } | +| `AuditResponse` | struct | [`AuditResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2566) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | +| `HostCheckResponse` | struct | [`HostCheckResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2616) | struct { `exit_code`: `u8`; `findings`: `Vec` } | +| `KeysListResponse` | struct | [`KeysListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2634) | struct { `entries`: `Vec` } | +| `KeysShowResponse` | struct | [`KeysShowResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2640) | struct { `vm`: `String`; `env`: `Option`; `managed_key_path`: `String`; `public_key`: `String`; `fingerprint`: `String`; `known_hosts_entry`: `Option` } | +| `UsbipProbeResponse` | struct | [`UsbipProbeResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2881) | struct { `entries`: `Vec` } | +| `UsbSecurityKeyStatusResponse` | struct | [`UsbSecurityKeyStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4294) | struct { `host_proxy_enabled`: `bool`; `physical_keys`: `Vec`; `vm_devices`: `Vec`; `lease`: `UsbSkLeaseStatus` } | +| `UsbSecurityKeySessionsResponse` | struct | [`UsbSecurityKeySessionsResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4356) | struct { `sessions`: `Vec` } | +| `UsbSecurityKeyCancelResponse` | struct | [`UsbSecurityKeyCancelResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4374) | struct { `cancelled_session_id`: `Option`; `already_idle`: `bool` } | +| `UsbSecurityKeyTestResponse` | struct | [`UsbSecurityKeyTestResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4411) | struct { `vm`: `String`; `ok`: `bool`; `checks`: `Vec` } | ### Broker socket response types @@ -538,7 +540,7 @@ running live guest activation. | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `VmLifecycleState` | enum | [`VmLifecycleState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2789) | `Stopped`; `Starting`; `Booted`; `Running`; `Stopping`; `Restarting`; `Failed`; `Unknown` | +| `VmLifecycleState` | enum | [`VmLifecycleState`](../../packages/d2b-contracts-control/src/public_wire.rs#L3041) | `Stopped`; `Starting`; `Booted`; `Running`; `Stopping`; `Restarting`; `Failed`; `Unknown` | ### Other documented enums @@ -617,33 +619,33 @@ running live guest activation. | `ExecOp` | enum | [`ExecOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L911) | `Start` - (ExecStartArgs); `WriteStdin` - (ExecWriteStdinArgs); `ReadOutput` - (ExecReadOutputArgs); `Signal` - (ExecSignalArgs); `Resize` - (ExecResizeArgs); `Wait` - (ExecWaitArgs); `Close` - (ExecCloseArgs); `List` - (ExecDetachedListArgs); `Logs` - (ExecDetachedLogsArgs); `Status` - (ExecDetachedStatusArgs); `Kill` - (ExecDetachedKillArgs) | | `ExecTerminalStatus` | enum | [`ExecTerminalStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L1027) | `Exited` - struct { `code`: `i32` }; `Signaled` - struct { `signal`: `u32` }; `Error` - struct { `slug`: `String` } | | `ExecDetachedKillOutcome` | enum | [`ExecDetachedKillOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L1218) | `Cancelling`; `AlreadyTerminal` | -| `ShellSessionState` | enum | [`ShellSessionState`](../../packages/d2b-contracts-control/src/public_wire.rs#L1412) | `Attached`; `Detached`; `Killed`; `PoolUnavailable`; `FeatureDisabled`; `OutputGap` | -| `ShellCloseCause` | enum | [`ShellCloseCause`](../../packages/d2b-contracts-control/src/public_wire.rs#L1423) | `ClientDetach`; `EvictedByForce`; `EvictedByAdminDetach`; `KilledByAdmin`; `PoolUnavailable`; `OutputGap` | -| `ConsoleProviderKind` | enum | [`ConsoleProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1533) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | -| `ConsoleOp` | enum | [`ConsoleOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1676) | `Attach` - (ConsoleAttachArgs); `WriteStdin` - (ConsoleWriteStdinArgs); `ReadOutput` - (ConsoleReadOutputArgs); `Resize` - (ConsoleResizeArgs); `Wait` - (ConsoleWaitArgs); `Close` - (ConsoleCloseArgs) | -| `AudioChannel` | enum | [`AudioChannel`](../../packages/d2b-contracts-control/src/public_wire.rs#L1822) | `Speaker`; `Microphone` | -| `AudioEnforcementPosture` | enum | [`AudioEnforcementPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L1836) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | -| `AudioProviderKind` | enum | [`AudioProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1878) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | -| `AudioOp` | enum | [`AudioOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1923) | `Status` - (AudioStatusArgs); `SetVolume` - (AudioSetVolumeArgs); `Mute` - (AudioMuteArgs) | -| `AudioSetApplied` | enum | [`AudioSetApplied`](../../packages/d2b-contracts-control/src/public_wire.rs#L1986) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | -| `HostCutoverOperation` | enum | [`HostCutoverOperation`](../../packages/d2b-contracts-control/src/public_wire.rs#L2142) | `Preview`; `Status`; `Hold`; `Resume`; `Apply`; `Rollback`; `Verify`; `Doctor`; `Finalize`; `Reset` | -| `HostCutoverResetScope` | enum | [`HostCutoverResetScope`](../../packages/d2b-contracts-control/src/public_wire.rs#L2168) | `Zone`; `Provider`; `Guest` | -| `MutatingVerbOutcome` | enum | [`MutatingVerbOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L2256) | `DryRunPlanned`; `Applied`; `ApiReadyTimeout`; `NotYetImplemented`; `BrokerError`; `InvalidRequest` | -| `UsbipProbeStatus` | enum | [`UsbipProbeStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L2400) | `Bound`; `Unbound`; `Degraded`; `Enrollable`; `Enrolled`; `Stale`; `DirectConfig`; `Unknown` | -| `UsbipDurableClaimState` | enum | [`UsbipDurableClaimState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2418) | `Missing`; `HeldByDesiredOwner`; `HeldByOtherOwner`; `StaleOwner`; `Corrupt`; `NotApplicable`; `Unknown` | -| `UsbipHostBindState` | enum | [`UsbipHostBindState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2443) | `Unbound`; `BoundToUsbipHost`; `BoundToUnexpectedDriver`; `DeviceMissing`; `NotApplicable`; `Unknown` | -| `UsbipHostCarrierState` | enum | [`UsbipHostCarrierState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2456) | `Absent`; `Unavailable`; `WithheldForOwner`; `Ready`; `DepartedDuringProbe`; `NotApplicable`; `Unknown` | -| `UsbipProxyState` | enum | [`UsbipProxyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2470) | `NotDeclared`; `Stopped`; `Starting`; `Listening`; `Stale`; `Failed`; `NotApplicable`; `Unknown` | -| `UsbipGuestImportState` | enum | [`UsbipGuestImportState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2493) | `Detached`; `Imported`; `Unavailable`; `NotApplicable`; `Unknown` | -| `UsbipTopologyState` | enum | [`UsbipTopologyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2511) | `Match`; `Mismatch`; `Incomplete`; `NotObserved`; `NotApplicable`; `Unknown` | -| `UsbipPolicyState` | enum | [`UsbipPolicyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2524) | `Allowed`; `Denied`; `Missing`; `NotApplicable`; `Unknown` | -| `UsbipProbeDegradedReasonCode` | enum | [`UsbipProbeDegradedReasonCode`](../../packages/d2b-contracts-control/src/public_wire.rs#L2543) | `PolicyFailed`; `DeviceDepartedBeforeClaim`; `DeviceDepartedAfterLock`; `DeviceDepartedDuringMutation`; `DeviceReappearedWithDifferentTopology`; `LockHeldByOtherOwner`; `InvalidPersistedLockClaim`; `CarrierUnavailable`; `HostBindUnavailable`; `ProxyUnavailable`; `GuestImportUnavailable`; `StaleHostState`; `StaleGuestState`; `ProbeIncomplete`; `Unknown` | -| `UsbProbeEntryKind` | enum | [`UsbProbeEntryKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2579) | `Usbip`; `QemuMediaSlot` | -| `AuditFormat` | enum | [`AuditFormat`](../../packages/d2b-contracts-control/src/public_wire.rs#L2649) | `Human`; `Json` | -| `AuthRole` | enum | [`AuthRole`](../../packages/d2b-contracts-control/src/public_wire.rs#L2657) | `None`; `Launcher`; `Admin` | -| `HostFindingSeverity` | enum | [`HostFindingSeverity`](../../packages/d2b-contracts-control/src/public_wire.rs#L2878) | `Pass`; `Warn`; `Fail` | -| `UsbSkLeaseState` | enum | [`UsbSkLeaseState`](../../packages/d2b-contracts-control/src/public_wire.rs#L3930) | `Idle`; `Active`; `Queued`; `Unknown` | -| `UsbSkSessionOutcome` | enum | [`UsbSkSessionOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L3985) | `Success`; `Timeout`; `Cancelled`; `DeviceUnavailable`; `Active`; `Unknown` | +| `ShellSessionState` | enum | [`ShellSessionState`](../../packages/d2b-contracts-control/src/public_wire.rs#L1664) | `Attached`; `Detached`; `Killed`; `PoolUnavailable`; `FeatureDisabled`; `OutputGap` | +| `ShellCloseCause` | enum | [`ShellCloseCause`](../../packages/d2b-contracts-control/src/public_wire.rs#L1675) | `ClientDetach`; `EvictedByForce`; `EvictedByAdminDetach`; `KilledByAdmin`; `PoolUnavailable`; `OutputGap` | +| `ConsoleProviderKind` | enum | [`ConsoleProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1785) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | +| `ConsoleOp` | enum | [`ConsoleOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1928) | `Attach` - (ConsoleAttachArgs); `WriteStdin` - (ConsoleWriteStdinArgs); `ReadOutput` - (ConsoleReadOutputArgs); `Resize` - (ConsoleResizeArgs); `Wait` - (ConsoleWaitArgs); `Close` - (ConsoleCloseArgs) | +| `AudioChannel` | enum | [`AudioChannel`](../../packages/d2b-contracts-control/src/public_wire.rs#L2074) | `Speaker`; `Microphone` | +| `AudioEnforcementPosture` | enum | [`AudioEnforcementPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L2088) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | +| `AudioProviderKind` | enum | [`AudioProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2130) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | +| `AudioOp` | enum | [`AudioOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L2175) | `Status` - (AudioStatusArgs); `SetVolume` - (AudioSetVolumeArgs); `Mute` - (AudioMuteArgs) | +| `AudioSetApplied` | enum | [`AudioSetApplied`](../../packages/d2b-contracts-control/src/public_wire.rs#L2238) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | +| `HostCutoverOperation` | enum | [`HostCutoverOperation`](../../packages/d2b-contracts-control/src/public_wire.rs#L2394) | `Preview`; `Status`; `Hold`; `Resume`; `Apply`; `Rollback`; `Verify`; `Doctor`; `Finalize`; `Reset` | +| `HostCutoverResetScope` | enum | [`HostCutoverResetScope`](../../packages/d2b-contracts-control/src/public_wire.rs#L2420) | `Zone`; `Provider`; `Guest` | +| `MutatingVerbOutcome` | enum | [`MutatingVerbOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L2508) | `DryRunPlanned`; `Applied`; `ApiReadyTimeout`; `NotYetImplemented`; `BrokerError`; `InvalidRequest` | +| `UsbipProbeStatus` | enum | [`UsbipProbeStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L2652) | `Bound`; `Unbound`; `Degraded`; `Enrollable`; `Enrolled`; `Stale`; `DirectConfig`; `Unknown` | +| `UsbipDurableClaimState` | enum | [`UsbipDurableClaimState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2670) | `Missing`; `HeldByDesiredOwner`; `HeldByOtherOwner`; `StaleOwner`; `Corrupt`; `NotApplicable`; `Unknown` | +| `UsbipHostBindState` | enum | [`UsbipHostBindState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2695) | `Unbound`; `BoundToUsbipHost`; `BoundToUnexpectedDriver`; `DeviceMissing`; `NotApplicable`; `Unknown` | +| `UsbipHostCarrierState` | enum | [`UsbipHostCarrierState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2708) | `Absent`; `Unavailable`; `WithheldForOwner`; `Ready`; `DepartedDuringProbe`; `NotApplicable`; `Unknown` | +| `UsbipProxyState` | enum | [`UsbipProxyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2722) | `NotDeclared`; `Stopped`; `Starting`; `Listening`; `Stale`; `Failed`; `NotApplicable`; `Unknown` | +| `UsbipGuestImportState` | enum | [`UsbipGuestImportState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2745) | `Detached`; `Imported`; `Unavailable`; `NotApplicable`; `Unknown` | +| `UsbipTopologyState` | enum | [`UsbipTopologyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2763) | `Match`; `Mismatch`; `Incomplete`; `NotObserved`; `NotApplicable`; `Unknown` | +| `UsbipPolicyState` | enum | [`UsbipPolicyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2776) | `Allowed`; `Denied`; `Missing`; `NotApplicable`; `Unknown` | +| `UsbipProbeDegradedReasonCode` | enum | [`UsbipProbeDegradedReasonCode`](../../packages/d2b-contracts-control/src/public_wire.rs#L2795) | `PolicyFailed`; `DeviceDepartedBeforeClaim`; `DeviceDepartedAfterLock`; `DeviceDepartedDuringMutation`; `DeviceReappearedWithDifferentTopology`; `LockHeldByOtherOwner`; `InvalidPersistedLockClaim`; `CarrierUnavailable`; `HostBindUnavailable`; `ProxyUnavailable`; `GuestImportUnavailable`; `StaleHostState`; `StaleGuestState`; `ProbeIncomplete`; `Unknown` | +| `UsbProbeEntryKind` | enum | [`UsbProbeEntryKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2831) | `Usbip`; `QemuMediaSlot` | +| `AuditFormat` | enum | [`AuditFormat`](../../packages/d2b-contracts-control/src/public_wire.rs#L2901) | `Human`; `Json` | +| `AuthRole` | enum | [`AuthRole`](../../packages/d2b-contracts-control/src/public_wire.rs#L2909) | `None`; `Launcher`; `Admin` | +| `HostFindingSeverity` | enum | [`HostFindingSeverity`](../../packages/d2b-contracts-control/src/public_wire.rs#L3130) | `Pass`; `Warn`; `Fail` | +| `UsbSkLeaseState` | enum | [`UsbSkLeaseState`](../../packages/d2b-contracts-control/src/public_wire.rs#L4261) | `Idle`; `Active`; `Queued`; `Unknown` | +| `UsbSkSessionOutcome` | enum | [`UsbSkSessionOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L4316) | `Success`; `Timeout`; `Cancelled`; `DeviceUnavailable`; `Active`; `Unknown` | | `TerminalStream` | enum | [`TerminalStream`](../../packages/d2b-contracts-control/src/terminal_wire.rs#L19) | `Stdout`; `Stderr` | | `TerminalStatus` | enum | [`TerminalStatus`](../../packages/d2b-contracts-control/src/terminal_wire.rs#L197) | `Exited` - struct { `code`: `i32` }; `Signaled` - struct { `signal`: `u32` }; `Error` - struct { `slug`: `String` } | | `HelperScopeKind` | enum | [`HelperScopeKind`](../../packages/d2b-contracts-control/src/unsafe_local_wire.rs#L125) | `LauncherApp`; `WaylandProxy`; `PersistentShell` | @@ -745,9 +747,11 @@ the failure class, for example `host check`, `audit`, `status`, or | `HandoffError` | enum | [`HandoffError`](../../packages/d2b-contracts-broker/src/host_generation.rs#L194) | `CompatibilityFloorInvalid`; `GenerationTooOld`; `TargetFingerprintMismatch`; `TargetGenerationMismatch`; `GenerationAncestryInvalid`; `InvalidTransition` | | `GuestControlError` | struct | [`GuestControlError`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1489) | struct { `kind`: `GuestControlErrorKind`; `remediation`: `HealthRemediation`; `retry_after_ms`: `Option` } | | `GuestControlErrorKind` | enum | [`GuestControlErrorKind`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1497) | `ProtocolError`; `MaxChunkExceeded`; `StdinBackpressure`; `StdinClosed`; `StdinNotOpen`; `StdinClosedByProcess`; `StdinOffsetMismatch`; `StdinByteBudgetExhausted`; `OffsetExpired`; `OffsetInFuture`; `OffsetExhausted`; `OutputLost`; `TtyStderrUnavailable`; `TtyRequired`; `ExecCapacityExceeded`; `ExecAttachCapacityExceeded`; `ExecNotFound`; `ExecAlreadyExited`; `GuestExecDisabled`; `GuestExecRootDenied`; `GuestExecUserDenied`; `CwdInvalid`; `CwdDenied`; `RetainedLogPathUnsafe`; `RetainedLogQuotaExceeded`; `ReadWaitCapacityExceeded`; `WaitCapacityExceeded`; `SupersededReadWait`; `RateLimited`; `RequestIdConflict`; `ControlSeqMismatch`; `SlowConsumerCancelled`; `StaleSession`; `GuestControlUnavailableOldGeneration`; `AuthFailed`; `TransportUnreachable`; `ExecExpired`; `FileNotFound`; `FileTooLarge`; `PathUnsafe`; `ReadDenied`; `InvalidProgram`; `UsbipUnavailable`; `UsbipCommandFailed`; `UsbipInvalidBusId`; `UsbipInvalidHost`; `GuestShellDisabled`; `ShellInvalidName`; `ShellCapacityExceeded`; `ShellAttachCapacityExceeded`; `ShellNotFound`; `ShellAlreadyAttached`; `ShellPoolUnavailable`; `ShellDaemonEpochMismatch`; `ShellOutputGap`; `UsbipCommandTimeout`; `UsbipInvalidOutput`; `ActivationInvalidId`; `ActivationInvalidPath`; `ActivationInvalidMode`; `ActivationNotFound`; `ActivationStatusUnavailable`; `ActivationTimedOut`; `ActivationSpawnFailed`; `AudioPipeWireUnavailable`; `AudioChannelUnknown`; `AudioLevelOutOfRange`; `AudioEnforcementFailed` | -| `ShellNameError` | struct | [`ShellNameError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1281) | empty struct | -| `AudioErrorKind` | enum | [`AudioErrorKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1859) | `ProviderMisconfigured`; `VmNotFound`; `EnforcementUnavailable`; `AudioNotEnabled`; `InternalError` | -| `AudioVmError` | struct | [`AudioVmError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1960) | struct { `vm`: `String`; `kind`: `AudioErrorKind`; `remediation`: `Option` } | +| `NamedProcessStreamErrorKind` | enum | [`NamedProcessStreamErrorKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1440) | `Authorization`; `StaleSession`; `NotFound`; `Backpressure`; `Protocol`; `Timeout`; `Disconnected` | +| `NamedProcessStreamError` | struct | [`NamedProcessStreamError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1460) | struct { `kind`: `NamedProcessStreamErrorKind` } | +| `ShellNameError` | struct | [`ShellNameError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1533) | empty struct | +| `AudioErrorKind` | enum | [`AudioErrorKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2111) | `ProviderMisconfigured`; `VmNotFound`; `EnforcementUnavailable`; `AudioNotEnabled`; `InternalError` | +| `AudioVmError` | struct | [`AudioVmError`](../../packages/d2b-contracts-control/src/public_wire.rs#L2212) | struct { `vm`: `String`; `kind`: `AudioErrorKind`; `remediation`: `Option` } | | `LevelPercentError` | enum | [`LevelPercentError`](../../packages/d2b-contracts/src/audio.rs#L28) | `OutOfRange` - (u8) | | `AudioPolicyError` | enum | [`AudioPolicyError`](../../packages/d2b-contracts/src/audio.rs#L217) | `InvalidJson` - (String); `InvalidField` - (String); `UnknownSchemaVersion` - (String); `Serialize` - (String) | | `AuditExportErrorCode` | enum | [`AuditExportErrorCode`](../../packages/d2b-contracts/src/audit_wire.rs#L20) | `HashBreak`; `RecordInvalid`; `ReadFailed` | diff --git a/packages/d2b-bus/src/router.rs b/packages/d2b-bus/src/router.rs index 7572737fb9..29f24e2835 100644 --- a/packages/d2b-bus/src/router.rs +++ b/packages/d2b-bus/src/router.rs @@ -37,7 +37,8 @@ use d2b_resource_api::watch::{WatchFrame, WatchSink, WatchSinkError}; use d2b_session::{ AuthenticatedComponentSession, AuthenticatedSessionRouteBinding, AuthenticatedTtrpcHandle, GENERATED_OPERATION_CATALOG, OperationKind, SessionAcceptor, SessionAuthorizationRequest, - SessionCancellationHandle, SessionOperation, SessionRegistrationCapability, + SessionCancellationHandle, SessionDriverHandle, SessionOperation, + SessionRegistrationCapability, contract::{EndpointPolicy, ServicePackage}, resource_operation, rewrite_ttrpc_stream_id, ttrpc_request_id, ttrpc_stream_id, }; @@ -1471,7 +1472,7 @@ impl UnixSubjectRecord { mut self, execution_ref: ResourceRef, ) -> d2b_session::Result { - if execution_ref.resource_type().as_str() != "Host" { + if !matches!(execution_ref.resource_type().as_str(), "Host" | "Guest") { return Err(d2b_session::SessionError::new( d2b_session::contract::SessionErrorCode::SubjectMismatch, )); @@ -1665,7 +1666,8 @@ impl core::fmt::Debug for AuthoritativeUnixSubjectResolver { } } -/// Trusted committed state used to install daemon-owned interaction subjects. +/// Trusted committed state used to install daemon-owned interaction and +/// Process/Shell attach subjects. pub struct CommittedInteractionSubjectInput { /// The Guest identity committed by the display WaylandSession. pub display_subject_ref: ResourceRef, @@ -2593,10 +2595,11 @@ impl ZoneRegistrar { ) } - /// Install the daemon-owned interaction subject projection from the - /// committed resource snapshot. The Unix peer UID authenticates the - /// transport only; the Guest/Host refs, resource generations, and - /// controller generation are all supplied by trusted committed state. + /// Install the daemon-owned interaction and Process/Shell subject + /// projection from the committed resource snapshot. The Unix peer UID + /// authenticates the transport only; the Guest/Host refs, resource + /// generations, and controller generation are all supplied by trusted + /// committed state. pub fn install_committed_interaction_subject( &self, committed: CommittedInteractionSubjectInput, @@ -2619,7 +2622,7 @@ impl ZoneRegistrar { ResourceRef::parse("Provider/display-wayland").expect("fixed display Provider ref"), display_generation, )]; - let mut subjects = Vec::with_capacity(5); + let mut subjects = Vec::with_capacity(6); for (service, provider_ref, generation) in services { subjects.push( UnixSubjectRecord::guest_for_uid( @@ -2634,6 +2637,21 @@ impl ZoneRegistrar { .for_service(service), ); } + // Process and ShellSession named streams use the generic Provider + // package on the wire, but their listener identities are retained by + // d2bd. Bind that package to the enrolled Guest's own execution + // reference rather than borrowing the display Host route. + subjects.push( + UnixSubjectRecord::guest_for_uid( + display_subject_ref.clone(), + display_subject_uid.clone(), + zone_ref.clone(), + expected_peer_uid, + )? + .with_controller_generation(controller_generation) + .with_execution_ref(display_subject_ref.clone())? + .for_service(ServicePackage::ProviderV3), + ); if let Some(generation) = clipboard_generation { let (subject_ref, subject_uid, provider_ref) = if let Some(provider_uid) = clipboard_provider_uid { @@ -3048,6 +3066,14 @@ impl Drop for OperationLease { } impl BusIngress { + /// Clone the authenticated ComponentSession driver for a daemon-owned + /// target-local named-stream owner. + pub fn component_session_driver(&self) -> Option { + self.attachments + .as_ref() + .map(AuthenticatedTtrpcHandle::component_session_driver) + } + /// Clone the daemon-owned request receiver for one session loop. pub fn component_request_receiver(&self) -> ComponentRequestReceiver { ComponentRequestReceiver { diff --git a/packages/d2b-contracts-control/src/public_wire.rs b/packages/d2b-contracts-control/src/public_wire.rs index 802f49a21c..16169e2e86 100644 --- a/packages/d2b-contracts-control/src/public_wire.rs +++ b/packages/d2b-contracts-control/src/public_wire.rs @@ -1258,6 +1258,258 @@ pub enum ExecOpResponse { Kill(ExecDetachedKillResult), } +/// Request carried by an authenticated Process or ShellSession named stream. +/// +/// The stream itself binds the resource and caller, so no session handle, +/// execution target, user, argv, environment, cwd, or transport locator is +/// repeated in these messages. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde( + tag = "kind", + content = "payload", + rename_all = "camelCase", + deny_unknown_fields +)] +pub enum NamedProcessStreamRequest { + /// Send one bounded stdin chunk. + Stdin { + offset: u64, + chunk_base64: String, + #[serde(default)] + eof: bool, + }, + /// Request one bounded output chunk. + Read { + stream: ExecStream, + offset: u64, + max_len: u64, + #[serde(default)] + wait: bool, + #[serde(default)] + timeout_ms: u64, + }, + /// Deliver one allowlisted signal to the process. + Signal { control_seq: u64, signo: u32 }, + /// Resize the attached terminal. + Resize { + control_seq: u64, + rows: u32, + cols: u32, + }, + /// Half-close stdin without tearing down the process. + CloseStdin { offset: u64 }, + /// Cancel the attached process and named stream. + Cancel, + /// Cleanly close the attached named stream without cancelling its process. + Close, + /// Poll the process terminal state. + Wait { timeout_ms: u64 }, +} + +impl fmt::Debug for NamedProcessStreamRequest { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Stdin { + offset, + chunk_base64, + eof, + } => formatter + .debug_struct("NamedProcessStreamRequest::Stdin") + .field("offset", offset) + .field("chunk_base64_len", &chunk_base64.len()) + .field("eof", eof) + .finish(), + Self::Read { + stream, + offset, + max_len, + wait, + timeout_ms, + } => formatter + .debug_struct("NamedProcessStreamRequest::Read") + .field("stream", stream) + .field("offset", offset) + .field("max_len", max_len) + .field("wait", wait) + .field("timeout_ms", timeout_ms) + .finish(), + Self::Signal { control_seq, signo } => formatter + .debug_struct("NamedProcessStreamRequest::Signal") + .field("control_seq", control_seq) + .field("signo", signo) + .finish(), + Self::Resize { + control_seq, + rows, + cols, + } => formatter + .debug_struct("NamedProcessStreamRequest::Resize") + .field("control_seq", control_seq) + .field("rows", rows) + .field("cols", cols) + .finish(), + Self::CloseStdin { offset } => formatter + .debug_struct("NamedProcessStreamRequest::CloseStdin") + .field("offset", offset) + .finish(), + Self::Cancel => formatter.write_str("NamedProcessStreamRequest::Cancel"), + Self::Close => formatter.write_str("NamedProcessStreamRequest::Close"), + Self::Wait { timeout_ms } => formatter + .debug_struct("NamedProcessStreamRequest::Wait") + .field("timeout_ms", timeout_ms) + .finish(), + } + } +} + +/// Response carried by an authenticated Process or ShellSession named stream. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde( + tag = "kind", + content = "payload", + rename_all = "camelCase", + deny_unknown_fields +)] +pub enum NamedProcessStreamResponse { + /// Acknowledge a stdin write. + Stdin(ExecWriteStdinResult), + /// Return one output chunk. + Output(ExecReadOutputResult), + /// Acknowledge a control operation. + Delivered(ExecControlResult), + /// Return the current terminal state. + Wait(ExecWaitResult), + /// Confirm stdin half-close or clean named-stream close. + Closed(ExecCloseResult), + /// Return a terminal disposition. + Terminal(ExecTerminalStatus), + /// Refuse the stream operation with a closed redacted error class. + Error(NamedProcessStreamError), +} + +impl fmt::Debug for NamedProcessStreamResponse { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Stdin(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Stdin") + .field(result) + .finish(), + Self::Output(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Output") + .field(result) + .finish(), + Self::Delivered(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Delivered") + .field(result) + .finish(), + Self::Wait(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Wait") + .field(result) + .finish(), + Self::Closed(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Closed") + .field(result) + .finish(), + Self::Terminal(result) => match result { + ExecTerminalStatus::Exited { code } => formatter + .debug_struct("NamedProcessStreamResponse::Terminal") + .field("kind", &"exited") + .field("code", code) + .finish(), + ExecTerminalStatus::Signaled { signal } => formatter + .debug_struct("NamedProcessStreamResponse::Terminal") + .field("kind", &"signaled") + .field("signal", signal) + .finish(), + ExecTerminalStatus::Error { .. } => formatter + .debug_struct("NamedProcessStreamResponse::Terminal") + .field("kind", &"error") + .finish(), + }, + Self::Error(result) => formatter + .debug_tuple("NamedProcessStreamResponse::Error") + .field(result) + .finish(), + } + } +} + +/// Closed failure vocabulary for named Process/ShellSession streams. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum NamedProcessStreamErrorKind { + /// The authenticated session cannot perform this operation. + Authorization, + /// The resource or controller generation is stale. + StaleSession, + /// The named stream or process is no longer available. + NotFound, + /// The stream credit or process quota is exhausted. + Backpressure, + /// The request was malformed or violated the resource contract. + Protocol, + /// The bounded operation deadline elapsed. + Timeout, + /// The authenticated ComponentSession disconnected. + Disconnected, +} + +/// Redacted named-stream failure. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NamedProcessStreamError { + pub kind: NamedProcessStreamErrorKind, +} + +impl fmt::Debug for NamedProcessStreamError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("NamedProcessStreamError") + .field("kind", &self.kind) + .finish() + } +} + +/// Correlated request frame carried by a Process/ShellSession stream. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NamedProcessStreamRequestFrame { + /// Bounded in-stream correlation id. + pub request_id: u64, + /// The typed operation. + pub request: NamedProcessStreamRequest, +} + +impl NamedProcessStreamRequestFrame { + /// Construct one correlated request frame. + pub const fn new(request_id: u64, request: NamedProcessStreamRequest) -> Self { + Self { + request_id, + request, + } + } +} + +/// Correlated response frame carried by a Process/ShellSession stream. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NamedProcessStreamResponseFrame { + /// Request id being completed. + pub request_id: u64, + /// The typed operation result. + pub response: NamedProcessStreamResponse, +} + +impl NamedProcessStreamResponseFrame { + /// Construct one correlated response frame. + pub const fn new(request_id: u64, response: NamedProcessStreamResponse) -> Self { + Self { + request_id, + response, + } + } +} + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] #[serde(transparent)] pub struct ShellName(String); @@ -2888,7 +3140,10 @@ fn default_true() -> bool { #[cfg(test)] mod tests { use super::{ - AuditResponse, LevelPercent, MutationFlags, PublicRequest, PublicResponse, RuntimeSummary, + AuditResponse, ExecReadOutputResult, ExecStream, ExecTerminalStatus, LevelPercent, + MutationFlags, NamedProcessStreamError, NamedProcessStreamErrorKind, + NamedProcessStreamRequest, NamedProcessStreamRequestFrame, NamedProcessStreamResponse, + NamedProcessStreamResponseFrame, PublicRequest, PublicResponse, RuntimeSummary, VmLifecycleRequest, VmLifecycleState, }; use d2b_contracts::{ @@ -2904,6 +3159,82 @@ mod tests { assert_eq!(encoded, "\"Booted\""); } + #[test] + fn named_process_stream_frames_round_trip_without_identity_fields() { + let request = NamedProcessStreamRequest::Stdin { + offset: 7, + chunk_base64: "c2VjcmV0".to_owned(), + eof: false, + }; + let value = serde_json::to_value(&request).expect("request serializes"); + assert_eq!(value["kind"], "stdin"); + assert!(value.get("session").is_none()); + let decoded: NamedProcessStreamRequest = + serde_json::from_value(value).expect("request decodes"); + assert_eq!(decoded, request); + let close = NamedProcessStreamRequest::Close; + assert_eq!( + serde_json::from_value::( + serde_json::to_value(&close).expect("close serializes") + ) + .expect("close decodes"), + close + ); + let frame = NamedProcessStreamRequestFrame::new(11, request); + let decoded: NamedProcessStreamRequestFrame = + serde_json::from_value(serde_json::to_value(&frame).expect("request frame serializes")) + .expect("request frame decodes"); + assert_eq!(decoded.request_id, 11); + + let response = NamedProcessStreamResponse::Output(ExecReadOutputResult { + data_base64: "c2VjcmV0".to_owned(), + next_offset: 13, + eof: false, + dropped_bytes: 0, + truncated: false, + timed_out: false, + }); + let rendered = format!("{response:?}"); + assert!(!rendered.contains("c2VjcmV0")); + let terminal = NamedProcessStreamResponse::Terminal(ExecTerminalStatus::Exited { code: 0 }); + assert_eq!( + serde_json::from_value::( + serde_json::to_value(terminal).expect("terminal serializes") + ) + .expect("terminal decodes"), + NamedProcessStreamResponse::Terminal(ExecTerminalStatus::Exited { code: 0 }) + ); + let terminal_error = NamedProcessStreamResponse::Terminal(ExecTerminalStatus::Error { + slug: "D2B_TERMINAL_LEAK_CANARY".to_owned(), + }); + assert!(!format!("{terminal_error:?}").contains("D2B_TERMINAL_LEAK_CANARY")); + let error = NamedProcessStreamResponse::Error(NamedProcessStreamError { + kind: NamedProcessStreamErrorKind::Disconnected, + }); + assert_eq!( + serde_json::from_value::( + serde_json::to_value(error).expect("error serializes") + ) + .expect("error decodes"), + NamedProcessStreamResponse::Error(NamedProcessStreamError { + kind: NamedProcessStreamErrorKind::Disconnected, + }) + ); + let response_frame = NamedProcessStreamResponseFrame::new( + 11, + NamedProcessStreamResponse::Terminal(ExecTerminalStatus::Exited { code: 0 }), + ); + assert_eq!( + serde_json::from_value::( + serde_json::to_value(response_frame).expect("response frame serializes") + ) + .expect("response frame decodes") + .request_id, + 11 + ); + let _ = ExecStream::Stdout; + } + #[test] fn public_response_deserializes_success_envelopes() { let capability_value = serde_json::json!({ diff --git a/packages/d2b-provider-shell-terminal/src/lib.rs b/packages/d2b-provider-shell-terminal/src/lib.rs index 0b603b43d2..b27bbf6fd0 100644 --- a/packages/d2b-provider-shell-terminal/src/lib.rs +++ b/packages/d2b-provider-shell-terminal/src/lib.rs @@ -33,7 +33,8 @@ pub use resources::{ pub use service::{ AttachReceipt, AttachRequest, Attachment, CONTROLLER_SERVICE, InMemoryShellAuthority, OpenSessionRequest, OpenSessionResult, SUPERVISOR_SERVICE, SessionCapability, SessionGrant, - SessionSupervisor, ShellAuthorityPort, ShellTerminalController, TERMINAL_STREAM, + SessionSupervisor, ShellAuthorityLedger, ShellAuthorityPort, ShellTerminalController, + SupervisorProcessResource, TERMINAL_STREAM, }; pub use session::{ AdoptionDecision, OutputRing, SupervisorCandidate, SupervisorIdentity, adopt_supervisor, diff --git a/packages/d2b-provider-shell-terminal/src/resources/session.rs b/packages/d2b-provider-shell-terminal/src/resources/session.rs index 8d8165ff3e..42b9fb64e9 100644 --- a/packages/d2b-provider-shell-terminal/src/resources/session.rs +++ b/packages/d2b-provider-shell-terminal/src/resources/session.rs @@ -2,6 +2,7 @@ use super::{ShellPool, ShellTerminalError, validate_name}; use crate::resources::ExecutionTarget; +use d2b_contracts_resource::v3::ResourceRef; /// Common resource phases used by shell pools and sessions. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -34,6 +35,9 @@ pub struct ShellSession { session_name: String, output_ring_capacity: u64, phase: SessionPhase, + supervisor_process_ref: ResourceRef, + supervisor_execution_ref: ResourceRef, + supervisor_user_ref: ResourceRef, } impl std::fmt::Debug for ShellSession { @@ -53,6 +57,19 @@ impl ShellSession { name: impl Into, session_name: impl Into, output_ring_capacity: Option, + ) -> Result { + let session_name = session_name.into(); + let name = name.into(); + Self::from_pool_with_resource_name(pool, name, session_name, output_ring_capacity) + } + + /// Create a session while choosing its stable resource name separately + /// from its operator-facing session name. + pub fn from_pool_with_resource_name( + pool: &ShellPool, + name: impl Into, + session_name: impl Into, + output_ring_capacity: Option, ) -> Result { let name = name.into(); let session_name = session_name.into(); @@ -63,6 +80,18 @@ impl ShellSession { if !(4 * 1024..=pool.spec().output_ring_capacity()).contains(&output_ring_capacity) { return Err(ShellTerminalError::CapacityOutOfRange); } + let target_type = if pool.execution_target().is_host() { + "Host" + } else { + "Guest" + }; + let supervisor_execution_ref = + ResourceRef::parse(&format!("{target_type}/{}", pool.execution_target().name())) + .map_err(|_| ShellTerminalError::InvalidName)?; + let supervisor_process_ref = ResourceRef::parse(&format!("Process/{name}")) + .map_err(|_| ShellTerminalError::InvalidName)?; + let supervisor_user_ref = ResourceRef::parse(&format!("User/{}", pool.workload_user())) + .map_err(|_| ShellTerminalError::InvalidName)?; Ok(Self { name, zone: pool.zone().to_owned(), @@ -73,6 +102,9 @@ impl ShellSession { session_name, output_ring_capacity, phase: SessionPhase::Pending, + supervisor_process_ref, + supervisor_execution_ref, + supervisor_user_ref, }) } @@ -125,4 +157,19 @@ impl ShellSession { pub const fn phase(&self) -> SessionPhase { self.phase } + + /// Borrow the Process child resource owned by this shell session. + pub const fn supervisor_process_ref(&self) -> &ResourceRef { + &self.supervisor_process_ref + } + + /// Borrow the Host or Guest execution parent for the supervisor Process. + pub const fn supervisor_execution_ref(&self) -> &ResourceRef { + &self.supervisor_execution_ref + } + + /// Borrow the exact workload User resource for the supervisor Process. + pub const fn supervisor_user_ref(&self) -> &ResourceRef { + &self.supervisor_user_ref + } } diff --git a/packages/d2b-provider-shell-terminal/src/service/controller.rs b/packages/d2b-provider-shell-terminal/src/service/controller.rs index 5d6041bf5b..20688c0519 100644 --- a/packages/d2b-provider-shell-terminal/src/service/controller.rs +++ b/packages/d2b-provider-shell-terminal/src/service/controller.rs @@ -87,6 +87,7 @@ impl OpenSessionResult { if identity.generation() != self.supervisor_generation { return Err(ShellTerminalError::StaleSessionGeneration); } + self.authority.ensure_supervisor_process(&self.session)?; self.authority.claim_supervisor(&self.session, &identity)?; Ok(SessionSupervisor::new( self.session.clone(), @@ -235,6 +236,7 @@ impl ShellTerminalController { let grant = self .authority .advance_session(&session, retiring_identity)?; + self.authority.ensure_supervisor_process(&session)?; let supervisor_generation = grant.generation(); let capability = grant.capability(); Ok(OpenSessionResult { @@ -271,6 +273,10 @@ impl ShellTerminalController { request.output_ring_capacity, )?; let grant = self.authority.open_session(&session)?; + if let Err(error) = self.authority.ensure_supervisor_process(&session) { + let _ = self.authority.finalize_session(&session, None); + return Err(error); + } let supervisor_generation = grant.generation(); let capability = grant.capability(); let result = OpenSessionResult { @@ -317,6 +323,7 @@ impl ShellTerminalController { .get(session.pool_name()) .ok_or(ShellTerminalError::CapacityExceeded)?; Authorizer::authorize(subject, pool)?; + self.authority.remove_supervisor_process(&session)?; self.authority.finalize_session(&session, identity)?; self.sessions.remove(session_name); self.trusted_sessions.remove(session_name); diff --git a/packages/d2b-provider-shell-terminal/src/service/mod.rs b/packages/d2b-provider-shell-terminal/src/service/mod.rs index 0de2aaafd0..50ef5f9a84 100644 --- a/packages/d2b-provider-shell-terminal/src/service/mod.rs +++ b/packages/d2b-provider-shell-terminal/src/service/mod.rs @@ -6,7 +6,8 @@ mod supervisor; pub use controller::{OpenSessionRequest, OpenSessionResult, ShellTerminalController}; pub use supervisor::{ AttachReceipt, AttachRequest, Attachment, InMemoryShellAuthority, SessionCapability, - SessionGrant, SessionSupervisor, ShellAuthorityPort, + SessionGrant, SessionSupervisor, ShellAuthorityLedger, ShellAuthorityPort, + SupervisorProcessResource, }; /// Public controller ComponentSession service name. diff --git a/packages/d2b-provider-shell-terminal/src/service/supervisor.rs b/packages/d2b-provider-shell-terminal/src/service/supervisor.rs index 4dcfa92595..6cbbd7c49e 100644 --- a/packages/d2b-provider-shell-terminal/src/service/supervisor.rs +++ b/packages/d2b-provider-shell-terminal/src/service/supervisor.rs @@ -5,6 +5,12 @@ use std::{ sync::Mutex, }; +use d2b_contracts_resource::v3::{ + ResourceRef, + execution_policy::{BoundedToken, ExecutionDomain}, + process::{ExecutionSpec, ProcessClass, ProcessSpec}, +}; + use crate::{ Authorizer, ExecutionTarget, ShellPool, ShellSession, ShellTerminalError, Subject, session::{OutputRing, RingReplay, SupervisorIdentity}, @@ -206,6 +212,76 @@ struct AuthorityState { next_capability: u64, } +/// The target-local Process resource created for one ShellSession supervisor. +#[derive(Clone, PartialEq, Eq)] +pub struct SupervisorProcessResource { + resource_ref: ResourceRef, + owner_ref: ResourceRef, + spec: ProcessSpec, +} + +impl SupervisorProcessResource { + /// Build the target-local Process intent owned by one ShellSession. + pub fn for_session(session: &ShellSession) -> Self { + let resource_ref = session.supervisor_process_ref().clone(); + let owner_ref = + ResourceRef::parse(&format!("{}/{}", session.resource_type(), session.name())) + .expect("validated ShellSession resource reference"); + let execution = ExecutionSpec::new( + session.supervisor_execution_ref().clone(), + Some(ExecutionDomain::User), + Some(session.supervisor_user_ref().clone()), + ProcessClass::Service, + BoundedToken::parse("shell-supervisor-main").expect("static process template"), + None, + Vec::new(), + Vec::new(), + Default::default(), + Default::default(), + None, + Vec::new(), + Default::default(), + ) + .expect("validated supervisor Process execution spec"); + Self { + resource_ref, + owner_ref, + spec: ProcessSpec::minimal(execution), + } + } + + /// Borrow the target-local Process identity. + pub const fn resource_ref(&self) -> &ResourceRef { + &self.resource_ref + } + + /// Borrow the ShellSession owner reference. + pub const fn owner_ref(&self) -> &ResourceRef { + &self.owner_ref + } + + /// Borrow the Process resource spec. + pub const fn spec(&self) -> &ProcessSpec { + &self.spec + } +} + +impl SupervisorProcessResource { + fn from_session(session: &ShellSession) -> Self { + Self::for_session(session) + } +} + +impl std::fmt::Debug for SupervisorProcessResource { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("SupervisorProcessResource") + .field("resource_ref", &"") + .field("owner_ref", &"") + .finish_non_exhaustive() + } +} + /// Daemon-owned authority operations required by controller and supervisor processes. /// /// Production implementations must forward every operation to one durable @@ -223,6 +299,15 @@ pub trait ShellAuthorityPort: Send + Sync { /// Atomically create one exact session authority and mint its grant. fn open_session(&self, session: &ShellSession) -> Result; + /// Create or adopt the session's target-local supervisor Process child. + /// + /// This is a resource-intent operation only. The controller never gets a + /// process handle and never spawns the child itself. + fn ensure_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError>; + + /// Delete the session's supervisor Process child after its streams close. + fn remove_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError>; + /// Verify that a recovered session remains the exact authoritative incumbent. fn verify_recovery( &self, @@ -306,18 +391,19 @@ pub trait ShellAuthorityPort: Send + Sync { ) -> Result<(), ShellTerminalError>; } -/// Hermetic daemon-authority model for tests and integration fakes. +/// Daemon-owned session-generation, capability, and attachment ledger. /// -/// This type deliberately models a single daemon owner. Production callers -/// must implement [`ShellAuthorityPort`] with the daemon's durable authority -/// service and construct one client in each controller or supervisor process. +/// The ledger deliberately contains no resource-store or process authority. +/// A production daemon composes it with its authenticated Resource API adapter, +/// while the public [`InMemoryShellAuthority`] wrapper uses it for hermetic +/// tests. #[derive(Default)] -pub struct InMemoryShellAuthority { +pub struct ShellAuthorityLedger { state: Mutex, } -impl InMemoryShellAuthority { - /// Construct an empty test authority owner. +impl ShellAuthorityLedger { + /// Construct an empty daemon authority ledger. pub fn new() -> Self { Self::default() } @@ -328,6 +414,38 @@ impl InMemoryShellAuthority { .map_err(|_| ShellTerminalError::CapacityExceeded) } + /// Validate that a Provider-reconstructed session still matches the + /// authority ledger without touching its test-only Process map. + pub fn validate_session(&self, session: &ShellSession) -> Result<(), ShellTerminalError> { + let state = self.lock()?; + let Some(entry) = state.sessions.get(session.name()) else { + return Err(ShellTerminalError::SupervisorAmbiguous); + }; + if entry.fingerprint != SessionFingerprint::from_session(session) { + return Err(ShellTerminalError::StaleSessionGeneration); + } + Ok(()) + } + + /// Return whether any authenticated attachment still owns this session. + pub fn has_active_attachments( + &self, + session: &ShellSession, + ) -> Result { + let state = self.lock()?; + let Some(entry) = state.sessions.get(session.name()) else { + return Err(ShellTerminalError::SupervisorAmbiguous); + }; + if entry.fingerprint != SessionFingerprint::from_session(session) { + return Err(ShellTerminalError::StaleSessionGeneration); + } + Ok(state.pools.get(session.pool_name()).is_some_and(|pool| { + pool.entries + .iter() + .any(|attachment| attachment.session_name == session.name()) + })) + } + fn pool_mut<'a>( state: &'a mut AuthorityState, pool: &ShellPool, @@ -370,13 +488,13 @@ impl InMemoryShellAuthority { } } -impl std::fmt::Debug for InMemoryShellAuthority { +impl std::fmt::Debug for ShellAuthorityLedger { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("InMemoryShellAuthority()") + formatter.write_str("ShellAuthorityLedger()") } } -impl ShellAuthorityPort for InMemoryShellAuthority { +impl ShellAuthorityPort for ShellAuthorityLedger { fn restore_pool( &self, pool: &ShellPool, @@ -442,6 +560,14 @@ impl ShellAuthorityPort for InMemoryShellAuthority { )) } + fn ensure_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError> { + self.validate_session(session) + } + + fn remove_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError> { + self.validate_session(session) + } + fn verify_recovery( &self, session: &ShellSession, @@ -724,11 +850,198 @@ impl ShellAuthorityPort for InMemoryShellAuthority { } } +/// Hermetic authority wrapper for provider unit and integration tests. +/// +/// Production composition uses [`ShellAuthorityLedger`] directly and supplies +/// the Resource API process adapter at the daemon boundary. +#[derive(Default)] +pub struct InMemoryShellAuthority { + ledger: ShellAuthorityLedger, + supervisor_processes: Mutex>, +} + +impl InMemoryShellAuthority { + /// Construct an empty test authority owner. + pub fn new() -> Self { + Self::default() + } + + /// Return the currently declared supervisor Process resource. + pub fn supervisor_process_resource( + &self, + session_name: &str, + ) -> Option { + self.supervisor_processes + .lock() + .ok() + .and_then(|processes| processes.get(session_name).cloned()) + } + + fn ensure_supervisor_process_state( + &self, + session: &ShellSession, + ) -> Result<(), ShellTerminalError> { + self.ledger.validate_session(session)?; + let value = SupervisorProcessResource::from_session(session); + let mut processes = self + .supervisor_processes + .lock() + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + if let Some(existing) = processes.get(session.name()) { + if existing != &value { + return Err(ShellTerminalError::SupervisorAmbiguous); + } + return Ok(()); + } + processes.insert(session.name().to_owned(), value); + Ok(()) + } + + fn remove_supervisor_process_state( + &self, + session: &ShellSession, + ) -> Result<(), ShellTerminalError> { + self.ledger.validate_session(session)?; + if self.ledger.has_active_attachments(session)? { + return Err(ShellTerminalError::CapacityExceeded); + } + self.supervisor_processes + .lock() + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)? + .remove(session.name()); + Ok(()) + } +} + +impl std::fmt::Debug for InMemoryShellAuthority { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("InMemoryShellAuthority()") + } +} + +impl ShellAuthorityPort for InMemoryShellAuthority { + fn restore_pool( + &self, + pool: &ShellPool, + attached_streams: u32, + ) -> Result<(), ShellTerminalError> { + self.ledger.restore_pool(pool, attached_streams) + } + + fn open_session(&self, session: &ShellSession) -> Result { + self.ledger.open_session(session) + } + + fn ensure_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError> { + self.ensure_supervisor_process_state(session) + } + + fn remove_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError> { + self.remove_supervisor_process_state(session) + } + + fn verify_recovery( + &self, + session: &ShellSession, + identity: &SupervisorIdentity, + ) -> Result { + self.ledger.verify_recovery(session, identity) + } + + fn advance_session( + &self, + session: &ShellSession, + retired_identity: Option<&SupervisorIdentity>, + ) -> Result { + self.ledger.advance_session(session, retired_identity) + } + + fn claim_supervisor( + &self, + session: &ShellSession, + identity: &SupervisorIdentity, + ) -> Result<(), ShellTerminalError> { + self.ledger.claim_supervisor(session, identity) + } + + fn validate_session( + &self, + session: &ShellSession, + identity: &SupervisorIdentity, + ) -> Result<(), ShellTerminalError> { + ShellAuthorityPort::validate_session(&self.ledger, session, identity) + } + + fn consume_capability( + &self, + session: &ShellSession, + identity: &SupervisorIdentity, + capability: &SessionCapability, + ) -> Result<(), ShellTerminalError> { + self.ledger + .consume_capability(session, identity, capability) + } + + fn admit_capability_attachment( + &self, + session: &ShellSession, + identity: &SupervisorIdentity, + capability: &SessionCapability, + ) -> Result { + self.ledger + .admit_capability_attachment(session, identity, capability) + } + + fn reserve_attachment( + &self, + session: &ShellSession, + identity: &SupervisorIdentity, + ) -> Result { + self.ledger.reserve_attachment(session, identity) + } + + fn release_attachment( + &self, + session: &ShellSession, + attachment: &Attachment, + ) -> Result<(), ShellTerminalError> { + self.ledger.release_attachment(session, attachment) + } + + fn reconcile_pool_attachments( + &self, + pool: &ShellPool, + attached_streams: u32, + ) -> Result<(), ShellTerminalError> { + self.ledger + .reconcile_pool_attachments(pool, attached_streams) + } + + fn retire_proven_stale( + &self, + pool: &ShellPool, + stale_attachments: &[Attachment], + attached_streams: u32, + ) -> Result<(), ShellTerminalError> { + self.ledger + .retire_proven_stale(pool, stale_attachments, attached_streams) + } + + fn finalize_session( + &self, + session: &ShellSession, + identity: Option<&SupervisorIdentity>, + ) -> Result<(), ShellTerminalError> { + self.ledger.finalize_session(session, identity) + } +} + /// A successful attachment response with redacted terminal replay bytes. pub struct AttachReceipt { generation: u64, replay: RingReplay, attachment: Attachment, + stream_name: &'static str, } impl AttachReceipt { @@ -746,6 +1059,11 @@ impl AttachReceipt { pub fn attachment(&self) -> Attachment { self.attachment.clone() } + + /// Return the sole authenticated ComponentSession stream name. + pub const fn stream_name(&self) -> &'static str { + self.stream_name + } } impl std::fmt::Debug for AttachReceipt { @@ -801,9 +1119,15 @@ impl SessionSupervisor { generation, replay: self.ring.tail(request.tail_bytes as usize), attachment, + stream_name: super::TERMINAL_STREAM, }) } + /// Borrow the Process child resource represented by this supervisor. + pub const fn process_ref(&self) -> &ResourceRef { + self.session.supervisor_process_ref() + } + /// Consume a one-shot capability after rechecking the current request authority. pub fn attach_with_capability( &mut self, @@ -821,6 +1145,7 @@ impl SessionSupervisor { generation, replay: self.ring.tail(0), attachment, + stream_name: super::TERMINAL_STREAM, }) } diff --git a/packages/d2b-provider-shell-terminal/tests/controller_reconcile.rs b/packages/d2b-provider-shell-terminal/tests/controller_reconcile.rs index d09597e4b0..b42f21ef31 100644 --- a/packages/d2b-provider-shell-terminal/tests/controller_reconcile.rs +++ b/packages/d2b-provider-shell-terminal/tests/controller_reconcile.rs @@ -45,6 +45,78 @@ fn controller_creates_one_pool_derived_session_without_provider_state() { assert_eq!(controller.session_count("guest-alice"), 1); } +#[test] +fn opening_a_session_creates_and_finalizing_deletes_its_process_resource() { + let authority = Arc::new(InMemoryShellAuthority::new()); + let mut controller = ShellTerminalController::new(authority.clone()); + controller.insert_pool(pool()).unwrap(); + let admin = Subject::new("dev", CallerOrigin::Local, [Role::ZoneAdmin]); + let opened = controller + .open_session( + &admin, + OpenSessionRequest::new("guest-alice", "main", None).unwrap(), + ) + .unwrap(); + + let process = authority + .supervisor_process_resource(opened.session().name()) + .expect("session supervisor Process resource"); + assert_eq!( + process.resource_ref().to_canonical_string(), + "Process/guest-alice-main" + ); + assert_eq!( + process.owner_ref().to_canonical_string(), + "shell-terminal.d2bus.org.ShellSession/guest-alice-main" + ); + assert_eq!( + process + .spec() + .execution() + .execution_ref() + .to_canonical_string(), + "Guest/work" + ); + assert_eq!( + process + .spec() + .execution() + .user_ref() + .expect("user-domain supervisor") + .to_canonical_string(), + "User/alice" + ); + + controller + .finalize_session(&admin, opened.session().name(), None) + .unwrap(); + assert!( + authority + .supervisor_process_resource(opened.session().name()) + .is_none(), + "finalization must delete the target-local Process resource" + ); +} + +#[test] +fn shell_session_declares_a_process_child_for_its_supervisor() { + let pool = pool(); + let session = + ShellSession::from_pool(&pool, "guest-alice-main", "main", None).expect("valid session"); + assert_eq!( + session.supervisor_process_ref().to_canonical_string(), + "Process/guest-alice-main" + ); + assert_eq!( + session.supervisor_execution_ref().to_canonical_string(), + "Guest/work" + ); + assert_eq!( + session.supervisor_user_ref().to_canonical_string(), + "User/alice" + ); +} + #[test] fn unauthorized_requests_fail_before_resource_lookup() { let mut controller = controller(); @@ -74,6 +146,11 @@ fn restored_sessions_block_recreation_after_controller_restart() { OpenSessionRequest::new("guest-alice", "main", None).unwrap(), ) .unwrap(); + assert!( + authority + .supervisor_process_resource(opened.session().name()) + .is_some() + ); let identity = SupervisorIdentity::new([1; 32], [2; 32], opened.supervisor_generation()).unwrap(); let mut old_supervisor = opened.start_supervisor(identity.clone()).unwrap(); @@ -86,7 +163,7 @@ fn restored_sessions_block_recreation_after_controller_restart() { .unwrap() .attachment(); - let mut recovered_controller = ShellTerminalController::new(authority); + let mut recovered_controller = ShellTerminalController::new(authority.clone()); recovered_controller.restore_pool(pool(), 1).unwrap(); assert_eq!( recovered_controller @@ -112,11 +189,9 @@ fn restored_sessions_block_recreation_after_controller_restart() { let restarted = recovered_controller .restart_supervisor(&admin, opened.session().name(), Some(&identity)) .unwrap(); - let mut new_supervisor = restarted - .start_supervisor( - SupervisorIdentity::new([3; 32], [4; 32], restarted.supervisor_generation()).unwrap(), - ) - .unwrap(); + let new_identity = + SupervisorIdentity::new([3; 32], [4; 32], restarted.supervisor_generation()).unwrap(); + let mut new_supervisor = restarted.start_supervisor(new_identity.clone()).unwrap(); assert!(matches!( old_supervisor.attach( &admin, @@ -134,17 +209,22 @@ fn restored_sessions_block_recreation_after_controller_restart() { Err(d2b_provider_shell_terminal::ShellTerminalError::CapacityExceeded) )); old_supervisor.detach(&admin, old_attachment).unwrap(); - assert!( - new_supervisor - .attach( - &admin, - d2b_provider_shell_terminal::AttachRequest::new( - restarted.supervisor_generation(), - 0, - ) + let new_attachment = new_supervisor + .attach( + &admin, + d2b_provider_shell_terminal::AttachRequest::new(restarted.supervisor_generation(), 0) .unwrap(), - ) - .is_ok() + ) + .unwrap() + .attachment(); + new_supervisor.detach(&admin, new_attachment).unwrap(); + recovered_controller + .finalize_session(&admin, opened.session().name(), Some(&new_identity)) + .unwrap(); + assert!( + authority + .supervisor_process_resource(opened.session().name()) + .is_none() ); } diff --git a/packages/d2b-provider-shell-terminal/tests/supervisor_runtime.rs b/packages/d2b-provider-shell-terminal/tests/supervisor_runtime.rs index 2dfeb2cebd..9afcdab8e4 100644 --- a/packages/d2b-provider-shell-terminal/tests/supervisor_runtime.rs +++ b/packages/d2b-provider-shell-terminal/tests/supervisor_runtime.rs @@ -47,6 +47,10 @@ fn supervisor_rejects_stale_generation_and_reused_capability() { SupervisorIdentity::new([1; 32], [2; 32], opened.supervisor_generation()).unwrap(), ) .unwrap(); + assert_eq!( + supervisor.process_ref().to_canonical_string(), + "Process/guest-alice-main" + ); assert!( supervisor @@ -109,6 +113,7 @@ fn detach_releases_the_bounded_attachment_slot() { AttachRequest::new(opened.supervisor_generation(), 0).unwrap(), ) .unwrap(); + assert_eq!(first.stream_name(), "terminal"); assert!( supervisor .attach( diff --git a/packages/d2b-provider-system-systemd/BUILD.bazel b/packages/d2b-provider-system-systemd/BUILD.bazel index 8ae3ac98ba..e917cd0cf9 100644 --- a/packages/d2b-provider-system-systemd/BUILD.bazel +++ b/packages/d2b-provider-system-systemd/BUILD.bazel @@ -13,6 +13,7 @@ rust_library( compile_data = ["Cargo.toml"], visibility = ["//visibility:public"], deps = [ + "//packages/d2b-contracts-control:d2b_contracts_control", "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-process-conformance:d2b_process_conformance", @@ -27,6 +28,7 @@ rust_library( crate_name = "d2b_provider_system_systemd", visibility = ["//visibility:public"], deps = [ + "//packages/d2b-contracts-control:d2b_contracts_control", "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-process-conformance:d2b_process_conformance_test_support", diff --git a/packages/d2b-provider-system-systemd/Cargo.toml b/packages/d2b-provider-system-systemd/Cargo.toml index 1ae9b7164b..44f2cc83a5 100644 --- a/packages/d2b-provider-system-systemd/Cargo.toml +++ b/packages/d2b-provider-system-systemd/Cargo.toml @@ -9,9 +9,11 @@ license.workspace = true workspace = true [dependencies] +d2b-contracts-control = { path = "../d2b-contracts-control", version = "0.0.0-bootstrap" } d2b-contracts-zone-session = { path = "../d2b-contracts-zone-session", version = "0.0.0-bootstrap" } d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } d2b-process-conformance = { path = "../d2b-process-conformance", version = "0.0.0-bootstrap" } d2b-provider-toolkit = { path = "../d2b-provider-toolkit", version = "0.0.0-bootstrap" } serde = { workspace = true } +serde_json.workspace = true tokio = { workspace = true, features = ["sync", "time"] } diff --git a/packages/d2b-provider-system-systemd/src/guest_exec.rs b/packages/d2b-provider-system-systemd/src/guest_exec.rs index a5353fce2b..a04ee19e87 100644 --- a/packages/d2b-provider-system-systemd/src/guest_exec.rs +++ b/packages/d2b-provider-system-systemd/src/guest_exec.rs @@ -6,11 +6,19 @@ //! argv, environment, paths, and credentials are resolved by the signed //! Provider template and never cross this boundary. -use std::{fmt, future::Future}; +use std::{ + fmt, + future::Future, + sync::atomic::{AtomicU8, Ordering}, +}; +use d2b_contracts_control::public_wire::{ + NamedProcessStreamRequestFrame, NamedProcessStreamResponseFrame, +}; use d2b_contracts_resource::v3::{ ResourceRef, - execution_policy::BoundedToken, + execution_policy::{BoundedToken, DurationMs}, + process::{EphemeralProcessSpec, ExecutionSpec, ProcessClass}, }; use d2b_provider_toolkit::{ComponentSessionDriver, StreamEvent, StreamId}; @@ -18,6 +26,12 @@ use d2b_provider_toolkit::{ComponentSessionDriver, StreamEvent, StreamId}; pub const DETACHED_FAILED_TTL_MS: u64 = 24 * 60 * 60 * 1_000; /// Default per-direction named-stream credit for guest attachment. pub const GUEST_EXEC_STREAM_CREDIT: u32 = 256 * 1024; +/// The authenticated named stream used by guest Process attachments. +pub const GUEST_EXEC_STREAM_NAME: &str = "process"; + +const STREAM_OPEN: u8 = 0; +const STREAM_CLOSING: u8 = 1; +const STREAM_CLOSED: u8 = 2; /// Bounded terminal geometry supplied to an attach operation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -110,6 +124,34 @@ impl GuestExecRequest { pub const fn initial_size(&self) -> Option { self.initial_size } + + /// Build the resource-owned EphemeralProcess spec for this request. + /// + /// The command remains a signed template concern. Only the Guest target, + /// worker classification, and bounded lifecycle retention cross this + /// Provider seam. + pub fn ephemeral_process_spec(&self) -> Result { + let execution = ExecutionSpec::minimal( + self.execution_ref.clone(), + ProcessClass::Worker, + self.template.clone(), + ) + .map_err(|_| GuestExecError::InvalidProcessSpec)?; + let runtime_deadline = if self.detached { "24h" } else { "6h" }; + EphemeralProcessSpec::new( + execution, + DurationMs::parse("60s", 1_000, 3_600_000) + .map_err(|_| GuestExecError::InvalidProcessSpec)?, + DurationMs::parse(runtime_deadline, 1_000, 86_400_000) + .map_err(|_| GuestExecError::InvalidProcessSpec)?, + DurationMs::parse("1h", 0, 7 * 86_400_000) + .map_err(|_| GuestExecError::InvalidProcessSpec)?, + DurationMs::parse("24h", 0, 30 * 86_400_000) + .map_err(|_| GuestExecError::InvalidProcessSpec)?, + false, + ) + .map_err(|_| GuestExecError::InvalidProcessSpec) + } } /// A typed attach request replacing the userd socket protocol. @@ -169,10 +211,20 @@ impl NamedAttachmentStream { Self(name) } + /// Construct the fixed guest Process attachment stream name. + pub fn process() -> Self { + Self::new(BoundedToken::parse(GUEST_EXEC_STREAM_NAME).expect("valid process stream name")) + } + /// Borrow the stream name for the session mux. pub const fn name(&self) -> &BoundedToken { &self.0 } + + /// Return the stream name's bounded text. + pub fn as_str(&self) -> &str { + self.0.as_str() + } } impl fmt::Debug for NamedAttachmentStream { @@ -187,6 +239,7 @@ pub struct ComponentSessionAttachment { driver: D, stream: StreamId, name: NamedAttachmentStream, + state: AtomicU8, } impl fmt::Debug for ComponentSessionAttachment { @@ -205,6 +258,9 @@ where stream_number: u16, name: NamedAttachmentStream, ) -> Result { + if name.as_str() != GUEST_EXEC_STREAM_NAME { + return Err(GuestExecError::InvalidStreamName); + } let stream = StreamId::new(stream_number).map_err(|_| GuestExecError::StreamUnavailable)?; driver .open_named_stream(stream, GUEST_EXEC_STREAM_CREDIT, GUEST_EXEC_STREAM_CREDIT) @@ -214,6 +270,7 @@ where driver, stream, name, + state: AtomicU8::new(STREAM_OPEN), }) } @@ -222,36 +279,127 @@ where &self.name } + /// Whether the named stream has been closed or reset. + pub fn is_closed(&self) -> bool { + self.state.load(Ordering::Acquire) == STREAM_CLOSED + } + /// Send one bounded logical stream message. pub async fn send(&self, bytes: Vec) -> Result<(), GuestExecError> { + if bytes.is_empty() + || bytes.len() + > d2b_contracts_zone_session::v3::component_session::MAX_LOGICAL_MESSAGE_BYTES + as usize + { + return Err(GuestExecError::InvalidStreamPayload); + } + if self.state.load(Ordering::Acquire) != STREAM_OPEN { + return Err(GuestExecError::StreamUnavailable); + } self.driver .send_named_stream(self.stream, bytes) .await .map_err(|_| GuestExecError::StreamUnavailable) } + /// Send one canonical Process named-stream request frame. + pub async fn send_frame( + &self, + frame: &NamedProcessStreamRequestFrame, + ) -> Result<(), GuestExecError> { + if frame.request_id == 0 { + return Err(GuestExecError::InvalidStreamPayload); + } + let bytes = serde_json::to_vec(frame).map_err(|_| GuestExecError::InvalidStreamPayload)?; + self.send(bytes).await + } + /// Receive the next typed stream event. pub async fn receive(&self) -> Result { - self.driver + if self.state.load(Ordering::Acquire) != STREAM_OPEN { + return Err(GuestExecError::StreamUnavailable); + } + let event = self + .driver .receive_named_stream() .await - .map_err(|_| GuestExecError::StreamUnavailable) + .map_err(|_| GuestExecError::StreamUnavailable)?; + if matches!( + event, + StreamEvent::RemoteClosed { .. } | StreamEvent::Reset { .. } + ) { + self.state.store(STREAM_CLOSED, Ordering::Release); + } + Ok(event) + } + + /// Receive and decode one canonical Process named-stream response frame. + pub async fn receive_frame(&self) -> Result { + let event = self.receive().await?; + let StreamEvent::Data { stream, bytes } = event else { + return Err(GuestExecError::InvalidStreamPayload); + }; + if stream != self.stream { + return Err(GuestExecError::InvalidStreamPayload); + } + let frame: NamedProcessStreamResponseFrame = + serde_json::from_slice(&bytes).map_err(|_| GuestExecError::InvalidStreamPayload)?; + if frame.request_id == 0 { + return Err(GuestExecError::InvalidStreamPayload); + } + Ok(frame) } /// Close this named stream. pub async fn close(&self) -> Result<(), GuestExecError> { - self.driver - .close_named_stream(self.stream) - .await - .map_err(|_| GuestExecError::StreamUnavailable) + if self + .state + .compare_exchange( + STREAM_OPEN, + STREAM_CLOSING, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_err() + { + return Ok(()); + } + match self.driver.close_named_stream(self.stream).await { + Ok(()) => { + self.state.store(STREAM_CLOSED, Ordering::Release); + Ok(()) + } + Err(_) => { + self.state.store(STREAM_OPEN, Ordering::Release); + Err(GuestExecError::StreamUnavailable) + } + } } /// Reset this named stream after cancellation or protocol failure. pub async fn reset(&self) -> Result<(), GuestExecError> { - self.driver - .reset_named_stream(self.stream) - .await - .map_err(|_| GuestExecError::StreamUnavailable) + if self + .state + .compare_exchange( + STREAM_OPEN, + STREAM_CLOSING, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_err() + { + return Ok(()); + } + match self.driver.reset_named_stream(self.stream).await { + Ok(()) => { + self.state.store(STREAM_CLOSED, Ordering::Release); + Ok(()) + } + Err(_) => { + self.state.store(STREAM_OPEN, Ordering::Release); + Err(GuestExecError::StreamUnavailable) + } + } } } @@ -270,6 +418,12 @@ pub enum GuestExecError { NotAttachable, /// The ComponentSession stream could not be opened. StreamUnavailable, + /// The request used a stream name outside the Process contract. + InvalidStreamName, + /// The stream payload was empty or exceeded the ComponentSession bound. + InvalidStreamPayload, + /// The resource-owned EphemeralProcess spec could not be built. + InvalidProcessSpec, } impl fmt::Display for GuestExecError { @@ -281,6 +435,9 @@ impl fmt::Display for GuestExecError { Self::TtySizeWithoutTty => "guest-exec-tty-size-unexpected", Self::NotAttachable => "guest-exec-process-not-attachable", Self::StreamUnavailable => "guest-exec-stream-unavailable", + Self::InvalidStreamName => "guest-exec-stream-name-invalid", + Self::InvalidStreamPayload => "guest-exec-stream-payload-invalid", + Self::InvalidProcessSpec => "guest-exec-process-spec-invalid", }) } } @@ -330,6 +487,44 @@ mod tests { assert_eq!(DETACHED_FAILED_TTL_MS, 86_400_000); } + #[test] + fn detached_exec_builds_the_guest_ephemeral_process_spec() { + let request = GuestExecRequest::new( + process(), + guest(), + BoundedToken::parse("shell-terminal").unwrap(), + true, + false, + None, + ) + .unwrap(); + let spec = request.ephemeral_process_spec().unwrap(); + assert_eq!(spec.execution().execution_ref(), &guest()); + assert_eq!( + spec.execution().process_class(), + d2b_contracts_resource::v3::process::ProcessClass::Worker + ); + assert_eq!(spec.execution().template().as_str(), "shell-terminal"); + assert_eq!(spec.failed_ttl().as_millis(), DETACHED_FAILED_TTL_MS); + let attached = GuestExecRequest::new( + process(), + guest(), + BoundedToken::parse("shell-terminal").unwrap(), + false, + false, + None, + ) + .unwrap(); + assert_eq!( + attached + .ephemeral_process_spec() + .unwrap() + .runtime_deadline() + .as_millis(), + 21_600_000 + ); + } + #[test] fn attach_validation_preserves_tty_and_stream_error_boundaries() { assert_eq!( @@ -342,5 +537,35 @@ mod tests { AttachRequest::new(process(), false, Some(size)).unwrap_err(), GuestExecError::TtySizeWithoutTty ); + assert_eq!( + NamedAttachmentStream::process().as_str(), + GUEST_EXEC_STREAM_NAME + ); + assert_eq!( + GuestExecError::InvalidStreamName.to_string(), + "guest-exec-stream-name-invalid" + ); + } + + #[test] + fn process_attachment_round_trips_the_shared_named_stream_frame() { + let request = d2b_contracts_control::public_wire::NamedProcessStreamRequestFrame::new( + 9, + d2b_contracts_control::public_wire::NamedProcessStreamRequest::Close, + ); + let encoded = serde_json::to_vec(&request).unwrap(); + let decoded: NamedProcessStreamRequestFrame = serde_json::from_slice(&encoded).unwrap(); + assert_eq!(decoded, request); + + let response = d2b_contracts_control::public_wire::NamedProcessStreamResponseFrame::new( + decoded.request_id, + d2b_contracts_control::public_wire::NamedProcessStreamResponse::Closed( + d2b_contracts_control::public_wire::ExecCloseResult { stdin_closed: true }, + ), + ); + let response_bytes = serde_json::to_vec(&response).unwrap(); + let response: NamedProcessStreamResponseFrame = + serde_json::from_slice(&response_bytes).unwrap(); + assert_eq!(response.request_id, 9); } } diff --git a/packages/d2b-provider-system-systemd/src/lifecycle.rs b/packages/d2b-provider-system-systemd/src/lifecycle.rs index d0d3138345..3176bda923 100644 --- a/packages/d2b-provider-system-systemd/src/lifecycle.rs +++ b/packages/d2b-provider-system-systemd/src/lifecycle.rs @@ -1,5 +1,6 @@ //! Bounded systemd Provider lifecycle policy. +use d2b_contracts_resource::v3::process::EphemeralProcessSpec; use d2b_process_conformance::{ProcessExitClass, ProcessOutcome}; /// Provider-level systemd settings. @@ -139,7 +140,8 @@ impl RestartPolicy { /// One-shot EphemeralProcess lifecycle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct EphemeralProcessController { - ttl_seconds: u64, + successful_ttl_seconds: u64, + failed_ttl_seconds: u64, ttl_remaining: Option, incident_hold: bool, terminal: Option, @@ -149,7 +151,32 @@ impl EphemeralProcessController { /// Construct a one-shot process with a bounded terminal TTL. pub const fn new(ttl_seconds: u64, incident_hold: bool) -> Self { Self { - ttl_seconds, + successful_ttl_seconds: ttl_seconds, + failed_ttl_seconds: ttl_seconds, + ttl_remaining: None, + incident_hold, + terminal: None, + } + } + + /// Build lifecycle state from the resource-owned EphemeralProcess + /// retention policy. + pub fn from_spec(spec: &EphemeralProcessSpec) -> Self { + Self::with_ttls( + spec.successful_ttl().as_millis().div_ceil(1_000), + spec.failed_ttl().as_millis().div_ceil(1_000), + spec.incident_hold(), + ) + } + + fn with_ttls( + successful_ttl_seconds: u64, + failed_ttl_seconds: u64, + incident_hold: bool, + ) -> Self { + Self { + successful_ttl_seconds, + failed_ttl_seconds, ttl_remaining: None, incident_hold, terminal: None, @@ -160,7 +187,11 @@ impl EphemeralProcessController { pub fn observe(&mut self, outcome: ProcessOutcome) -> ProcessExitClass { self.terminal = Some(outcome.exit_class); if self.ttl_remaining.is_none() { - self.ttl_remaining = Some(self.ttl_seconds); + self.ttl_remaining = Some(if outcome.exit_class == ProcessExitClass::CleanExit { + self.successful_ttl_seconds + } else { + self.failed_ttl_seconds + }); } outcome.exit_class } diff --git a/packages/d2b-provider-system-systemd/tests/lifecycle.rs b/packages/d2b-provider-system-systemd/tests/lifecycle.rs index 5a41816524..5251c25dfd 100644 --- a/packages/d2b-provider-system-systemd/tests/lifecycle.rs +++ b/packages/d2b-provider-system-systemd/tests/lifecycle.rs @@ -1,6 +1,7 @@ +use d2b_contracts_resource::v3::{ResourceRef, execution_policy::BoundedToken}; use d2b_process_conformance::{ProcessExitClass, ProcessOutcome}; use d2b_provider_system_systemd::{ - EphemeralProcessController, RestartPolicy, SystemdProviderConfig, + EphemeralProcessController, GuestExecRequest, RestartPolicy, SystemdProviderConfig, }; #[test] @@ -65,3 +66,26 @@ fn incident_hold_blocks_ephemeral_cleanup_after_ttl_expiry() { assert_eq!(process.ttl_remaining(), Some(0)); assert!(!process.cleanup_eligible()); } + +#[test] +fn resource_retention_policy_drives_ephemeral_cleanup_state() { + let request = GuestExecRequest::new( + ResourceRef::parse("EphemeralProcess/exec").unwrap(), + ResourceRef::parse("Guest/work").unwrap(), + BoundedToken::parse("shell-terminal").unwrap(), + true, + false, + None, + ) + .unwrap(); + let spec = request.ephemeral_process_spec().unwrap(); + let mut process = EphemeralProcessController::from_spec(&spec); + process.observe(ProcessOutcome::exited(0).unwrap()); + assert_eq!(process.ttl_remaining(), Some(3_600)); + process.tick(86_400); + assert!(process.cleanup_eligible()); + + let mut failed = EphemeralProcessController::from_spec(&spec); + failed.observe(ProcessOutcome::crashed()); + assert_eq!(failed.ttl_remaining(), Some(86_400)); +} diff --git a/packages/d2b-resource-client/BUILD.bazel b/packages/d2b-resource-client/BUILD.bazel index 28294499e6..808411a091 100644 --- a/packages/d2b-resource-client/BUILD.bazel +++ b/packages/d2b-resource-client/BUILD.bazel @@ -13,6 +13,7 @@ rust_library( compile_data = ["Cargo.toml"], visibility = ["//visibility:public"], deps = [ + "//packages/d2b-contracts-control:d2b_contracts_control", "//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-core-controller:d2b_core_controller", diff --git a/packages/d2b-resource-client/Cargo.toml b/packages/d2b-resource-client/Cargo.toml index a88428c7a6..edddc3f923 100644 --- a/packages/d2b-resource-client/Cargo.toml +++ b/packages/d2b-resource-client/Cargo.toml @@ -9,10 +9,12 @@ license.workspace = true workspace = true [dependencies] +d2b-contracts-control = { path = "../d2b-contracts-control", version = "0.0.0-bootstrap" } d2b-contracts-zone-session = { path = "../d2b-contracts-zone-session", version = "0.0.0-bootstrap" } d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } d2b-core-controller = { path = "../d2b-core-controller", version = "0.0.0-bootstrap" } +serde_json.workspace = true tokio = { workspace = true, features = ["sync", "time"] } [dev-dependencies] diff --git a/packages/d2b-resource-client/src/process_attach.rs b/packages/d2b-resource-client/src/process_attach.rs index 6b8b1c0c85..9ec626f668 100644 --- a/packages/d2b-resource-client/src/process_attach.rs +++ b/packages/d2b-resource-client/src/process_attach.rs @@ -13,6 +13,9 @@ use std::{ time::Duration, }; +use d2b_contracts_control::public_wire::{ + NamedProcessStreamRequestFrame, NamedProcessStreamResponseFrame, +}; use d2b_contracts_resource::v3::ResourceRef; use d2b_contracts_zone_session::v3::zone_routing::ZonePath; @@ -34,6 +37,11 @@ const STREAM_OPEN: u8 = 0; const STREAM_CLOSING: u8 = 1; const STREAM_CLOSED: u8 = 2; const SHELL_SESSION_TYPE: &str = "shell-terminal.d2bus.org.ShellSession"; +/// The authenticated named stream used by Process and EphemeralProcess +/// attachments. +pub const PROCESS_ATTACH_STREAM_NAME: &str = "process"; +/// The authenticated named stream used by persistent ShellSession resources. +pub const SHELL_SESSION_STREAM_NAME: &str = "terminal"; /// Resource classes that may be named by an attach request. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -137,6 +145,16 @@ impl ProcessAttachTarget { } } + /// Return the fixed named-stream contract for this target class. + pub const fn stream_name(&self) -> &'static str { + match self { + Self::EphemeralProcess { .. } | Self::ConfiguredLauncher { .. } => { + PROCESS_ATTACH_STREAM_NAME + } + Self::ShellSession { .. } => SHELL_SESSION_STREAM_NAME, + } + } + /// Borrow the exact Zone route. pub const fn zone(&self) -> &ZonePath { match self { @@ -311,6 +329,11 @@ impl ProcessAttachOpenRequest { self.options } + /// Return the fixed authenticated stream name selected by the target. + pub const fn stream_name(&self) -> &'static str { + self.target.stream_name() + } + /// Borrow the opaque request correlation id. pub const fn request_id(&self) -> &[u8; REQUEST_ID_BYTES] { &self.request_id @@ -404,6 +427,11 @@ impl ProcessAttachStream { &self.target } + /// Return the fixed named-stream contract for this attachment. + pub const fn stream_name(&self) -> &'static str { + self.target.stream_name() + } + /// Whether the named stream has been closed or cancelled. pub fn is_closed(&self) -> bool { self.state.load(Ordering::Acquire) == STREAM_CLOSED @@ -441,6 +469,18 @@ where self.transport.send(bytes.to_vec()).await } + /// Send one canonical Process named-stream request frame. + pub async fn send_frame( + &self, + frame: &NamedProcessStreamRequestFrame, + ) -> Result<(), ClientError> { + if frame.request_id == 0 { + return Err(ClientError::ContractViolation); + } + let bytes = serde_json::to_vec(frame).map_err(|_| ClientError::ContractViolation)?; + self.send(&bytes).await + } + /// Deliver a terminal-size control update without interpreting stdin. pub async fn resize(&self, size: TerminalSize) -> Result<(), ClientError> { self.require_open()?; @@ -457,6 +497,17 @@ where result } + /// Receive and decode one canonical Process named-stream response frame. + pub async fn receive_frame(&self) -> Result { + let bytes = self.receive().await?; + let frame: NamedProcessStreamResponseFrame = + serde_json::from_slice(&bytes).map_err(|_| ClientError::ContractViolation)?; + if frame.request_id == 0 { + return Err(ClientError::ContractViolation); + } + Ok(frame) + } + /// Close the named stream exactly once. /// /// Dropping this value cannot perform async I/O. Call `close` when the @@ -983,6 +1034,7 @@ mod tests { ) .await .unwrap(); + assert_eq!(attached.stream_name(), PROCESS_ATTACH_STREAM_NAME); assert_eq!( requested_services.lock().unwrap().as_slice(), &[ZoneServiceKind::Zone] @@ -995,6 +1047,46 @@ mod tests { assert!(attached.is_closed()); } + #[tokio::test(flavor = "current_thread")] + async fn process_attach_stream_round_trips_the_shared_named_frame_codec() { + let response = NamedProcessStreamResponseFrame::new( + 17, + d2b_contracts_control::public_wire::NamedProcessStreamResponse::Delivered( + d2b_contracts_control::public_wire::ExecControlResult { delivered: true }, + ), + ); + let stream = Arc::new(FakeStream { + received: Mutex::new(VecDeque::from([serde_json::to_vec(&response).unwrap()])), + ..Default::default() + }); + let request = NamedProcessStreamRequestFrame::new( + 17, + d2b_contracts_control::public_wire::NamedProcessStreamRequest::Close, + ); + let session = Arc::new(FakeSession::new(vec![Ok(Arc::clone(&stream))], None)); + let requested_services = Arc::new(Mutex::new(Vec::new())); + let client = client(FakeConnector { + session, + pin: pin(ZoneServiceKind::Zone), + requested_services, + }); + let attached = client + .attach( + target(), + ProcessAttachOptions::non_tty(false), + call_options(1), + TransportSelection::exact(TransportKind::LocalUnix), + &CancellationToken::default(), + ) + .await + .unwrap(); + attached.send_frame(&request).await.unwrap(); + let sent: NamedProcessStreamRequestFrame = + serde_json::from_slice(&stream.sent.lock().unwrap()[0]).unwrap(); + assert_eq!(sent, request); + assert_eq!(attached.receive_frame().await.unwrap(), response); + } + #[tokio::test(flavor = "current_thread")] async fn cancellation_stops_a_pending_open_and_does_not_leak_a_stream() { let gate = Arc::new(tokio::sync::Notify::new()); @@ -1095,6 +1187,7 @@ mod tests { .unwrap_err(), ClientError::InvalidTarget ); + assert_eq!(target().stream_name(), PROCESS_ATTACH_STREAM_NAME); assert_eq!( ProcessAttachTarget::configured_launcher( zone("dev"), @@ -1133,6 +1226,7 @@ mod tests { ProcessAttachTarget::shell_session(zone("dev"), shell.clone(), Some(host), false) .expect("qualified shell target"); assert_eq!(target.kind(), ProcessAttachKind::ShellSession); + assert_eq!(target.stream_name(), SHELL_SESSION_STREAM_NAME); assert_eq!(target.resource_ref(), &shell); assert_eq!( diff --git a/packages/d2b-session/src/admission.rs b/packages/d2b-session/src/admission.rs index c53e377b5e..604b5f6ef2 100644 --- a/packages/d2b-session/src/admission.rs +++ b/packages/d2b-session/src/admission.rs @@ -729,6 +729,16 @@ fn validate_ttrpc_permit(permit: &AuthorizedSessionOperation, now_tick: u64) -> } impl AuthenticatedTtrpcHandle { + /// Clone the driver for a named-stream owner that remains under the + /// authenticated ComponentSession lifetime. + /// + /// The returned handle carries no subject or authorization lease. It is + /// exposed only to daemon composition after the session has been + /// registered, where the owning route and target are already fixed. + pub fn component_session_driver(&self) -> SessionDriverHandle { + self.driver.clone() + } + /// Mint an attempt guard that can synchronously fence an admitted write. pub fn attempt_guard(&self) -> crate::Cancellation { crate::Cancellation::new() diff --git a/packages/d2b/src/context.rs b/packages/d2b/src/context.rs index 6ab440ee0c..56377eb159 100644 --- a/packages/d2b/src/context.rs +++ b/packages/d2b/src/context.rs @@ -15,49 +15,24 @@ use std::{ time::{Duration, Instant}, }; +use d2b_contracts_control::public_wire::{ + ExecReadOutputResult, ExecStream, ExecWriteStdinResult, NamedProcessStreamErrorKind, + NamedProcessStreamRequest, NamedProcessStreamRequestFrame, NamedProcessStreamResponse, + NamedProcessStreamResponseFrame, +}; +use d2b_contracts_resource::v3::identity::STANDARD_RESOURCE_TYPES; use d2b_contracts_resource::v3::{ - CanonicalJsonObject, - ResourceErrorKind, - ResourceRef, - ResourceTypeName, - RetryClass, - ZoneId, + CanonicalJsonObject, ResourceErrorKind, ResourceRef, ResourceTypeName, RetryClass, ZoneId, }; use d2b_resource_client::{ - AssignmentIdentity, - CallOptions, - CancellationToken, - ClientError, - ConnectedSession, - ConnectedZoneSession, - MetadataInput, - NamedStreamTransport, - ProcessAttachClient, - ProcessAttachOpenRequest, - ProcessAttachOptions, - ProcessAttachTarget, - ResourceCallOptions, - ResourceVerb, - RetryPolicy, - RouteRecord, - RouteTable, - ScopedResourceMutation, - ServiceOwner, - SystemClock, - TargetInput, - TerminalSize, - TransportKind, - TransportSelection, - WallClock, - ZoneClient, - ZonePeerIdentity, - ZoneServiceKind, - ZoneSessionConnector, - ZoneSessionPin, - ZoneSocketConnector, - resource_verb_is_mutating, + AssignmentIdentity, CallOptions, CancellationToken, ClientError, ConnectedSession, + ConnectedZoneSession, MetadataInput, NamedStreamTransport, ProcessAttachClient, + ProcessAttachOpenRequest, ProcessAttachOptions, ProcessAttachTarget, ResourceCallOptions, + ResourceVerb, RetryPolicy, RouteRecord, RouteTable, ScopedResourceMutation, ServiceOwner, + SystemClock, TargetInput, TerminalSize, TransportKind, TransportSelection, WallClock, + ZoneClient, ZonePeerIdentity, ZoneServiceKind, ZoneSessionConnector, ZoneSessionPin, + ZoneSocketConnector, resource_verb_is_mutating, }; -use d2b_contracts_resource::v3::identity::STANDARD_RESOURCE_TYPES; use serde_json::{Value, json}; use sha2::{Digest, Sha256}; @@ -1277,20 +1252,26 @@ impl std::fmt::Debug for CliConnectedSession { /// close messages on the admitted named stream. struct CliAttachStream { closed: AtomicBool, + teardown_sent: AtomicBool, eof: AtomicBool, socket: Option>>, stdin_offset: Mutex, stdout_offset: AtomicU64, + next_request_id: AtomicU64, + control_sequence: AtomicU64, } impl CliAttachStream { fn new(socket: Option>>) -> Self { Self { closed: AtomicBool::new(false), + teardown_sent: AtomicBool::new(false), eof: AtomicBool::new(false), socket, stdin_offset: Mutex::new(0), stdout_offset: AtomicU64::new(0), + next_request_id: AtomicU64::new(1), + control_sequence: AtomicU64::new(1), } } } @@ -1302,12 +1283,15 @@ impl NamedStreamTransport for CliAttachStream { fn resize(&self, size: TerminalSize) -> impl Future> + Send { ready( - self.stream_round_trip(json!({ - "type": "namedStreamResize", - "rows": size.rows(), - "cols": size.cols(), - })) - .map(|_| ()), + self.stream_round_trip(NamedProcessStreamRequest::Resize { + control_seq: self.control_sequence.fetch_add(1, Ordering::AcqRel), + rows: u32::from(size.rows()), + cols: u32::from(size.cols()), + }) + .and_then(|response| match response { + NamedProcessStreamResponse::Delivered(_) => Ok(()), + _ => Err(ClientError::ContractViolation), + }), ) } @@ -1316,30 +1300,36 @@ impl NamedStreamTransport for CliAttachStream { return ready(Err(ClientError::Cancelled)); } let result = self - .stream_round_trip(json!({ - "type": "namedStreamReceive", - "offset": self.stdout_offset.load(Ordering::Acquire), - "maxLen": d2b_contracts_control::public_wire::EXEC_MAX_CHUNK_BYTES, - })) - .and_then(|value| { - let data = value - .get("dataBase64") - .and_then(Value::as_str) - .ok_or(ClientError::ContractViolation) - .and_then(|data| { - d2b_core::base64_codec::decode(data) - .map_err(|_| ClientError::ContractViolation) - })?; - if let Some(offset) = value.get("nextOffset").and_then(Value::as_u64) { - self.stdout_offset.store(offset, Ordering::Release); + .stream_round_trip(NamedProcessStreamRequest::Read { + stream: ExecStream::Stdout, + offset: self.stdout_offset.load(Ordering::Acquire), + max_len: d2b_contracts_control::public_wire::EXEC_MAX_CHUNK_BYTES, + wait: true, + timeout_ms: 50, + }) + .and_then(|response| match response { + NamedProcessStreamResponse::Output(ExecReadOutputResult { + data_base64, + next_offset, + eof, + .. + }) => { + let data = d2b_core::base64_codec::decode(&data_base64) + .map_err(|_| ClientError::ContractViolation)?; + self.stdout_offset.store(next_offset, Ordering::Release); + if eof { + self.eof.store(true, Ordering::Release); + if data.is_empty() { + return Err(ClientError::Cancelled); + } + } + Ok(data) } - if value.get("eof").and_then(Value::as_bool) == Some(true) { + NamedProcessStreamResponse::Terminal(_) => { self.eof.store(true, Ordering::Release); - if data.is_empty() { - return Err(ClientError::Cancelled); - } + Err(ClientError::Cancelled) } - Ok(data) + _ => Err(ClientError::ContractViolation), }); ready(result) } @@ -1347,26 +1337,48 @@ impl NamedStreamTransport for CliAttachStream { fn close(&self) -> impl Future> + Send { self.closed.store(true, Ordering::Release); let result = if self.socket.is_some() { - self.stream_round_trip(json!({"type": "namedStreamClose"})) - .map(|_| ()) + self.stream_round_trip(NamedProcessStreamRequest::Close) + .and_then(|response| match response { + NamedProcessStreamResponse::Closed(_) => Ok(()), + _ => Err(ClientError::ContractViolation), + }) } else { Ok(()) }; + if result.is_ok() { + self.teardown_sent.store(true, Ordering::Release); + } ready(result) } fn cancel(&self) -> impl Future> + Send { self.closed.store(true, Ordering::Release); let result = if self.socket.is_some() { - self.stream_round_trip(json!({"type": "namedStreamCancel"})) - .map(|_| ()) + self.stream_round_trip(NamedProcessStreamRequest::Cancel) + .and_then(|response| match response { + NamedProcessStreamResponse::Closed(_) + | NamedProcessStreamResponse::Delivered(_) => Ok(()), + _ => Err(ClientError::ContractViolation), + }) } else { Ok(()) }; + if result.is_ok() { + self.teardown_sent.store(true, Ordering::Release); + } ready(result) } } +impl Drop for CliAttachStream { + fn drop(&mut self) { + if self.teardown_sent.swap(true, Ordering::AcqRel) || self.socket.is_none() { + return; + } + let _ = self.stream_round_trip(NamedProcessStreamRequest::Cancel); + } +} + impl CliAttachStream { fn send_stdin(&self, bytes: &[u8]) -> Result<(), ClientError> { let mut offset = self @@ -1376,30 +1388,32 @@ impl CliAttachStream { let deadline = Instant::now() + Duration::from_secs(5); let mut consumed = 0; while consumed < bytes.len() { - let value = self.stream_round_trip(json!({ - "type": "namedStreamSend", - "offset": *offset, - "dataBase64": d2b_core::base64_codec::encode(&bytes[consumed..]), - }))?; - let accepted = value - .get("acceptedLen") - .and_then(Value::as_u64) - .and_then(|value| usize::try_from(value).ok()) - .ok_or(ClientError::ContractViolation)?; - let next_offset = value - .get("nextOffset") - .and_then(Value::as_u64) - .ok_or(ClientError::ContractViolation)?; + let response = self.stream_round_trip(NamedProcessStreamRequest::Stdin { + offset: *offset, + chunk_base64: d2b_core::base64_codec::encode(&bytes[consumed..]), + eof: false, + })?; + let NamedProcessStreamResponse::Stdin(ExecWriteStdinResult { + accepted_len, + next_offset, + backpressured, + stdin_closed, + }) = response + else { + return Err(ClientError::ContractViolation); + }; + let accepted = + usize::try_from(accepted_len).map_err(|_| ClientError::ContractViolation)?; if accepted > bytes.len() - consumed || next_offset != (*offset).saturating_add(accepted as u64) { return Err(ClientError::ContractViolation); } if accepted == 0 { - if value.get("stdinClosed").and_then(Value::as_bool) == Some(true) { + if stdin_closed { return Err(ClientError::SessionLost); } - if value.get("backpressured").and_then(Value::as_bool) != Some(true) { + if !backpressured { return Err(ClientError::ContractViolation); } if Instant::now() >= deadline { @@ -1417,23 +1431,60 @@ impl CliAttachStream { Ok(()) } - fn stream_round_trip(&self, request: Value) -> Result { + fn stream_round_trip( + &self, + request: NamedProcessStreamRequest, + ) -> Result { let socket = self.socket.as_ref().ok_or(ClientError::ContractViolation)?; - let bytes = serde_json::to_vec(&request).map_err(|_| ClientError::ContractViolation)?; + let request_id = self.next_request_id.fetch_add(1, Ordering::AcqRel); + if request_id == 0 { + return Err(ClientError::ContractViolation); + } + let frame = NamedProcessStreamRequestFrame::new(request_id, request); + let bytes = serde_json::to_vec(&frame).map_err(|_| ClientError::ContractViolation)?; let mut socket = socket.lock().map_err(|_| ClientError::SessionLost)?; socket .send_frame(&bytes) .map_err(|_| ClientError::TransportFailed)?; let response = socket.recv_frame().map_err(|_| ClientError::SessionLost)?; - let value: Value = + let frame: NamedProcessStreamResponseFrame = serde_json::from_slice(&response).map_err(|_| ClientError::ContractViolation)?; - if value.get("type").and_then(Value::as_str) == Some("error") { - return Err(remote_client_error(&value)); + if frame.request_id != request_id { + return Err(ClientError::ContractViolation); } - Ok(value) + if let NamedProcessStreamResponse::Error(error) = frame.response { + return Err(named_stream_client_error(error.kind)); + } + Ok(frame.response) } } +fn named_stream_client_error(kind: NamedProcessStreamErrorKind) -> ClientError { + let (kind, retry) = match kind { + NamedProcessStreamErrorKind::Authorization => ( + ResourceErrorKind::AuthorizationDenied, + RetryClass::Reauthorize, + ), + NamedProcessStreamErrorKind::StaleSession | NamedProcessStreamErrorKind::NotFound => { + (ResourceErrorKind::ResourceNotFound, RetryClass::Never) + } + NamedProcessStreamErrorKind::Backpressure => { + (ResourceErrorKind::Backpressure, RetryClass::AfterDelay) + } + NamedProcessStreamErrorKind::Protocol => { + (ResourceErrorKind::ResourceSchemaInvalid, RetryClass::Never) + } + NamedProcessStreamErrorKind::Timeout => { + (ResourceErrorKind::Timeout, RetryClass::AfterDelay) + } + NamedProcessStreamErrorKind::Disconnected => ( + ResourceErrorKind::ResourceProviderUnavailable, + RetryClass::AfterDelay, + ), + }; + ClientError::Remote { kind, retry } +} + impl ConnectedZoneSession for CliConnectedSession { fn call( &self, @@ -2383,6 +2434,29 @@ mod tests { assert!(stream.closed.load(Ordering::Acquire)); } + #[test] + fn cli_attach_stream_drop_sends_a_typed_cancel_frame() { + let (client, server) = socketpair( + AddressFamily::Unix, + SockType::SeqPacket, + None, + SockFlag::SOCK_CLOEXEC, + ) + .unwrap(); + let server = std::thread::spawn(move || { + let mut server = SeqpacketUnixSocket::from_owned_fd(server); + let request: NamedProcessStreamRequestFrame = + serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); + assert_eq!(request.request_id, 1); + assert!(matches!(request.request, NamedProcessStreamRequest::Cancel)); + }); + let stream = CliAttachStream::new(Some(Arc::new(Mutex::new( + SeqpacketUnixSocket::from_owned_fd(client), + )))); + drop(stream); + server.join().unwrap(); + } + #[test] fn cli_attach_stream_retries_partial_stdin_writes() { let (client, server) = socketpair( @@ -2394,35 +2468,65 @@ mod tests { .unwrap(); let server = std::thread::spawn(move || { let mut server = SeqpacketUnixSocket::from_owned_fd(server); - let first: Value = serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); - assert_eq!(first["type"], "namedStreamSend"); - assert_eq!(first["offset"], 0); + let first: NamedProcessStreamRequestFrame = + serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); + assert_eq!(first.request_id, 1); + let NamedProcessStreamRequest::Stdin { + offset, + chunk_base64, + eof, + } = first.request + else { + panic!("expected stdin frame"); + }; + assert_eq!(offset, 0); + assert!(!eof); + assert_eq!( + d2b_core::base64_codec::decode(&chunk_base64).unwrap(), + b"abc" + ); server .send_frame( - &serde_json::to_vec(&json!({ - "acceptedLen": 1, - "nextOffset": 1, - "backpressured": true, - "stdinClosed": false, - })) + &serde_json::to_vec(&NamedProcessStreamResponseFrame::new( + 1, + NamedProcessStreamResponse::Stdin(ExecWriteStdinResult { + accepted_len: 1, + next_offset: 1, + backpressured: true, + stdin_closed: false, + }), + )) .unwrap(), ) .unwrap(); - let second: Value = serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); - assert_eq!(second["type"], "namedStreamSend"); - assert_eq!(second["offset"], 1); + let second: NamedProcessStreamRequestFrame = + serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); + assert_eq!(second.request_id, 2); + let NamedProcessStreamRequest::Stdin { + offset, + chunk_base64, + eof, + } = second.request + else { + panic!("expected stdin frame"); + }; + assert_eq!(offset, 1); + assert!(!eof); assert_eq!( - d2b_core::base64_codec::decode(second["dataBase64"].as_str().unwrap()).unwrap(), + d2b_core::base64_codec::decode(&chunk_base64).unwrap(), b"bc" ); server .send_frame( - &serde_json::to_vec(&json!({ - "acceptedLen": 2, - "nextOffset": 3, - "backpressured": false, - "stdinClosed": false, - })) + &serde_json::to_vec(&NamedProcessStreamResponseFrame::new( + 2, + NamedProcessStreamResponse::Stdin(ExecWriteStdinResult { + accepted_len: 2, + next_offset: 3, + backpressured: false, + stdin_closed: false, + }), + )) .unwrap(), ) .unwrap(); @@ -2446,15 +2550,29 @@ mod tests { .unwrap(); let server = std::thread::spawn(move || { let mut server = SeqpacketUnixSocket::from_owned_fd(server); - let request: Value = serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); - assert_eq!(request["type"], "namedStreamReceive"); + let request: NamedProcessStreamRequestFrame = + serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); + assert_eq!(request.request_id, 1); + assert!(matches!( + request.request, + NamedProcessStreamRequest::Read { + stream: ExecStream::Stdout, + .. + } + )); server .send_frame( - &serde_json::to_vec(&json!({ - "dataBase64": d2b_core::base64_codec::encode(b"done"), - "nextOffset": 4, - "eof": true, - })) + &serde_json::to_vec(&NamedProcessStreamResponseFrame::new( + 1, + NamedProcessStreamResponse::Output(ExecReadOutputResult { + data_base64: d2b_core::base64_codec::encode(b"done"), + next_offset: 4, + eof: true, + dropped_bytes: 0, + truncated: false, + timed_out: false, + }), + )) .unwrap(), ) .unwrap(); @@ -2483,19 +2601,32 @@ mod tests { let expected = stdin.clone(); let server = std::thread::spawn(move || { let mut server = SeqpacketUnixSocket::from_owned_fd(server); - let request: Value = serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); - assert_eq!(request["type"], "namedStreamSend"); - let data = - d2b_core::base64_codec::decode(request["dataBase64"].as_str().unwrap()).unwrap(); + let request: NamedProcessStreamRequestFrame = + serde_json::from_slice(&server.recv_frame().unwrap()).unwrap(); + let NamedProcessStreamRequest::Stdin { + offset, + chunk_base64, + eof, + } = request.request + else { + panic!("expected stdin frame"); + }; + assert_eq!(request.request_id, 1); + assert_eq!(offset, 0); + assert!(!eof); + let data = d2b_core::base64_codec::decode(&chunk_base64).unwrap(); assert_eq!(data, expected); server .send_frame( - &serde_json::to_vec(&json!({ - "acceptedLen": data.len(), - "nextOffset": data.len(), - "backpressured": false, - "stdinClosed": false, - })) + &serde_json::to_vec(&NamedProcessStreamResponseFrame::new( + 1, + NamedProcessStreamResponse::Stdin(ExecWriteStdinResult { + accepted_len: data.len() as u64, + next_offset: data.len() as u64, + backpressured: false, + stdin_closed: false, + }), + )) .unwrap(), ) .unwrap(); diff --git a/packages/d2b/src/exec_client.rs b/packages/d2b/src/exec_client.rs index 3036b3886a..f0adfa376e 100644 --- a/packages/d2b/src/exec_client.rs +++ b/packages/d2b/src/exec_client.rs @@ -1,9 +1,8 @@ //! CLI-side `d2b vm exec` owner-connection FSM + host terminal safety. //! -//! `d2b vm exec` routes one owner connection over the daemon `public.sock` -//! `exec` verb: a single `Start` op establishes the daemon-held authenticated -//! guest-control session, then the remaining ops -//! (`WriteStdin`/`ReadOutput`/`Signal`/`Resize`/`Wait`/`Close`) drive it. The +//! `d2b vm exec` establishes one Process/EphemeralProcess resource owner over +//! the daemon `public.sock`, then drives the authenticated named stream with +//! (`WriteStdin`/`ReadOutput`/`Signal`/`Resize`/`Wait`/`Close`) operations. The //! CLI never opens a new connection per op and never allocates a host PTY - //! the guest owns the PTY (helper-exec). This module is the pure FSM + //! host-termios safety; the real socket/signal/host wiring lives in `lib.rs` @@ -16,7 +15,9 @@ use std::sync::{Arc, Mutex}; use d2b_contracts_control::public_wire::{ ExecCloseArgs, ExecOp, ExecOpResponse, ExecReadOutputArgs, ExecReadOutputResult, ExecResizeArgs, ExecSignalArgs, ExecStartResult, ExecStream, ExecTerminalStatus, ExecWaitArgs, - ExecWaitResult, ExecWriteStdinArgs, ExecWriteStdinResult, + ExecWaitResult, ExecWriteStdinArgs, ExecWriteStdinResult, NamedProcessStreamErrorKind, + NamedProcessStreamRequest, NamedProcessStreamRequestFrame, NamedProcessStreamResponse, + NamedProcessStreamResponseFrame, }; use d2b_core::base64_codec; use serde_json::Value; @@ -185,6 +186,10 @@ pub fn exit_for_kind(kind: &str) -> (i32, ExecFailureSource) { "guest-control-transport-unavailable" | "guest-control-timeout" => { (EXIT_EXEC_TRANSPORT, ExecFailureSource::Transport) } + "resource-provider-unavailable" | "resource-not-found" | "session-disconnected" => { + (EXIT_EXEC_TRANSPORT, ExecFailureSource::Transport) + } + "backpressure" => (EXIT_EXEC_CAPACITY, ExecFailureSource::GuestControl), "guest-control-unavailable-old-generation" | "guest-control-capability-unavailable" | "guest-control-exec-detached-unavailable" => { @@ -199,7 +204,9 @@ pub fn exit_for_kind(kind: &str) -> (i32, ExecFailureSource) { | "guest-control-exec-expired" => (EXIT_EXEC_PROTOCOL, ExecFailureSource::Protocol), "guest-control-invalid-program" => (2, ExecFailureSource::GuestControl), "guest-control-auth-failed" => (EXIT_EXEC_AUTH, ExecFailureSource::GuestControl), - "guest-control-stale-session" => (EXIT_EXEC_AUTH, ExecFailureSource::GuestControl), + "guest-control-stale-session" | "stale-session" => { + (EXIT_EXEC_AUTH, ExecFailureSource::GuestControl) + } // The daemon's admin gate refused the caller before any guest contact // (caller not in `d2b.site.adminUsers`). It is an authorization // failure, NOT an internal bug - map it to the AUTH reserved code so @@ -210,6 +217,145 @@ pub fn exit_for_kind(kind: &str) -> (i32, ExecFailureSource) { } } +/// Translate one established Exec operation to the authenticated named-stream +/// request carried by its Process or ShellSession attachment. +/// +/// Start and detached-management operations are Resource API calls and +/// therefore do not belong on an already-open named stream. +pub fn named_stream_request(op: &ExecOp) -> Result { + named_stream_request_with_offset(op, 0) +} + +/// Translate an Exec operation while supplying the current stdin cursor for a +/// `Close` half-close. +pub fn named_stream_request_with_offset( + op: &ExecOp, + stdin_offset: u64, +) -> Result { + match op { + ExecOp::WriteStdin(args) => Ok(NamedProcessStreamRequest::Stdin { + offset: args.offset, + chunk_base64: args.chunk_base64.clone(), + eof: args.eof, + }), + ExecOp::ReadOutput(args) => Ok(NamedProcessStreamRequest::Read { + stream: args.stream, + offset: args.offset, + max_len: args.max_len, + wait: args.wait, + timeout_ms: args.timeout_ms, + }), + ExecOp::Signal(args) => Ok(NamedProcessStreamRequest::Signal { + control_seq: args.op_id, + signo: args.signo, + }), + ExecOp::Resize(args) => Ok(NamedProcessStreamRequest::Resize { + control_seq: args.op_id, + rows: args.rows, + cols: args.cols, + }), + ExecOp::Wait(args) => Ok(NamedProcessStreamRequest::Wait { + timeout_ms: args.timeout_ms, + }), + ExecOp::Close(_) => Ok(NamedProcessStreamRequest::CloseStdin { + offset: stdin_offset, + }), + ExecOp::Start(_) + | ExecOp::List(_) + | ExecOp::Logs(_) + | ExecOp::Status(_) + | ExecOp::Kill(_) => Err(ExecClientError::protocol( + "operation is not valid on a Process named stream", + )), + } +} + +/// Wrap one named-stream request in its bounded in-stream correlation frame. +pub fn named_stream_request_frame( + op: &ExecOp, + request_id: u64, + stdin_offset: u64, +) -> Result { + if request_id == 0 { + return Err(ExecClientError::protocol( + "named-stream request id must be non-zero", + )); + } + Ok(NamedProcessStreamRequestFrame::new( + request_id, + named_stream_request_with_offset(op, stdin_offset)?, + )) +} + +/// Translate one named-stream response into the existing Exec wire result. +pub fn named_stream_response( + response: NamedProcessStreamResponse, +) -> Result { + named_stream_response_for(None, response) +} + +/// Decode one correlated named-stream response and restore the existing Exec +/// result envelope. +pub fn named_stream_response_frame( + op: &ExecOp, + bytes: &[u8], +) -> Result<(u64, ExecOpResponse), ExecClientError> { + let frame: NamedProcessStreamResponseFrame = serde_json::from_slice(bytes) + .map_err(|_| ExecClientError::protocol("named-stream response frame was malformed"))?; + if frame.request_id == 0 { + return Err(ExecClientError::protocol( + "named-stream response id must be non-zero", + )); + } + let request = named_stream_request(op)?; + Ok(( + frame.request_id, + named_stream_response_for(Some(&request), frame.response)?, + )) +} + +/// Translate a named-stream response while preserving whether a delivered +/// acknowledgement belongs to a signal or resize operation. +pub fn named_stream_response_for( + request: Option<&NamedProcessStreamRequest>, + response: NamedProcessStreamResponse, +) -> Result { + match response { + NamedProcessStreamResponse::Stdin(result) => Ok(ExecOpResponse::WriteStdin(result)), + NamedProcessStreamResponse::Output(result) => Ok(ExecOpResponse::ReadOutput(result)), + NamedProcessStreamResponse::Delivered(result) => match request { + Some(NamedProcessStreamRequest::Resize { .. }) => Ok(ExecOpResponse::Resize(result)), + _ => Ok(ExecOpResponse::Signal(result)), + }, + NamedProcessStreamResponse::Wait(result) => Ok(ExecOpResponse::Wait(result)), + NamedProcessStreamResponse::Closed(result) => Ok(ExecOpResponse::Close( + d2b_contracts_control::public_wire::ExecCloseResult { + stdin_closed: result.stdin_closed, + }, + )), + NamedProcessStreamResponse::Terminal(status) => Ok(ExecOpResponse::Wait(ExecWaitResult { + running: false, + terminal_status: Some(status), + })), + NamedProcessStreamResponse::Error(error) => { + let kind = match error.kind { + NamedProcessStreamErrorKind::Authorization => "guest-control-auth-failed", + NamedProcessStreamErrorKind::StaleSession => "stale-session", + NamedProcessStreamErrorKind::NotFound => "guest-control-exec-not-found", + NamedProcessStreamErrorKind::Backpressure => "backpressure", + NamedProcessStreamErrorKind::Protocol => "guest-control-protocol-error", + NamedProcessStreamErrorKind::Timeout => "guest-control-timeout", + NamedProcessStreamErrorKind::Disconnected => "session-disconnected", + }; + Err(ExecClientError::from_daemon_error( + kind, + "the authenticated Process named stream rejected the operation", + "inspect the Process resource status and retry when its ComponentSession is ready", + )) + } + } +} + /// Host signal events the CLI forwards into the guest exec (enqueue-only; the /// real signal source never touches termios or syscalls in a handler). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -678,23 +824,6 @@ fn write_output( .map_err(|error| ExecClientError::internal(format!("writing host output failed: {error}"))) } -/// Encode an [`ExecOp`] as the `exec` daemon wire frame: the adjacently-tagged -/// `{ "op": …, "args": … }` body with a `type: "exec"` discriminator and an -/// envelope-level `opId` correlation id. The daemon echoes `opId` on -/// the matching response so a pending long-poll and an urgent control reply can -/// be matched out of order. -pub fn encode_exec_op_frame(op: &ExecOp, op_id: u64) -> Result, ExecClientError> { - let mut value = serde_json::to_value(op) - .map_err(|error| ExecClientError::internal(format!("encoding exec op failed: {error}")))?; - let object = value - .as_object_mut() - .ok_or_else(|| ExecClientError::internal("encoded exec op was not a JSON object"))?; - object.insert("type".to_owned(), Value::String("exec".to_owned())); - object.insert("opId".to_owned(), Value::from(op_id)); - serde_json::to_vec(&value) - .map_err(|error| ExecClientError::internal(format!("serializing exec op failed: {error}"))) -} - /// Decode an `execResponse` (or `error`) daemon wire frame into an /// [`ExecOpResponse`], mapping a daemon error frame to a typed /// [`ExecClientError`] with the CLI exec exit-code contract. @@ -2013,4 +2142,62 @@ mod tests { assert!(!err.remediation.is_empty()); } } + + #[test] + fn named_stream_mapping_preserves_control_operation_and_redaction() { + let resize = ExecOp::Resize(ExecResizeArgs { + session: "opaque-session".to_owned(), + rows: 24, + cols: 80, + op_id: 9, + }); + let request = named_stream_request(&resize).expect("resize maps to named stream"); + assert!(matches!( + request, + NamedProcessStreamRequest::Resize { + control_seq: 9, + rows: 24, + cols: 80 + } + )); + let close = named_stream_request_with_offset( + &ExecOp::Close(ExecCloseArgs { + session: "opaque-session".to_owned(), + }), + 12, + ) + .expect("close maps to a named-stream half-close"); + assert!(matches!( + close, + NamedProcessStreamRequest::CloseStdin { offset: 12 } + )); + let frame = named_stream_request_frame(&resize, 17, 12).expect("frame has correlation"); + assert_eq!(frame.request_id, 17); + let response_frame = + d2b_contracts_control::public_wire::NamedProcessStreamResponseFrame::new( + 17, + NamedProcessStreamResponse::Delivered(ExecControlResult { delivered: true }), + ); + let (_, decoded) = named_stream_response_frame( + &resize, + &serde_json::to_vec(&response_frame).expect("response frame serializes"), + ) + .expect("response frame decodes"); + assert!(matches!(decoded, ExecOpResponse::Resize(_))); + let response = named_stream_response_for( + Some(&request), + NamedProcessStreamResponse::Delivered(ExecControlResult { delivered: true }), + ) + .expect("resize response maps"); + assert!(matches!(response, ExecOpResponse::Resize(_))); + + let error = named_stream_response(NamedProcessStreamResponse::Error( + d2b_contracts_control::public_wire::NamedProcessStreamError { + kind: NamedProcessStreamErrorKind::Disconnected, + }, + )) + .expect_err("disconnect is a typed failure"); + assert_eq!(error.kind, "session-disconnected"); + assert!(!format!("{error:?}").contains("opaque-session")); + } } diff --git a/packages/d2b/src/legacy.rs b/packages/d2b/src/legacy.rs index 3ac23f2a27..8ca764e12c 100644 --- a/packages/d2b/src/legacy.rs +++ b/packages/d2b/src/legacy.rs @@ -6581,12 +6581,33 @@ pub(super) fn cmd_vm_status( ) } -/// The owner-connection transport: one op per round trip over the held -/// public.sock seqpacket connection. The daemon multiplexes a single -/// authenticated guest-control session behind this connection. +/// The resource-owner transport: one Resource attach request establishes the +/// Process stream, then every operation is a correlated named-stream frame +/// over the held public.sock seqpacket connection. pub(super) struct OwnerSocketTransport { socket: SeqpacketUnixSocket, next_op_id: u64, + stdin_offset: u64, + resource_ref: Option, +} + +impl Drop for OwnerSocketTransport { + fn drop(&mut self) { + if self.resource_ref.is_none() { + return; + } + let request_id = self.next_op_id; + if request_id == 0 { + return; + } + let frame = public_wire::NamedProcessStreamRequestFrame::new( + request_id, + public_wire::NamedProcessStreamRequest::Cancel, + ); + if let Ok(bytes) = serde_json::to_vec(&frame) { + let _ = self.socket.send_frame(&bytes); + } + } } impl terminal_client::TerminalTransport for OwnerSocketTransport { @@ -6601,17 +6622,270 @@ impl terminal_client::TerminalTransport for OwnerSocketTransport { { let op_id = self.next_op_id; self.next_op_id = self.next_op_id.wrapping_add(1); - let frame = exec_client::encode_exec_op_frame(op, op_id)?; + if op_id == 0 { + return Err(exec_client::ExecClientError::protocol( + "process stream request id exhausted", + )); + } + if !matches!(op, public_wire::ExecOp::Start(_)) && self.resource_ref.is_none() { + return Err(exec_client::ExecClientError::protocol( + "process stream operation preceded resource attach", + )); + } + let frame = if let public_wire::ExecOp::Start(start) = op { + if self.resource_ref.is_some() { + return Err(exec_client::ExecClientError::protocol( + "duplicate process resource attach", + )); + } + encode_process_resource_attach(start, op_id)? + } else { + let frame = exec_client::named_stream_request_frame(op, op_id, self.stdin_offset)?; + serde_json::to_vec(&frame).map_err(|_| { + exec_client::ExecClientError::protocol( + "process named-stream request frame was malformed", + ) + })? + }; self.socket.send_frame(&frame).map_err(|err| { - exec_client::ExecClientError::transport(format!("exec op send failed: {err}")) + exec_client::ExecClientError::transport(format!("process stream send failed: {err}")) + })?; + let reply = self.socket.recv_frame().map_err(|err| { + exec_client::ExecClientError::transport(format!("process stream recv failed: {err}")) + })?; + let response = if let public_wire::ExecOp::Start(start) = op { + let result = decode_process_resource_attach(&reply)?; + self.resource_ref = Some(format!("EphemeralProcess/exec-{op_id}")); + let _ = start; + public_wire::ExecOpResponse::Start(result) + } else { + let (response_id, response) = exec_client::named_stream_response_frame(op, &reply)?; + if response_id != op_id { + return Err(exec_client::ExecClientError::protocol( + "process named-stream response id did not match request", + )); + } + response + }; + if let public_wire::ExecOpResponse::WriteStdin(result) = &response { + self.stdin_offset = result.next_offset; + } + Ok(response) + } +} + +impl OwnerSocketTransport { + fn resource_management_round_trip( + &mut self, + op: &public_wire::ExecOp, + ) -> Result { + let request = encode_process_resource_management(op)?; + self.socket.send_frame(&request).map_err(|err| { + exec_client::ExecClientError::transport(format!( + "process resource request send failed: {err}" + )) + })?; + let reply = self.socket.recv_frame().map_err(|err| { + exec_client::ExecClientError::transport(format!( + "process resource response receive failed: {err}" + )) + })?; + exec_client::decode_exec_response_frame(&reply) + } + + fn resource_detached_create_round_trip( + &mut self, + start: &public_wire::ExecStartArgs, + ) -> Result { + let request = encode_process_resource_detached_create(start, self.next_op_id)?; + self.next_op_id = self.next_op_id.wrapping_add(1); + self.socket.send_frame(&request).map_err(|err| { + exec_client::ExecClientError::transport(format!( + "detached Process resource request send failed: {err}" + )) })?; let reply = self.socket.recv_frame().map_err(|err| { - exec_client::ExecClientError::transport(format!("exec op recv failed: {err}")) + exec_client::ExecClientError::transport(format!( + "detached Process resource response receive failed: {err}" + )) })?; exec_client::decode_exec_response_frame(&reply) } } +fn encode_process_resource_management( + op: &public_wire::ExecOp, +) -> Result, exec_client::ExecClientError> { + let (method, vm, resource_ref, extra) = match op { + public_wire::ExecOp::List(args) => ("List", args.vm.clone(), None, serde_json::json!({})), + public_wire::ExecOp::Status(args) => ( + "Status", + args.vm.clone(), + Some(format!("EphemeralProcess/{}", args.exec_id)), + serde_json::json!({}), + ), + public_wire::ExecOp::Logs(args) => ( + "Logs", + args.vm.clone(), + Some(format!("EphemeralProcess/{}", args.exec_id)), + serde_json::json!({ + "stdoutOffset": args.stdout_offset, + "stderrOffset": args.stderr_offset, + "maxLen": args.max_len, + }), + ), + public_wire::ExecOp::Kill(args) => ( + "Kill", + args.vm.clone(), + Some(format!("EphemeralProcess/{}", args.exec_id)), + serde_json::json!({}), + ), + _ => { + return Err(exec_client::ExecClientError::protocol( + "operation is not detached Process resource management", + )); + } + }; + let mut request = serde_json::json!({ + "type": "resourceRequest", + "method": method, + "service": "d2b.resource.v3", + "sessionVerb": "invoke", + "zoneRef": "Zone/local-root", + "resourceType": "EphemeralProcess", + "executionRef": format!("Guest/{vm}"), + }); + if let Some(resource_ref) = resource_ref { + request["resourceRef"] = Value::String(resource_ref); + } + if let (Some(object), Some(extra)) = (request.as_object_mut(), extra.as_object()) { + object.extend( + extra + .iter() + .map(|(key, value)| (key.clone(), value.clone())), + ); + } + serde_json::to_vec(&request).map_err(|_| { + exec_client::ExecClientError::protocol("process resource management request was malformed") + }) +} + +fn encode_process_resource_attach( + start: &public_wire::ExecStartArgs, + request_id: u64, +) -> Result, exec_client::ExecClientError> { + let resource_ref = format!("EphemeralProcess/exec-{request_id}"); + let initial_size = start + .term_size + .map(|size| serde_json::json!({ "rows": size.rows, "cols": size.cols })); + let request = serde_json::json!({ + "type": "resourceRequest", + "method": "Create", + "service": "d2b.resource.v3", + "sessionVerb": "attach", + "zoneRef": "Zone/local-root", + "resourceType": "EphemeralProcess", + "resourceRef": resource_ref, + "executionRef": format!("Guest/{}", start.vm), + "interactive": start.tty, + "tty": start.tty, + "initialSize": initial_size, + "detached": false, + "argv": start.argv, + "env": start.env, + "cwd": start.cwd, + "opId": request_id, + }); + serde_json::to_vec(&request).map_err(|_| { + exec_client::ExecClientError::protocol("process resource attach request was malformed") + }) +} + +fn encode_process_resource_detached_create( + start: &public_wire::ExecStartArgs, + request_id: u64, +) -> Result, exec_client::ExecClientError> { + let request = serde_json::json!({ + "type": "resourceRequest", + "method": "Create", + "service": "d2b.resource.v3", + "sessionVerb": "invoke", + "zoneRef": "Zone/local-root", + "resourceType": "EphemeralProcess", + "resourceRef": format!("EphemeralProcess/exec-{request_id}"), + "executionRef": format!("Guest/{}", start.vm), + "interactive": false, + "tty": false, + "initialSize": Value::Null, + "detached": true, + "argv": start.argv, + "env": start.env, + "cwd": start.cwd, + "opId": request_id, + }); + serde_json::to_vec(&request).map_err(|_| { + exec_client::ExecClientError::protocol( + "detached Process resource create request was malformed", + ) + }) +} + +fn decode_process_resource_attach( + bytes: &[u8], +) -> Result { + let value: Value = serde_json::from_slice(bytes).map_err(|_| { + exec_client::ExecClientError::protocol("process resource attach response was malformed") + })?; + if value.get("type").and_then(Value::as_str) == Some("error") { + let error = value.get("error").unwrap_or(&value); + let kind = error + .get("kind") + .and_then(Value::as_str) + .unwrap_or("resource-provider-unavailable"); + let message = error + .get("message") + .and_then(Value::as_str) + .unwrap_or("the Process resource attach was refused"); + let remediation = error + .get("remediation") + .and_then(Value::as_str) + .unwrap_or(""); + return Err(exec_client::ExecClientError::from_daemon_error( + kind, + message, + remediation, + )); + } + if value.get("attached").and_then(Value::as_bool) != Some(true) { + return Err(exec_client::ExecClientError::protocol( + "process resource attach did not establish a stream", + )); + } + let session = value + .get("session") + .and_then(Value::as_str) + .ok_or_else(|| { + exec_client::ExecClientError::protocol( + "process resource attach response omitted its session", + ) + })?; + let tty = value.get("tty").and_then(Value::as_bool).unwrap_or(false); + let stdout_offset = value + .get("stdoutOffset") + .and_then(Value::as_u64) + .unwrap_or(0); + let stderr_offset = value + .get("stderrOffset") + .and_then(Value::as_u64) + .unwrap_or(0); + Ok(public_wire::ExecStartResult { + session: session.to_owned(), + tty, + stdout_offset, + stderr_offset, + }) +} + /// Typed transport error for an unreachable daemon on the exec path: there is /// no SSH fallback, so an absent/unreachable daemon is a transport failure. pub(super) fn exec_daemon_unavailable_error() -> exec_client::ExecClientError { @@ -6650,7 +6924,9 @@ pub(super) fn exec_owner_transport( .map_err(|failure| exec_client::ExecClientError::protocol(failure.message))?; Ok(OwnerSocketTransport { socket, - next_op_id: 0, + next_op_id: 1, + stdin_offset: 0, + resource_ref: None, }) } @@ -6866,10 +7142,10 @@ pub(super) fn parse_vm_exec_u64_flag(flag: &str, value: &str) -> Result Result { use d2b_contracts_control::public_wire::{ExecEnvVar, ExecOp, ExecStartArgs, ExecTermSize}; @@ -6996,7 +7272,14 @@ pub(super) fn cmd_vm_exec(context: &LegacyContext, args: &VmExecArgs) -> Result< Ok(transport) => transport, Err(err) => return exec_terminate(args, err), }; - let start_response = match transport.round_trip(&start_op) { + let start_response = match if args.detach { + let public_wire::ExecOp::Start(start) = &start_op else { + unreachable!("the command path always builds ExecOp::Start"); + }; + transport.resource_detached_create_round_trip(start) + } else { + transport.round_trip(&start_op) + } { Ok(response) => response, Err(err) => { return exec_terminate(args, err); @@ -7202,7 +7485,7 @@ pub(super) fn exec_send_one_op( op: d2b_contracts_control::public_wire::ExecOp, ) -> Result { let mut transport = exec_owner_transport(context)?; - transport.round_trip(&op) + transport.resource_management_round_trip(&op) } pub(super) fn exec_render_detached_create( @@ -12527,11 +12810,11 @@ mod host_install_dispatch_tests { } /// Drive `cmd_vm_exec` (json) against a mock daemon that completes the - /// hello handshake, accepts the `Start` op, and replies with the daemon - /// `error` frame whose `kind` is supplied. Returns the CLI result plus the - /// list of post-hello frames the daemon received before the response. - /// Attached and detached-create forms send `Start`; management verbs send - /// their single management op. + /// hello handshake, accepts the Process resource request, and replies + /// with the daemon frame whose `kind` is supplied. Returns the CLI result + /// plus the list of post-hello frames the daemon received before the + /// response. Attached and detached-create forms send a Process resource + /// create; management verbs send their single resource operation. fn run_vm_exec_with_mock_daemon_response( args: VmExecArgs, response_frame: Value, @@ -12583,8 +12866,7 @@ mod host_install_dispatch_tests { ) .expect("encode hello reply"); send_test_frame(accepted, &hello_reply)?; - // First post-hello frame: the Start op. - // First post-hello frame: the Start op. + // First post-hello frame: the Process resource operation. let start_bytes = recv_test_frame(accepted)?; let start: Value = serde_json::from_slice(&start_bytes) .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; @@ -12715,17 +12997,19 @@ mod host_install_dispatch_tests { "a failure envelope never carries captured stdio bytes: {envelope}" ); // The daemon received exactly ONE post-hello frame before the - // terminal response: the Start establishment op. + // terminal response: the Process resource create request. assert_eq!( frames.len(), 1, - "exactly the rejected Start may be sent; no proxied op may follow" + "exactly the rejected resource create may be sent; no proxied op may follow" ); assert_eq!( - frames[0].get("op").and_then(Value::as_str), - Some("start"), - "the single proxied frame is the Start op" + frames[0].get("type").and_then(Value::as_str), + Some("resourceRequest"), + "the single establishment frame is a Resource request" ); + assert_eq!(frames[0]["method"], "Create"); + assert_eq!(frames[0]["resourceType"], "EphemeralProcess"); // No SSH/SCP client may be spawned on the fail-closed exec path: the // "exactly one frame (the Start)" + "exit 70" assertions above prove // the path stops before any further transport, and the crate-wide @@ -13122,11 +13406,21 @@ mod host_install_dispatch_tests { assert_eq!(human_result.expect("detached create human"), 0); assert_eq!(String::from_utf8(human_stdout).unwrap(), "exec-abc\n"); assert_eq!( - human_frames[0] - .pointer("/args/detached") - .and_then(Value::as_bool), + human_frames[0].get("detached").and_then(Value::as_bool), Some(true) ); + assert_eq!( + human_frames[0].get("method").and_then(Value::as_str), + Some("Create") + ); + assert_eq!( + human_frames[0].get("sessionVerb").and_then(Value::as_str), + Some("invoke") + ); + assert_eq!( + human_frames[0].get("resourceType").and_then(Value::as_str), + Some("EphemeralProcess") + ); let json_args = parse_vm_exec(&["d2b", "vm", "exec", "-d", "work", "--json", "--", "id"]); let (json_result, _json_frames, json_stdout) = run_vm_exec_with_mock_daemon_response( @@ -13185,9 +13479,11 @@ mod host_install_dispatch_tests { ); assert_eq!(list_result.expect("list json"), 0); assert_eq!( - list_frames[0].get("op").and_then(Value::as_str), - Some("list") + list_frames[0].get("method").and_then(Value::as_str), + Some("List") ); + assert_eq!(list_frames[0]["resourceType"], "EphemeralProcess"); + assert_eq!(list_frames[0]["executionRef"], "Guest/work"); let list_envelope: Value = serde_json::from_slice(&list_stdout).expect("list JSON"); assert_eq!( list_envelope, @@ -13297,21 +13593,15 @@ mod host_install_dispatch_tests { ); assert_eq!(logs_result.expect("logs json"), 0); assert_eq!( - logs_frames[0] - .pointer("/args/stdoutOffset") - .and_then(Value::as_i64), + logs_frames[0].get("stdoutOffset").and_then(Value::as_i64), Some(4) ); assert_eq!( - logs_frames[0] - .pointer("/args/stderrOffset") - .and_then(Value::as_i64), + logs_frames[0].get("stderrOffset").and_then(Value::as_i64), Some(8) ); assert_eq!( - logs_frames[0] - .pointer("/args/maxLen") - .and_then(Value::as_i64), + logs_frames[0].get("maxLen").and_then(Value::as_i64), Some(16) ); let logs_envelope: Value = serde_json::from_slice(&logs_stdout).expect("logs JSON"); @@ -16493,9 +16783,12 @@ mod exec_json_envelope_tests { //! status number (the 70-vs-70 case): `source` + `reason` + //! `guestExitCode`/`transportExitCode` carry the distinction. - use d2b_contracts_control::public_wire::ExecTerminalStatus; + use d2b_contracts_control::public_wire::{ExecStartArgs, ExecTerminalStatus}; - use super::{VmExecArgs, exec_client, exec_json_failure_value, exec_json_success_value}; + use super::{ + VmExecArgs, encode_process_resource_attach, exec_client, exec_json_failure_value, + exec_json_success_value, + }; fn exec_args(vm: &str) -> VmExecArgs { VmExecArgs { @@ -16549,6 +16842,29 @@ mod exec_json_envelope_tests { assert!(value.get("stdoutBase64").is_none()); assert!(value.get("stderrBase64").is_none()); } + + #[test] + fn exec_start_uses_a_process_resource_request_not_the_retired_exec_wire() { + let bytes = encode_process_resource_attach( + &ExecStartArgs { + vm: "work".to_owned(), + argv: vec!["true".to_owned()], + tty: false, + detached: false, + env: None, + cwd: None, + term_size: None, + }, + 9, + ) + .expect("resource attach frame"); + let value: serde_json::Value = serde_json::from_slice(&bytes).expect("JSON request"); + assert_eq!(value["type"], "resourceRequest"); + assert_eq!(value["method"], "Create"); + assert_eq!(value["resourceType"], "EphemeralProcess"); + assert_eq!(value["sessionVerb"], "attach"); + assert_ne!(value["type"], "exec"); + } } #[cfg(test)] diff --git a/packages/d2bd-runtime/src/exec_detached.rs b/packages/d2bd-runtime/src/exec_detached.rs index 1d3670190d..f0db3d2cba 100644 --- a/packages/d2bd-runtime/src/exec_detached.rs +++ b/packages/d2bd-runtime/src/exec_detached.rs @@ -1,10 +1,9 @@ -//! One-shot detached guest-control exec routing. +//! One-shot detached Process resource routing. //! -//! Attached `vm exec` sessions keep an authenticated guest-control client in the -//! owner FSM. Detached create/list/logs/status/kill are deliberately one-shot: -//! connect, authenticate, issue exactly one management RPC (two for logs/kill -//! where the guest protocol requires it), return a redacted public DTO, then -//! drop the client. +//! Production detached create/list/logs/status/kill uses +//! [`ResourceDetachedClient`] and the authenticated Resource API. The former +//! guest-control RPC adapter remains available only to test characterization +//! callers so the daemon composition cannot reach it. use std::path::PathBuf; use std::time::Duration; @@ -18,15 +17,16 @@ use d2b_contracts_control::public_wire::{ ExecDetachedListEntry, ExecDetachedListResult, ExecDetachedLogsResult, ExecDetachedStatusResult, }; +use d2b_contracts_resource::v3::ResourceRef; use d2b_core::base64_codec; use protobuf::{EnumOrUnknown, MessageField}; use crate::exec_session::{ExecOpDeadlines, ExecOpError, ExecStartSpec, GuestOpError}; +use crate::exec_session_real; use crate::guest_control_bridge::{ BrokerSigner, GUEST_CONTROL_ATTEMPT_CAP, ProbeParams, VMADDR_CID_HOST, connect_and_build_client, host_nonce, }; -use crate::exec_session_real; use crate::guest_control_health::{ AttemptBudget, GuestControlHealthError, TtrpcGuestControlClient, probe_guest_control_health, }; @@ -56,6 +56,143 @@ const DETACHED_CREATE_GUEST_WINDOW: Duration = Duration::from_millis(10_000); #[cfg(test)] const DETACHED_CANCEL_GUEST_WINDOW: Duration = Duration::from_millis(15_000); +/// Resource API seam for detached EphemeralProcess management. +/// +/// Implementations perform the authenticated Resource API calls and named +/// stream/log attachment. No broker role, guest socket, process identifier, or +/// transport locator crosses this boundary. +#[async_trait] +pub trait DetachedProcessResourcePort: Send + Sync { + /// Create one detached EphemeralProcess. + async fn create_ephemeral_process( + &self, + execution_ref: &ResourceRef, + spec: &ExecStartSpec, + ) -> Result; + + /// List detached EphemeralProcess resources under one execution target. + async fn list_ephemeral_processes( + &self, + execution_ref: &ResourceRef, + ) -> Result; + + /// Read one EphemeralProcess status. + async fn status_ephemeral_process( + &self, + process_ref: &ResourceRef, + ) -> Result; + + /// Read retained EphemeralProcess output. + async fn logs_ephemeral_process( + &self, + process_ref: &ResourceRef, + stdout_offset: Option, + stderr_offset: Option, + max_len: Option, + ) -> Result; + + /// Cancel one EphemeralProcess. + async fn kill_ephemeral_process( + &self, + process_ref: &ResourceRef, + ) -> Result; +} + +/// Detached execution facade over the authenticated Resource API. +pub struct ResourceDetachedClient

{ + port: P, + execution_ref: ResourceRef, +} + +impl

std::fmt::Debug for ResourceDetachedClient

{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("ResourceDetachedClient()") + } +} + +impl

ResourceDetachedClient

{ + /// Bind detached operations to one exact Host or Guest execution target. + pub fn new(port: P, execution_ref: ResourceRef) -> Result { + if !matches!(execution_ref.resource_type().as_str(), "Host" | "Guest") { + return Err(ExecOpError::Protocol); + } + Ok(Self { + port, + execution_ref, + }) + } + + /// Borrow the target bound before resource admission. + pub const fn execution_ref(&self) -> &ResourceRef { + &self.execution_ref + } +} + +impl

ResourceDetachedClient

+where + P: DetachedProcessResourcePort, +{ + /// Create one detached EphemeralProcess resource. + pub async fn create( + &self, + spec: &ExecStartSpec, + ) -> Result { + if !spec.detached { + return Err(ExecOpError::Protocol); + } + self.port + .create_ephemeral_process(&self.execution_ref, spec) + .await + } + + /// List detached EphemeralProcess resources. + pub async fn list(&self) -> Result { + self.port + .list_ephemeral_processes(&self.execution_ref) + .await + } + + /// Read one detached EphemeralProcess status. + pub async fn status( + &self, + process_ref: &ResourceRef, + ) -> Result { + validate_ephemeral_process_ref(process_ref)?; + self.port.status_ephemeral_process(process_ref).await + } + + /// Read retained detached output. + pub async fn logs( + &self, + process_ref: &ResourceRef, + stdout_offset: Option, + stderr_offset: Option, + max_len: Option, + ) -> Result { + validate_ephemeral_process_ref(process_ref)?; + self.port + .logs_ephemeral_process(process_ref, stdout_offset, stderr_offset, max_len) + .await + } + + /// Cancel one detached EphemeralProcess. + pub async fn kill( + &self, + process_ref: &ResourceRef, + ) -> Result { + validate_ephemeral_process_ref(process_ref)?; + self.port.kill_ephemeral_process(process_ref).await + } +} + +fn validate_ephemeral_process_ref(process_ref: &ResourceRef) -> Result<(), ExecOpError> { + if process_ref.resource_type().as_str() == "EphemeralProcess" { + Ok(()) + } else { + Err(ExecOpError::Protocol) + } +} + #[cfg(any(test, feature = "test-support"))] use std::sync::{Arc, Mutex, OnceLock}; @@ -179,7 +316,7 @@ fn test_hook(request: DetachedTestRequest) -> Option>, + } + + #[async_trait] + impl DetachedProcessResourcePort for RecordingResourcePort { + async fn create_ephemeral_process( + &self, + execution_ref: &ResourceRef, + _spec: &ExecStartSpec, + ) -> Result { + assert_eq!(execution_ref.resource_type().as_str(), "Guest"); + self.calls.lock().unwrap().push("create"); + Ok(ExecDetachedCreateResult { + exec_id: "resource-id".to_owned(), + state: PublicExecState::Created, + }) + } + + async fn list_ephemeral_processes( + &self, + _execution_ref: &ResourceRef, + ) -> Result { + self.calls.lock().unwrap().push("list"); + Ok(ExecDetachedListResult { execs: Vec::new() }) + } + + async fn status_ephemeral_process( + &self, + process_ref: &ResourceRef, + ) -> Result { + assert_eq!(process_ref.resource_type().as_str(), "EphemeralProcess"); + self.calls.lock().unwrap().push("status"); + Ok(ExecDetachedStatusResult { + exec_id: process_ref.name().as_str().to_owned(), + state: PublicExecState::Running, + reason: None, + exit_code: None, + signal: None, + start_offset: 0, + end_offset: 0, + dropped_bytes: 0, + truncated: false, + }) + } + + async fn logs_ephemeral_process( + &self, + _process_ref: &ResourceRef, + _stdout_offset: Option, + _stderr_offset: Option, + _max_len: Option, + ) -> Result { + self.calls.lock().unwrap().push("logs"); + Ok(ExecDetachedLogsResult { + exec_id: "resource-id".to_owned(), + stdout_base64: String::new(), + stderr_base64: String::new(), + start_offset: 0, + end_offset: 0, + dropped_bytes: 0, + truncated: false, + stdout_start_offset: 0, + stdout_end_offset: 0, + stdout_next_offset: 0, + stdout_eof: true, + stdout_dropped_bytes: 0, + stdout_truncated: false, + stderr_start_offset: 0, + stderr_end_offset: 0, + stderr_next_offset: 0, + stderr_eof: true, + stderr_dropped_bytes: 0, + stderr_truncated: false, + }) + } + + async fn kill_ephemeral_process( + &self, + _process_ref: &ResourceRef, + ) -> Result { + self.calls.lock().unwrap().push("kill"); + Ok(ExecDetachedKillResult { + exec_id: "resource-id".to_owned(), + result: ExecDetachedKillOutcome::Cancelling, + state: PublicExecState::Running, + }) + } + } + + #[tokio::test(flavor = "current_thread")] + async fn resource_detached_client_routes_lifecycle_by_resource_ref() { + let port = RecordingResourcePort::default(); + let client = + ResourceDetachedClient::new(port, ResourceRef::parse("Guest/work").unwrap()).unwrap(); + let spec = ExecStartSpec { + vm: "work".to_owned(), + request_id: None, + argv: vec!["true".to_owned()], + tty: false, + detached: true, + env: Vec::new(), + cwd: None, + term_size: None, + }; + assert_eq!(client.create(&spec).await.unwrap().exec_id, "resource-id"); + assert!( + client + .status(&ResourceRef::parse("Process/wrong").unwrap()) + .await + .is_err() + ); + assert_eq!(client.execution_ref().to_canonical_string(), "Guest/work"); + } } diff --git a/packages/d2bd-runtime/src/exec_session.rs b/packages/d2bd-runtime/src/exec_session.rs index 6c386693a4..c7adb56a38 100644 --- a/packages/d2bd-runtime/src/exec_session.rs +++ b/packages/d2bd-runtime/src/exec_session.rs @@ -1,12 +1,12 @@ //! In-process exec session table + per-session worker for `d2b vm exec`. //! -//! The daemon owns a long-lived, authenticated guest-control client per exec -//! session. The CLI drives the session through admin-gated `public.sock` -//! round trips (one frame per [`ExecOp`]); the daemon never opens a new -//! transport per op. A dedicated worker thread (current-thread tokio runtime) -//! owns the authenticated client, the guest `exec_id`, the authoritative stdin -//! offset, and the monotone control sequence; it is reached over a bounded -//! sync command channel. +//! The daemon owns a long-lived, authenticated Process named-stream client per +//! exec session. The CLI establishes the resource owner through the +//! admin-gated `public.sock` route, then sends one correlated named-stream +//! frame per [`ExecOp`]. A dedicated worker thread (current-thread tokio +//! runtime) owns the authenticated client, the target-local process resource, +//! the authoritative stdin offset, and the monotone control sequence; it is +//! reached over a bounded sync command channel. //! //! Concurrency contract (no head-of-line blocking): long-poll ops //! (`ReadOutput`, `Wait`) are spawned onto the worker runtime so the worker @@ -15,15 +15,22 @@ //! they mutate shared session state (stdin offset, control sequence). //! //! Teardown contract (non-detached): when the owner connection drops, the -//! command channel closes, the worker returns, the runtime is dropped, and -//! every clone of the authenticated client is dropped with it. That prompts -//! the guest's `close_connection` → PTY hangup→grace→stop. The daemon -//! never issues `ExecCancel` for a non-detached session. - -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +//! command channel closes, the worker explicitly cancels/resets the named +//! stream and target-local process before returning. The runtime is then +//! dropped, so no attachment or process remains owned by a disconnected +//! caller. + +use std::sync::{ + Arc, Mutex, + atomic::AtomicU64, + atomic::{AtomicBool, Ordering}, +}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Weak, +}; use async_trait::async_trait; @@ -32,15 +39,16 @@ use d2b_contracts_control::guest_wire::GUEST_CONTROL_PROTOCOL_VERSION; use d2b_contracts_control::public_wire::{ EXEC_MAX_CHUNK_BYTES, ExecCloseResult, ExecControlResult, ExecOp, ExecOpResponse, ExecReadOutputResult, ExecStartResult, ExecStream, ExecTerminalStatus, ExecWaitResult, - ExecWriteStdinResult, + ExecWriteStdinResult, NamedProcessStreamErrorKind, NamedProcessStreamRequest, + NamedProcessStreamRequestFrame, NamedProcessStreamResponse, NamedProcessStreamResponseFrame, }; use d2b_core::base64_codec; +use d2b_session::{ComponentSessionDriver, StreamEvent, StreamId}; use protobuf::{EnumOrUnknown, MessageField}; use tokio::sync::{mpsc, oneshot}; use crate::guest_control_health::GuestControlHealthError; use crate::terminal_session::{OutputStreamSel, TerminalBackend, TerminalKind}; -#[cfg(test)] use crate::terminal_session::{ReadOutputOutcome, WaitOutcome, WriteStdinOutcome}; /// Closed enum of per-op proxy failures. Each maps to a redaction-safe slug; @@ -282,6 +290,564 @@ pub trait ExecGuestClient: TerminalBackend {} impl ExecGuestClient for T where T: TerminalBackend {} +/// ComponentSession named-stream implementation of the terminal backend. +/// +/// The stream is admitted by the authenticated session before this value is +/// constructed. Resource identity and caller authorization therefore stay in +/// the Resource API/session layers; this adapter only moves bounded typed +/// control messages and returns redacted protocol failures. +const MAX_PENDING_NAMED_RESPONSES: usize = 64; + +pub struct ComponentSessionExecClient { + inner: Arc>, +} + +struct ComponentSessionExecClientInner { + driver: Arc, + stream: StreamId, + closed: AtomicBool, + next_request_id: AtomicU64, + waiters: Mutex>>>, + late_responses: Mutex>, + pending_credit: Mutex, + send_lock: tokio::sync::Mutex<()>, + demux_abort: Mutex>, + reset_sent: AtomicBool, +} + +impl std::fmt::Debug for ComponentSessionExecClient { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ComponentSessionExecClient") + .field("stream", &"") + .field("closed", &self.inner.closed.load(Ordering::Acquire)) + .finish() + } +} + +impl ComponentSessionExecClient +where + D: ComponentSessionDriver + 'static, +{ + /// Open the fixed process named stream on an authenticated session. + pub async fn open( + driver: D, + stream_number: u16, + send_credit: u32, + receive_credit: u32, + ) -> Result { + let stream = StreamId::new(stream_number).map_err(|_| ExecOpError::Protocol)?; + driver + .open_named_stream(stream, send_credit, receive_credit) + .await + .map_err(|_| ExecOpError::Transport)?; + let inner = Arc::new(ComponentSessionExecClientInner { + driver: Arc::new(driver), + stream, + closed: AtomicBool::new(false), + next_request_id: AtomicU64::new(1), + waiters: Mutex::new(BTreeMap::new()), + late_responses: Mutex::new(BTreeMap::new()), + pending_credit: Mutex::new(0), + send_lock: tokio::sync::Mutex::new(()), + demux_abort: Mutex::new(None), + reset_sent: AtomicBool::new(false), + }); + let task = tokio::spawn(named_stream_demux(Arc::downgrade(&inner))); + *inner + .demux_abort + .lock() + .map_err(|_| ExecOpError::Protocol)? = Some(task.abort_handle()); + Ok(Self { inner }) + } + + /// Cancel/reset the named stream after owner disconnect or timeout. + pub async fn cancel(&self) -> Result<(), ExecOpError> { + self.inner.closed.store(true, Ordering::Release); + self.inner.stop_demux(); + self.inner.fail_waiters(ExecOpError::Transport); + self.inner + .late_responses + .lock() + .map_err(|_| ExecOpError::Protocol)? + .clear(); + *self + .inner + .pending_credit + .lock() + .map_err(|_| ExecOpError::Protocol)? = 0; + if self.inner.reset_sent.swap(true, Ordering::AcqRel) { + return Ok(()); + } + self.inner + .driver + .reset_named_stream(self.inner.stream) + .await + .map_err(|_| ExecOpError::Transport) + } + + /// Acknowledge that the last received response reached its downstream + /// consumer and release its ComponentSession receive credit. + pub async fn acknowledge_received(&self) -> Result<(), ExecOpError> { + self.flush_pending_credit().await + } + + /// Close the named stream after a clean owner detach. + pub async fn close_stream(&self) -> Result<(), ExecOpError> { + if self.inner.closed.swap(true, Ordering::AcqRel) { + return Ok(()); + } + self.inner.stop_demux(); + self.inner.fail_waiters(ExecOpError::Transport); + self.flush_pending_credit().await?; + self.inner + .driver + .close_named_stream(self.inner.stream) + .await + .map_err(|_| ExecOpError::Transport)?; + self.inner + .late_responses + .lock() + .map_err(|_| ExecOpError::Protocol)? + .clear(); + Ok(()) + } + + async fn request( + &self, + request: NamedProcessStreamRequest, + timeout: Duration, + ) -> Result { + if self.inner.closed.load(Ordering::Acquire) { + return Err(ExecOpError::StaleSession); + } + self.flush_pending_credit().await?; + let request_id = self.inner.next_request_id.fetch_add(1, Ordering::AcqRel); + if request_id == 0 { + return Err(ExecOpError::Protocol); + } + let frame = serde_json::to_vec(&NamedProcessStreamRequestFrame::new(request_id, request)) + .map_err(|_| ExecOpError::Protocol)?; + if frame.len() + > d2b_contracts_zone_session::v3::component_session::MAX_LOGICAL_MESSAGE_BYTES as usize + { + return Err(ExecOpError::Protocol); + } + let (reply, receive) = oneshot::channel(); + if let Some(response) = self + .inner + .late_responses + .lock() + .map_err(|_| ExecOpError::Protocol)? + .remove(&request_id) + { + return Ok(response); + } + { + let mut waiters = self + .inner + .waiters + .lock() + .map_err(|_| ExecOpError::Protocol)?; + if waiters.len() >= MAX_PENDING_NAMED_RESPONSES { + return Err(ExecOpError::Guest(GuestOpError::StdinBackpressure)); + } + waiters.insert(request_id, reply); + } + let send_result = { + let _send_lock = self.inner.send_lock.lock().await; + self.inner + .driver + .send_named_stream(self.inner.stream, frame) + .await + }; + if send_result.is_err() { + self.inner + .waiters + .lock() + .map_err(|_| ExecOpError::Protocol)? + .remove(&request_id); + return Err(ExecOpError::Transport); + } + match tokio::time::timeout(timeout, receive).await { + Ok(Ok(response)) => response, + Ok(Err(_)) => Err(ExecOpError::Transport), + Err(_) => { + self.inner + .waiters + .lock() + .map_err(|_| ExecOpError::Protocol)? + .remove(&request_id); + Err(ExecOpError::Timeout) + } + } + } + + async fn flush_pending_credit(&self) -> Result<(), ExecOpError> { + let credit = { + let mut pending = self + .inner + .pending_credit + .lock() + .map_err(|_| ExecOpError::Protocol)?; + std::mem::take(&mut *pending) + }; + if credit == 0 { + return Ok(()); + } + self.inner + .driver + .grant_named_stream_credit(self.inner.stream, credit) + .await + .map_err(|_| ExecOpError::Transport) + } + + fn map_error(kind: NamedProcessStreamErrorKind) -> ExecOpError { + match kind { + NamedProcessStreamErrorKind::Authorization => ExecOpError::Auth, + NamedProcessStreamErrorKind::StaleSession => ExecOpError::StaleSession, + NamedProcessStreamErrorKind::NotFound => ExecOpError::Guest(GuestOpError::ExecNotFound), + NamedProcessStreamErrorKind::Backpressure => { + ExecOpError::Guest(GuestOpError::StdinBackpressure) + } + NamedProcessStreamErrorKind::Protocol => ExecOpError::Protocol, + NamedProcessStreamErrorKind::Timeout => ExecOpError::Timeout, + NamedProcessStreamErrorKind::Disconnected => ExecOpError::Transport, + } + } + + fn response_error( + response: NamedProcessStreamResponse, + ) -> Result { + match response { + NamedProcessStreamResponse::Error(error) => Err(Self::map_error(error.kind)), + response => Ok(response), + } + } + + fn terminal_kind(status: &ExecTerminalStatus) -> Option { + match status { + ExecTerminalStatus::Exited { code } => Some(TerminalKind::Exited(*code)), + ExecTerminalStatus::Signaled { signal } => Some(TerminalKind::Signaled(*signal)), + ExecTerminalStatus::Error { .. } => Some(TerminalKind::Error("process-stream-error")), + } + } +} + +impl Drop for ComponentSessionExecClient { + fn drop(&mut self) { + self.inner.closed.store(true, Ordering::Release); + self.inner.stop_demux(); + self.inner.fail_waiters(ExecOpError::Transport); + } +} + +impl ComponentSessionExecClientInner { + fn stop_demux(&self) { + if let Ok(mut abort) = self.demux_abort.lock() + && let Some(abort) = abort.take() + { + abort.abort(); + } + } + + fn fail_waiters(&self, error: ExecOpError) { + if let Ok(mut waiters) = self.waiters.lock() { + for (_, waiter) in std::mem::take(&mut *waiters) { + let _ = waiter.send(Err(error)); + } + } + } + + fn fail_closed(&self, error: ExecOpError) { + self.closed.store(true, Ordering::Release); + self.fail_waiters(error); + } +} + +async fn named_stream_demux(weak: Weak>) +where + D: ComponentSessionDriver + 'static, +{ + loop { + let Some(inner) = weak.upgrade() else { + return; + }; + let event = inner.driver.receive_named_stream().await; + match event { + Ok(StreamEvent::Data { stream, bytes }) if stream == inner.stream => { + let frame: NamedProcessStreamResponseFrame = match serde_json::from_slice(&bytes) { + Ok(frame) => frame, + Err(_) => { + inner.fail_closed(ExecOpError::Protocol); + return; + } + }; + if frame.request_id == 0 { + inner.fail_closed(ExecOpError::Protocol); + return; + } + let credit = match u32::try_from(bytes.len()) { + Ok(credit) => credit, + Err(_) => { + inner.fail_closed(ExecOpError::Protocol); + return; + } + }; + let credit_result = match inner.pending_credit.lock() { + Ok(mut pending) => match pending.checked_add(credit) { + Some(next) => { + *pending = next; + Ok(()) + } + None => Err(ExecOpError::Protocol), + }, + Err(_) => Err(ExecOpError::Protocol), + }; + match credit_result { + Ok(()) => {} + Err(error) => { + inner.fail_closed(error); + return; + } + } + let waiter = inner + .waiters + .lock() + .ok() + .and_then(|mut waiters| waiters.remove(&frame.request_id)); + if let Some(waiter) = waiter { + let _ = waiter.send(Ok(frame.response)); + continue; + } + let mut late = match inner.late_responses.lock() { + Ok(late) => late, + Err(_) => { + inner.fail_closed(ExecOpError::Protocol); + return; + } + }; + if late.len() >= MAX_PENDING_NAMED_RESPONSES { + inner.fail_closed(ExecOpError::Guest(GuestOpError::StdinBackpressure)); + return; + } + if late.contains_key(&frame.request_id) { + inner.fail_closed(ExecOpError::Protocol); + return; + } + late.insert(frame.request_id, frame.response); + } + Ok(StreamEvent::RemoteClosed { stream } | StreamEvent::Reset { stream }) + if stream == inner.stream => + { + inner.fail_closed(ExecOpError::Transport); + return; + } + Ok(_) => {} + Err(_) => { + inner.fail_closed(ExecOpError::Transport); + return; + } + } + } +} + +#[async_trait] +impl TerminalBackend for ComponentSessionExecClient +where + D: ComponentSessionDriver + 'static, +{ + type Error = ExecOpError; + + async fn write_stdin( + &self, + offset: u64, + data: Vec, + eof: bool, + timeout: Duration, + ) -> Result { + let data_len = u64::try_from(data.len()).map_err(|_| ExecOpError::Protocol)?; + if data.is_empty() || data_len > EXEC_MAX_CHUNK_BYTES { + return Err(ExecOpError::Guest(GuestOpError::MaxChunkExceeded)); + } + let response = Self::response_error( + self.request( + NamedProcessStreamRequest::Stdin { + offset, + chunk_base64: base64_codec::encode(&data), + eof, + }, + timeout, + ) + .await?, + )?; + match response { + NamedProcessStreamResponse::Stdin(result) => { + if result.accepted_len > data_len + || result.next_offset < offset + || result.next_offset - offset != result.accepted_len + { + return Err(ExecOpError::Protocol); + } + Ok(WriteStdinOutcome { + accepted_len: result.accepted_len, + next_offset: result.next_offset, + backpressured: result.backpressured, + stdin_closed: result.stdin_closed, + }) + } + _ => Err(ExecOpError::Protocol), + } + } + + async fn read_output( + &self, + stream: OutputStreamSel, + offset: u64, + max_len: u64, + wait: bool, + timeout_ms: u64, + timeout: Duration, + ) -> Result { + if max_len == 0 || max_len > EXEC_MAX_CHUNK_BYTES { + return Err(ExecOpError::Guest(GuestOpError::MaxChunkExceeded)); + } + let response = Self::response_error( + self.request( + NamedProcessStreamRequest::Read { + stream: match stream { + OutputStreamSel::Stdout => ExecStream::Stdout, + OutputStreamSel::Stderr => ExecStream::Stderr, + }, + offset, + max_len, + wait, + timeout_ms, + }, + timeout, + ) + .await?, + )?; + match response { + NamedProcessStreamResponse::Output(result) => { + let data = + base64_codec::decode(&result.data_base64).map_err(|_| ExecOpError::Protocol)?; + let data_len = u64::try_from(data.len()).map_err(|_| ExecOpError::Protocol)?; + if data_len > max_len + || result.next_offset < offset + || result.next_offset - offset != data_len + { + return Err(ExecOpError::Protocol); + } + Ok(ReadOutputOutcome { + data, + next_offset: result.next_offset, + eof: result.eof, + dropped_bytes: result.dropped_bytes, + truncated: result.truncated, + timed_out: result.timed_out, + }) + } + NamedProcessStreamResponse::Terminal(_) => Ok(ReadOutputOutcome { + data: Vec::new(), + next_offset: offset, + eof: true, + dropped_bytes: 0, + truncated: false, + timed_out: false, + }), + _ => Err(ExecOpError::Protocol), + } + } + + async fn signal( + &self, + control_seq: u64, + signo: u32, + timeout: Duration, + ) -> Result<(), Self::Error> { + if !matches!(signo, 1 | 2 | 3 | 9 | 10 | 12 | 15 | 18 | 19 | 20 | 28) { + return Err(ExecOpError::Protocol); + } + let response = Self::response_error( + self.request( + NamedProcessStreamRequest::Signal { control_seq, signo }, + timeout, + ) + .await?, + )?; + match response { + NamedProcessStreamResponse::Delivered(_) => Ok(()), + _ => Err(ExecOpError::Protocol), + } + } + + async fn resize( + &self, + control_seq: u64, + rows: u32, + cols: u32, + timeout: Duration, + ) -> Result<(), Self::Error> { + if rows == 0 || cols == 0 || rows > u32::from(u16::MAX) || cols > u32::from(u16::MAX) { + return Err(ExecOpError::Protocol); + } + let response = Self::response_error( + self.request( + NamedProcessStreamRequest::Resize { + control_seq, + rows, + cols, + }, + timeout, + ) + .await?, + )?; + match response { + NamedProcessStreamResponse::Delivered(_) => Ok(()), + _ => Err(ExecOpError::Protocol), + } + } + + async fn wait(&self, timeout_ms: u64, timeout: Duration) -> Result { + let response = Self::response_error( + self.request(NamedProcessStreamRequest::Wait { timeout_ms }, timeout) + .await?, + )?; + match response { + NamedProcessStreamResponse::Wait(result) => Ok(WaitOutcome { + running: result.running, + terminal: result + .terminal_status + .as_ref() + .and_then(Self::terminal_kind), + }), + NamedProcessStreamResponse::Terminal(status) => Ok(WaitOutcome { + running: false, + terminal: Self::terminal_kind(&status), + }), + _ => Err(ExecOpError::Protocol), + } + } + + async fn close_stdin(&self, offset: u64, timeout: Duration) -> Result<(), Self::Error> { + let response = Self::response_error( + self.request(NamedProcessStreamRequest::CloseStdin { offset }, timeout) + .await?, + )?; + match response { + NamedProcessStreamResponse::Closed(_) => Ok(()), + _ => Err(ExecOpError::Protocol), + } + } + + async fn cancel(&self, _control_seq: u64, timeout: Duration) -> Result<(), Self::Error> { + tokio::time::timeout(timeout, ComponentSessionExecClient::cancel(self)) + .await + .map_err(|_| ExecOpError::Timeout)? + } +} + /// Establishment seam: connect + authenticate + cap-gate + `ExecCreate`. #[async_trait] pub trait ExecGuestConnector: Send + Sync { @@ -454,7 +1020,10 @@ async fn worker_main( } = established; if establish_tx.send(Ok(info)).is_err() { // Owner vanished before the establish reply landed. Returning here - // drops `client`, closing the guest connection promptly. + // must still reset/cancel the guest process before the client drops. + let _ = client + .cancel(control_seq.saturating_add(1), deadlines.control) + .await; return; } @@ -509,9 +1078,13 @@ async fn worker_main( } } } - // `control_rx` closed → owner disconnected → drop `state` (and its sole - // client reference). Any still-spawned long-poll is aborted when the - // runtime is dropped at thread exit. + // `control_rx` closed -> owner disconnected. Explicitly cancel/reset the + // established process before dropping the client so a named stream and + // its target-local process cannot outlive their owner. + let _ = state + .client + .cancel(state.control_seq.saturating_add(1), state.deadlines.control) + .await; } /// Arm the terminal-cleanup timer: after the TTL elapses, if the command @@ -1057,8 +1630,575 @@ fn hex_encode(bytes: &[u8]) -> String { #[cfg(test)] mod tests { use super::*; + use d2b_contracts_zone_session::v3::component_session::{ + AttachmentPolicy, EndpointPolicy, EndpointPurpose, EndpointRole, + IdentityEvidenceRequirement, LimitProfile, Locality, NoiseProfile, PurposeClass, + ServicePackage, TransportBinding, TransportClass, + }; + use d2b_session::{ + HandshakeCredentials, OwnedTransport, SessionEngine, TransportDescriptor, TransportError, + TransportPacket, TransportReader, TransportWriter, + }; use std::collections::VecDeque; use std::sync::atomic::{AtomicUsize, Ordering}; + use tokio::sync::Notify; + + #[derive(Default)] + struct NamedDriverState { + sent: Mutex>>, + events: Mutex>>, + opened: Mutex>, + granted: AtomicUsize, + resets: AtomicUsize, + event_ready: Notify, + } + + #[derive(Clone, Default)] + struct NamedDriver { + state: Arc, + } + + impl NamedDriver { + fn push_event(&self, event: d2b_session::Result) { + self.state.events.lock().unwrap().push_back(event); + self.state.event_ready.notify_one(); + } + } + + #[async_trait] + impl ComponentSessionDriver for NamedDriver { + fn generation(&self) -> u64 { + 1 + } + + async fn start_ttrpc( + &self, + _request_id: d2b_contracts_zone_session::v3::component_session::RequestId, + _frame: Vec, + ) -> d2b_session::Result<()> { + Ok(()) + } + + async fn complete_ttrpc( + &self, + _request_id: d2b_contracts_zone_session::v3::component_session::RequestId, + ) -> d2b_session::Result { + Ok(true) + } + + async fn cancel( + &self, + _generation: u64, + _request_id: d2b_contracts_zone_session::v3::component_session::RequestId, + ) -> d2b_session::Result<()> { + Ok(()) + } + + async fn send_ttrpc(&self, _frame: Vec) -> d2b_session::Result<()> { + Ok(()) + } + + async fn receive_ttrpc(&self) -> d2b_session::Result> { + Err(d2b_session::SessionError::new( + d2b_contracts_zone_session::v3::component_session::SessionErrorCode::InternalInvariant, + )) + } + + async fn register_inbound_call( + &self, + _request_id: d2b_contracts_zone_session::v3::component_session::RequestId, + ) -> d2b_session::Result { + panic!("named-stream test driver does not accept inbound calls") + } + + async fn mark_inbound_dispatched( + &self, + _request_id: d2b_contracts_zone_session::v3::component_session::RequestId, + ) -> d2b_session::Result<()> { + Ok(()) + } + + async fn complete_inbound_call( + &self, + _request_id: d2b_contracts_zone_session::v3::component_session::RequestId, + ) -> d2b_session::Result { + Ok(true) + } + + async fn remove_inbound_call( + &self, + _request_id: d2b_contracts_zone_session::v3::component_session::RequestId, + ) -> d2b_session::Result { + Ok(true) + } + + async fn send_attachments( + &self, + _attachments: Vec, + ) -> d2b_session::Result<()> { + Ok(()) + } + + async fn receive_attachments( + &self, + ) -> d2b_session::Result> { + Ok(Vec::new()) + } + + async fn open_named_stream( + &self, + stream: StreamId, + send_credit: u32, + receive_credit: u32, + ) -> d2b_session::Result<()> { + self.state + .opened + .lock() + .unwrap() + .push((stream, send_credit, receive_credit)); + Ok(()) + } + + async fn send_named_stream( + &self, + _stream: StreamId, + bytes: Vec, + ) -> d2b_session::Result<()> { + self.state.sent.lock().unwrap().push(bytes); + Ok(()) + } + + async fn receive_named_stream(&self) -> d2b_session::Result { + loop { + if let Some(event) = self.state.events.lock().unwrap().pop_front() { + return event; + } + self.state.event_ready.notified().await; + } + } + + async fn grant_named_stream_credit( + &self, + _stream: StreamId, + bytes: u32, + ) -> d2b_session::Result<()> { + self.state + .granted + .fetch_add(bytes as usize, Ordering::AcqRel); + Ok(()) + } + + async fn close_named_stream(&self, _stream: StreamId) -> d2b_session::Result<()> { + Ok(()) + } + + async fn reset_named_stream(&self, _stream: StreamId) -> d2b_session::Result<()> { + self.state.resets.fetch_add(1, Ordering::AcqRel); + Ok(()) + } + + async fn drive_keepalive(&self, _now: Instant) -> d2b_session::Result<()> { + Ok(()) + } + + async fn receive_control(&self) -> d2b_session::Result { + Err(d2b_session::SessionError::new( + d2b_contracts_zone_session::v3::component_session::SessionErrorCode::InternalInvariant, + )) + } + + async fn close( + &self, + _reason: d2b_contracts_zone_session::v3::component_session::CloseReason, + _remediation: d2b_contracts_zone_session::v3::component_session::Remediation, + ) -> d2b_session::Result<()> { + Ok(()) + } + } + + fn named_event( + stream: StreamId, + response: NamedProcessStreamResponse, + ) -> d2b_session::Result { + named_event_with_id(stream, 1, response) + } + + fn named_event_with_id( + stream: StreamId, + request_id: u64, + response: NamedProcessStreamResponse, + ) -> d2b_session::Result { + Ok(StreamEvent::Data { + stream, + bytes: serde_json::to_vec(&NamedProcessStreamResponseFrame::new(request_id, response)) + .unwrap(), + }) + } + + #[tokio::test(flavor = "current_thread")] + async fn component_session_exec_client_uses_one_authenticated_named_stream() { + let stream = StreamId::new(0x100).unwrap(); + let driver = NamedDriver::default(); + driver.push_event(named_event( + stream, + NamedProcessStreamResponse::Stdin(ExecWriteStdinResult { + accepted_len: 2, + next_offset: 2, + backpressured: false, + stdin_closed: false, + }), + )); + let client = ComponentSessionExecClient::open(driver.clone(), 0x100, 1024, 1024) + .await + .unwrap(); + let result = client + .write_stdin(0, b"hi".to_vec(), false, Duration::from_secs(1)) + .await + .unwrap(); + client.acknowledge_received().await.unwrap(); + assert_eq!(result.next_offset, 2); + assert_eq!(driver.state.opened.lock().unwrap().len(), 1); + assert!(driver.state.granted.load(Ordering::Acquire) > 0); + assert_eq!(driver.state.sent.lock().unwrap().len(), 1); + let request: NamedProcessStreamRequestFrame = + serde_json::from_slice(&driver.state.sent.lock().unwrap()[0]).unwrap(); + assert_eq!(request.request_id, 1); + assert!(matches!( + request.request, + NamedProcessStreamRequest::Stdin { .. } + )); + } + + #[tokio::test(flavor = "current_thread")] + async fn component_session_exec_client_releases_credit_after_output_consumption() { + let stream = StreamId::new(0x101).unwrap(); + let driver = NamedDriver::default(); + let output = NamedProcessStreamResponse::Output(ExecReadOutputResult { + data_base64: base64_codec::encode(b"output"), + next_offset: 6, + eof: false, + dropped_bytes: 0, + truncated: false, + timed_out: false, + }); + driver.push_event(named_event(stream, output)); + let client = ComponentSessionExecClient::open(driver.clone(), 0x101, 1024, 1024) + .await + .unwrap(); + let result = client + .read_output( + OutputStreamSel::Stdout, + 0, + 64, + false, + 0, + Duration::from_secs(1), + ) + .await + .unwrap(); + assert_eq!(result.data, b"output"); + assert_eq!(driver.state.granted.load(Ordering::Acquire), 0); + client.acknowledge_received().await.unwrap(); + assert!(driver.state.granted.load(Ordering::Acquire) > 0); + } + + #[tokio::test(flavor = "current_thread")] + async fn component_session_exec_client_cancel_resets_the_named_stream_once() { + let driver = NamedDriver::default(); + let client = ComponentSessionExecClient::open(driver.clone(), 0x105, 1024, 1024) + .await + .unwrap(); + client.cancel().await.unwrap(); + client.cancel().await.unwrap(); + assert_eq!(driver.state.resets.load(Ordering::Acquire), 1); + } + + #[tokio::test(flavor = "current_thread")] + async fn component_session_exec_client_correlates_out_of_order_responses() { + let stream = StreamId::new(0x102).unwrap(); + let driver = NamedDriver::default(); + let reply = |next_offset| { + NamedProcessStreamResponse::Stdin(ExecWriteStdinResult { + accepted_len: 1, + next_offset, + backpressured: false, + stdin_closed: false, + }) + }; + driver.push_event(named_event_with_id(stream, 2, reply(2))); + driver.push_event(named_event_with_id(stream, 1, reply(1))); + let client = ComponentSessionExecClient::open(driver, 0x102, 1024, 1024) + .await + .unwrap(); + let (first, second) = tokio::join!( + client.write_stdin(0, b"a".to_vec(), false, Duration::from_secs(1)), + client.write_stdin(1, b"b".to_vec(), false, Duration::from_secs(1)), + ); + assert_eq!(first.unwrap().next_offset, 1); + assert_eq!(second.unwrap().next_offset, 2); + } + + struct DriverTestTransport { + sender: mpsc::Sender, + receiver: Option>, + descriptor: TransportDescriptor, + } + + struct DriverTestReader { + receiver: mpsc::Receiver, + } + + struct DriverTestWriter { + sender: mpsc::Sender, + } + + #[async_trait] + impl OwnedTransport for DriverTestTransport { + fn descriptor(&self) -> TransportDescriptor { + self.descriptor + } + + fn into_split(mut self: Box) -> (Box, Box) { + ( + Box::new(DriverTestReader { + receiver: self.receiver.take().expect("driver test reader"), + }), + Box::new(DriverTestWriter { + sender: self.sender.clone(), + }), + ) + } + + async fn receive( + &mut self, + protected_limit: usize, + ) -> Result { + let receiver = self.receiver.as_mut().expect("handshake receiver"); + let packet = receiver.recv().await.ok_or(TransportError::Disconnected)?; + if packet.as_bytes().len() > protected_limit { + return Err(TransportError::LimitExceeded); + } + Ok(packet) + } + + async fn send(&mut self, packet: TransportPacket) -> Result<(), TransportError> { + self.sender + .send(packet) + .await + .map_err(|_| TransportError::Disconnected) + } + + async fn close(&mut self) -> Result<(), TransportError> { + Ok(()) + } + } + + #[async_trait] + impl TransportReader for DriverTestReader { + async fn receive( + &mut self, + protected_limit: usize, + ) -> Result { + let packet = self + .receiver + .recv() + .await + .ok_or(TransportError::Disconnected)?; + if packet.as_bytes().len() > protected_limit { + return Err(TransportError::LimitExceeded); + } + Ok(packet) + } + } + + #[async_trait] + impl TransportWriter for DriverTestWriter { + async fn send(&mut self, packet: TransportPacket) -> Result<(), TransportError> { + self.sender + .send(packet) + .await + .map_err(|_| TransportError::Disconnected) + } + + async fn close(&mut self) -> Result<(), TransportError> { + Ok(()) + } + } + + fn driver_test_policy() -> EndpointPolicy { + EndpointPolicy { + purpose: EndpointPurpose::LocalLifecycle, + purpose_class: PurposeClass::Local, + initiator_role: EndpointRole::ZoneController, + responder_role: EndpointRole::Component, + service: ServicePackage::ResourceV3, + schema_fingerprint: [0x11; 32], + noise_profile: NoiseProfile::Nn25519ChaChaPolySha256, + limits: LimitProfile::local_default(), + transport_binding: TransportBinding { + transport: TransportClass::UnixSeqpacket, + locality: Locality::HostLocal, + channel_binding: [0x22; 32], + identity_evidence: IdentityEvidenceRequirement::DirectionalUnix, + }, + reconnect_generation: 1, + attachment_policy: AttachmentPolicy::disabled(), + } + } + + fn driver_test_transport_pair() -> (DriverTestTransport, DriverTestTransport) { + let (left_sender, right_receiver) = mpsc::channel(128); + let (right_sender, left_receiver) = mpsc::channel(128); + let descriptor = TransportDescriptor { + class: TransportClass::UnixSeqpacket, + locality: Locality::HostLocal, + packet_atomic: true, + supports_attachments: false, + }; + ( + DriverTestTransport { + sender: left_sender, + receiver: Some(left_receiver), + descriptor, + }, + DriverTestTransport { + sender: right_sender, + receiver: Some(right_receiver), + descriptor, + }, + ) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn component_session_exec_client_demuxes_concurrent_controls_on_real_driver_handle() { + let (initiator_transport, responder_transport) = driver_test_transport_pair(); + let policy = driver_test_policy(); + let now = Instant::now(); + let (initiator, responder) = tokio::join!( + SessionEngine::establish_initiator( + initiator_transport, + policy.clone(), + HandshakeCredentials::Nn, + now, + ), + SessionEngine::establish_responder( + responder_transport, + policy, + HandshakeCredentials::Nn, + now, + ), + ); + let initiator = initiator.expect("initiator session"); + let responder = responder.expect("responder session"); + let initiator = initiator.into_driver(); + let responder = responder.into_driver(); + let stream = StreamId::new(0x104).unwrap(); + responder + .open_named_stream(stream, 64 * 1024, 64 * 1024) + .await + .unwrap(); + + let wait_gate = Arc::new(Notify::new()); + let peer_gate = Arc::clone(&wait_gate); + let wait_seen = Arc::new(Notify::new()); + let peer_wait_seen = Arc::clone(&wait_seen); + let peer = tokio::spawn(async move { + while let Ok(event) = responder.receive_named_stream().await { + let StreamEvent::Data { stream, bytes } = event else { + continue; + }; + let frame: NamedProcessStreamRequestFrame = + serde_json::from_slice(&bytes).expect("request frame"); + if let NamedProcessStreamRequest::Wait { .. } = frame.request { + let responder = responder.clone(); + let peer_gate = Arc::clone(&peer_gate); + peer_wait_seen.notify_one(); + tokio::spawn(async move { + peer_gate.notified().await; + let response = NamedProcessStreamResponseFrame::new( + frame.request_id, + NamedProcessStreamResponse::Wait(ExecWaitResult { + running: true, + terminal_status: None, + }), + ); + responder + .send_named_stream(stream, serde_json::to_vec(&response).unwrap()) + .await + .expect("wait response frame"); + }); + continue; + } + let response = match frame.request { + NamedProcessStreamRequest::Resize { .. } => { + NamedProcessStreamResponse::Delivered(ExecControlResult { delivered: true }) + } + _ => NamedProcessStreamResponse::Error( + d2b_contracts_control::public_wire::NamedProcessStreamError { + kind: NamedProcessStreamErrorKind::Protocol, + }, + ), + }; + let response = NamedProcessStreamResponseFrame::new(frame.request_id, response); + responder + .send_named_stream(stream, serde_json::to_vec(&response).unwrap()) + .await + .expect("response frame"); + } + }); + + let client = Arc::new( + ComponentSessionExecClient::open(initiator, 0x104, 64 * 1024, 64 * 1024) + .await + .unwrap(), + ); + let wait_client = Arc::clone(&client); + let wait = + tokio::spawn(async move { wait_client.wait(1_000, Duration::from_secs(2)).await }); + tokio::time::timeout(Duration::from_secs(1), wait_seen.notified()) + .await + .expect("peer did not receive the long poll"); + let resize_client = Arc::clone(&client); + let resize = tokio::spawn(async move { + resize_client + .resize(1, 24, 80, Duration::from_secs(2)) + .await + }); + assert!( + tokio::time::timeout(Duration::from_secs(1), resize) + .await + .expect("resize was head-of-line blocked") + .unwrap() + .is_ok() + ); + wait_gate.notify_one(); + assert!(wait.await.unwrap().unwrap().running); + client.cancel().await.unwrap(); + peer.abort(); + } + + #[tokio::test(flavor = "current_thread")] + async fn component_session_exec_client_rejects_unallowlisted_controls() { + let driver = NamedDriver::default(); + let client = ComponentSessionExecClient::open(driver.clone(), 0x103, 1024, 1024) + .await + .unwrap(); + assert_eq!( + client + .signal(1, 4, Duration::from_secs(1)) + .await + .unwrap_err(), + ExecOpError::Protocol + ); + assert_eq!( + client + .resize(1, u32::from(u16::MAX) + 1, 80, Duration::from_secs(1)) + .await + .unwrap_err(), + ExecOpError::Protocol + ); + assert!(driver.state.sent.lock().unwrap().is_empty()); + } use d2b_contracts_control::public_wire::{ ExecCloseArgs, ExecReadOutputArgs, ExecResizeArgs, ExecSignalArgs, ExecStream, @@ -1133,6 +2273,7 @@ mod tests { struct FakeShared { write_calls: AtomicUsize, close_calls: AtomicUsize, + cancel_calls: AtomicUsize, signal_calls: AtomicUsize, resize_calls: AtomicUsize, read_calls: AtomicUsize, @@ -1268,6 +2409,16 @@ mod tests { .push(("close", timeout)); Ok(()) } + + async fn cancel(&self, _control_seq: u64, timeout: Duration) -> Result<(), ExecOpError> { + self.shared.cancel_calls.fetch_add(1, Ordering::SeqCst); + self.shared + .op_timeouts + .lock() + .unwrap() + .push(("cancel", timeout)); + Ok(()) + } } // ---- Fake connector (establish once from a builder closure) ---------------- @@ -1389,11 +2540,12 @@ mod tests { let alive = Arc::new(AtomicUsize::new(0)); let alive_for_builder = Arc::clone(&alive); let shared = Arc::new(FakeShared::default()); + let shared_for_builder = Arc::clone(&shared); let builder: Builder = Box::new(move || { alive_for_builder.fetch_add(1, Ordering::SeqCst); established(Arc::new(FakeClient { alive: alive_for_builder, - shared, + shared: shared_for_builder, write_outcome: WriteStdinOutcome { accepted_len: 0, next_offset: 0, @@ -1422,6 +2574,11 @@ mod tests { 0, "client dropped on teardown (prompts guest close_connection)" ); + assert_eq!( + shared.cancel_calls.load(Ordering::SeqCst), + 1, + "owner disconnect must cancel the established process" + ); } #[test] @@ -1430,11 +2587,13 @@ mod tests { let alive_for_builder = Arc::clone(&alive); let gate = Arc::new(tokio::sync::Notify::new()); let gate_for_builder = Arc::clone(&gate); + let shared = Arc::new(FakeShared::default()); + let shared_for_builder = Arc::clone(&shared); let builder: Builder = Box::new(move || { alive_for_builder.fetch_add(1, Ordering::SeqCst); established(Arc::new(FakeClient { alive: alive_for_builder, - shared: Arc::new(FakeShared::default()), + shared: shared_for_builder, write_outcome: WriteStdinOutcome { accepted_len: 0, next_offset: 0, @@ -1477,6 +2636,11 @@ mod tests { 0, "parked long-poll's client clone dropped at runtime teardown" ); + assert_eq!( + shared.cancel_calls.load(Ordering::SeqCst), + 1, + "disconnect must cancel even while a long poll is active" + ); } #[test] diff --git a/packages/d2bd-runtime/src/exec_session_real.rs b/packages/d2bd-runtime/src/exec_session_real.rs index b3a3f6b66d..a9a15ae596 100644 --- a/packages/d2bd-runtime/src/exec_session_real.rs +++ b/packages/d2bd-runtime/src/exec_session_real.rs @@ -1,46 +1,52 @@ -//! Production exec connector + authenticated guest-control exec client. +//! Resource-backed exec connector and named-stream helpers. //! -//! Bridges the in-process [`crate::exec_session`] machinery to the real -//! per-VM vsock transport: connect, run the authenticated handshake (reusing -//! the [`crate::guest_control_bridge`] connect/probe path), gate on the -//! guest's advertised exec capabilities, then issue `ExecCreate`. The -//! returned `RealExecClient` proxies each subsequent exec op with a FRESH -//! per-op deadline (never the exhausted one-shot establishment budget). +//! Attached execution is admitted through `EphemeralProcess` resources and +//! ComponentSession named streams. The former direct guest-control connector +//! remains test-only characterization coverage so production composition cannot +//! reach feature-specific `ExecCreate`. +#[cfg(any(test, feature = "test-support"))] use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; -use d2b_contracts_control::guest_proto as pb; -use protobuf::{EnumOrUnknown, MessageField}; +use d2b_contracts_resource::v3::ResourceRef; -#[cfg(test)] -use crate::exec_session::NegotiatedCaps; +#[cfg(any(test, feature = "test-support"))] +use crate::exec_session::ExecOpDeadlines; use crate::exec_session::{ - Established, ExecEstablishError, ExecGuestConnector, ExecOpDeadlines, ExecOpError, - ExecSessionInfo, ExecStartSpec, -}; -#[cfg(test)] -use crate::guest_control_bridge::connect_and_build_client_for_tests; -use crate::guest_control_bridge::{ - BrokerSigner, GUEST_CONTROL_ATTEMPT_CAP, ProbeParams, VMADDR_CID_HOST, - connect_and_build_client, host_nonce, + ComponentSessionExecClient, Established, ExecEstablishError, ExecGuestClient, + ExecGuestConnector, ExecOpError, ExecSessionInfo, ExecStartSpec, }; -use d2b_contracts_broker::broker_wire::BrokerCallerRole; -use d2b_contracts_control::guest_wire::GUEST_CONTROL_PROTOCOL_VERSION; pub use crate::exec_session::{ ack_result, build_exec_create_request, gate_capabilities, is_unspecified, map_establish_health_error, map_guest_control_error, map_op_health_error, map_op_health_error_for_establish, op_to_establish, terminal_from_state, }; +#[cfg(any(test, feature = "test-support"))] +use crate::guest_control_bridge::connect_and_build_client_for_tests; +#[cfg(any(test, feature = "test-support"))] +use crate::guest_control_bridge::{ + BrokerSigner, GUEST_CONTROL_ATTEMPT_CAP, ProbeParams, VMADDR_CID_HOST, + connect_and_build_client, host_nonce, +}; +#[cfg(any(test, feature = "test-support"))] use crate::guest_control_health::{ AttemptBudget, GuestControlHealthError, TtrpcGuestControlClient, probe_guest_control_health, }; -use crate::terminal_session::TerminalBackend; +#[cfg(any(test, feature = "test-support"))] use crate::terminal_session::{ - OutputStreamSel, ReadOutputOutcome, WaitOutcome, WriteStdinOutcome, + OutputStreamSel, ReadOutputOutcome, TerminalBackend, WaitOutcome, WriteStdinOutcome, }; +#[cfg(any(test, feature = "test-support"))] +use d2b_contracts_broker::broker_wire::BrokerCallerRole; +#[cfg(any(test, feature = "test-support"))] +use d2b_contracts_control::guest_proto as pb; +#[cfg(any(test, feature = "test-support"))] +use d2b_contracts_control::guest_wire::GUEST_CONTROL_PROTOCOL_VERSION; +#[cfg(any(test, feature = "test-support"))] +use protobuf::{EnumOrUnknown, MessageField}; /// Absolute deadline for the whole establish phase (connect + auth handshake: /// `CONNECT`-ack, Hello, sign, Authenticate, sign, Health). @@ -54,8 +60,197 @@ use crate::terminal_session::{ /// headroom for scheduling jitter without changing the per-op cap or protocol. pub const ESTABLISH_TIMEOUT: Duration = Duration::from_secs(20); +/// The resource handle returned after an EphemeralProcess Create admission. +/// +/// The handle contains only the resource identity and transport-neutral +/// stream metadata. Command data, credentials, paths, and process identities +/// remain owned by the Resource API and Process Provider. +#[derive(Clone, PartialEq, Eq)] +pub struct EphemeralProcessHandle { + resource_ref: ResourceRef, + stdout_offset: u64, + stderr_offset: u64, + control_sequence: u64, +} + +impl std::fmt::Debug for EphemeralProcessHandle { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("EphemeralProcessHandle") + .field("resource_ref", &"") + .field("stdout_offset", &self.stdout_offset) + .field("stderr_offset", &self.stderr_offset) + .field("control_sequence", &self.control_sequence) + .finish() + } +} + +impl EphemeralProcessHandle { + /// Construct a handle from an authenticated Resource API response. + pub fn new( + resource_ref: ResourceRef, + stdout_offset: u64, + stderr_offset: u64, + control_sequence: u64, + ) -> Result { + if resource_ref.resource_type().as_str() != "EphemeralProcess" { + return Err(ExecEstablishError::Protocol); + } + Ok(Self { + resource_ref, + stdout_offset, + stderr_offset, + control_sequence, + }) + } + + /// Borrow the created EphemeralProcess reference. + pub const fn resource_ref(&self) -> &ResourceRef { + &self.resource_ref + } + + /// Return the initial stdout cursor. + pub const fn stdout_offset(&self) -> u64 { + self.stdout_offset + } + + /// Return the initial stderr cursor. + pub const fn stderr_offset(&self) -> u64 { + self.stderr_offset + } + + /// Return the initial in-stream control sequence. + pub const fn control_sequence(&self) -> u64 { + self.control_sequence + } +} + +/// Resource API seam used by the ComponentSession exec connector. +/// +/// Implementations create an EphemeralProcess through the authenticated +/// Resource API, then open its admitted named stream. The connector has no +/// broker, socket, path, or direct child-process authority. +#[async_trait] +pub trait ProcessResourcePort: Send + Sync { + /// Create one target-local EphemeralProcess resource. + async fn create_ephemeral_process( + &self, + execution_ref: &ResourceRef, + spec: &ExecStartSpec, + ) -> Result; + + /// Attach the authenticated named stream for the created resource. + async fn attach_process( + &self, + process: &EphemeralProcessHandle, + tty: bool, + initial_size: Option<(u32, u32)>, + ) -> Result, ExecEstablishError>; +} + +/// Exec connector backed by Process/EphemeralProcess resources and a +/// ComponentSession named stream. +pub struct ResourceExecConnector

{ + port: P, + execution_ref: ResourceRef, +} + +impl

std::fmt::Debug for ResourceExecConnector

{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("ResourceExecConnector()") + } +} + +impl

ResourceExecConnector

{ + /// Bind the connector to one exact Host or Guest execution target. + pub fn new(port: P, execution_ref: ResourceRef) -> Result { + if !matches!(execution_ref.resource_type().as_str(), "Host" | "Guest") { + return Err(ExecEstablishError::Protocol); + } + Ok(Self { + port, + execution_ref, + }) + } + + /// Borrow the target bound before Resource API admission. + pub const fn execution_ref(&self) -> &ResourceRef { + &self.execution_ref + } +} + +#[async_trait] +impl

ExecGuestConnector for ResourceExecConnector

+where + P: ProcessResourcePort, +{ + async fn establish(&self, spec: &ExecStartSpec) -> Result { + if spec.detached { + return Err(ExecEstablishError::Capability); + } + let process = self + .port + .create_ephemeral_process(&self.execution_ref, spec) + .await?; + let client = self + .port + .attach_process(&process, spec.tty, spec.term_size) + .await?; + Ok(Established { + client, + info: ExecSessionInfo { + tty: spec.tty, + stdout_offset: process.stdout_offset(), + stderr_offset: process.stderr_offset(), + }, + control_seq: process.control_sequence(), + caps: crate::exec_session::NegotiatedCaps { + tty: spec.tty, + signals: true, + tty_resize: spec.tty, + output: true, + }, + }) + } +} + +/// Open the standard Process named stream on an already authenticated +/// ComponentSession driver. +pub async fn open_component_session_process( + driver: D, + stream_number: u16, +) -> Result, ExecEstablishError> +where + D: d2b_session::ComponentSessionDriver + 'static, +{ + let client = ComponentSessionExecClient::open( + driver, + stream_number, + d2b_contracts_zone_session::v3::component_session::MAX_NAMED_STREAM_QUEUE_BYTES, + d2b_contracts_zone_session::v3::component_session::MAX_NAMED_STREAM_QUEUE_BYTES, + ) + .await + .map_err(map_component_session_exec_error)?; + Ok(Arc::new(client)) +} + +fn map_component_session_exec_error(error: ExecOpError) -> ExecEstablishError { + match error { + ExecOpError::Timeout => ExecEstablishError::Timeout, + ExecOpError::Auth => ExecEstablishError::Auth, + ExecOpError::StaleSession => ExecEstablishError::OldGeneration, + ExecOpError::Transport => ExecEstablishError::Transport, + ExecOpError::Protocol + | ExecOpError::OldGeneration + | ExecOpError::Capability + | ExecOpError::DetachedUnavailable + | ExecOpError::Guest(_) => ExecEstablishError::Protocol, + } +} + /// Production exec connector. Owns the resolved probe params + broker socket /// path so it is `Send + Sync` and can move into the worker thread. +#[cfg(any(test, feature = "test-support"))] pub struct RealExecConnector { params: ProbeParams, broker_socket_path: PathBuf, @@ -64,10 +259,11 @@ pub struct RealExecConnector { /// Test-only: route the connect through the relaxed-directory test policy so /// a hermetic test can reach the genuine socket-missing transport branch /// under a non-root tempdir. Always `false` for the production constructor. - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] allow_test_dirs: bool, } +#[cfg(any(test, feature = "test-support"))] impl RealExecConnector { pub fn new( params: ProbeParams, @@ -80,7 +276,7 @@ impl RealExecConnector { broker_socket_path, caller_role, deadlines, - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] allow_test_dirs: false, } } @@ -110,7 +306,7 @@ impl RealExecConnector { &self, budget: AttemptBudget, ) -> Result { - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] if self.allow_test_dirs { return connect_and_build_client_for_tests(&self.params, budget); } @@ -119,6 +315,7 @@ impl RealExecConnector { } #[async_trait] +#[cfg(any(test, feature = "test-support"))] impl ExecGuestConnector for RealExecConnector { async fn establish(&self, spec: &ExecStartSpec) -> Result { let budget = AttemptBudget::from_now(ESTABLISH_TIMEOUT, GUEST_CONTROL_ATTEMPT_CAP); @@ -190,6 +387,7 @@ impl ExecGuestConnector for RealExecConnector { /// at connect/probe time. Surface the capability slug (exit 70, no SSH /// fallback) whose remediation points at enabling guest-control exec. /// Authenticated exec client bound to one `exec_id` on one guest connection. +#[cfg(any(test, feature = "test-support"))] struct RealExecClient { client: Arc, vm_id: String, @@ -197,6 +395,7 @@ struct RealExecClient { exec_id: String, } +#[cfg(any(test, feature = "test-support"))] impl RealExecClient { fn exec_metadata(&self) -> pb::ExecRequestMetadata { let mut common = pb::RequestMetadata::new(); @@ -212,6 +411,7 @@ impl RealExecClient { } #[async_trait] +#[cfg(any(test, feature = "test-support"))] impl TerminalBackend for RealExecClient { type Error = ExecOpError; @@ -371,6 +571,20 @@ impl TerminalBackend for RealExecClient { } Ok(()) } + + async fn cancel(&self, control_seq: u64, timeout: Duration) -> Result<(), ExecOpError> { + let mut request = pb::ExecCancelRequest::new(); + request.metadata = MessageField::some(self.exec_metadata()); + request.control_seq = control_seq; + request.reason = + EnumOrUnknown::new(pb::ExecCancelReason::EXEC_CANCEL_REASON_CLIENT_DISCONNECT); + let response: pb::ControlAck = self + .client + .unary_with_timeout("ExecCancel", request, timeout) + .await + .map_err(map_op_health_error)?; + ack_result(&response) + } } // =========================================================================== @@ -381,6 +595,7 @@ impl TerminalBackend for RealExecClient { #[cfg(test)] mod tests { use super::*; + use crate::exec_session::NegotiatedCaps; fn cap(value: pb::GuestCapability) -> EnumOrUnknown { EnumOrUnknown::new(value) @@ -525,6 +740,18 @@ mod tests { assert!(gate_capabilities(&full_tty_caps(), false).is_ok()); } + #[test] + fn stale_component_session_maps_to_old_generation() { + assert_eq!( + map_component_session_exec_error(ExecOpError::StaleSession), + ExecEstablishError::OldGeneration + ); + assert_eq!( + map_component_session_exec_error(ExecOpError::Transport), + ExecEstablishError::Transport + ); + } + /// Daemon-side fail-closed complement to the CLI-side /// `vm_exec_old_generation_fails_closed_without_proxy_or_ssh`: when the real /// connector cannot reach the guest vsock (absent socket / an old @@ -665,4 +892,160 @@ mod tests { "tty exec must open stdin for guestd's interactive validator", ); } + + struct StubResourceBackend; + + #[async_trait] + impl TerminalBackend for StubResourceBackend { + type Error = ExecOpError; + + async fn write_stdin( + &self, + offset: u64, + data: Vec, + eof: bool, + _timeout: Duration, + ) -> Result { + Ok(WriteStdinOutcome { + accepted_len: data.len() as u64, + next_offset: offset + data.len() as u64, + backpressured: false, + stdin_closed: eof, + }) + } + + async fn read_output( + &self, + _stream: OutputStreamSel, + offset: u64, + _max_len: u64, + _wait: bool, + _timeout_ms: u64, + _timeout: Duration, + ) -> Result { + Ok(ReadOutputOutcome { + data: Vec::new(), + next_offset: offset, + eof: true, + dropped_bytes: 0, + truncated: false, + timed_out: false, + }) + } + + async fn signal( + &self, + _control_seq: u64, + _signo: u32, + _timeout: Duration, + ) -> Result<(), Self::Error> { + Ok(()) + } + + async fn resize( + &self, + _control_seq: u64, + _rows: u32, + _cols: u32, + _timeout: Duration, + ) -> Result<(), Self::Error> { + Ok(()) + } + + async fn wait( + &self, + _timeout_ms: u64, + _timeout: Duration, + ) -> Result { + Ok(WaitOutcome { + running: true, + terminal: None, + }) + } + + async fn close_stdin(&self, _offset: u64, _timeout: Duration) -> Result<(), Self::Error> { + Ok(()) + } + } + + #[derive(Default)] + struct RecordingResourcePort { + created: std::sync::Mutex>, + } + + #[async_trait] + impl ProcessResourcePort for RecordingResourcePort { + async fn create_ephemeral_process( + &self, + execution_ref: &ResourceRef, + _spec: &ExecStartSpec, + ) -> Result { + self.created.lock().unwrap().push(execution_ref.clone()); + EphemeralProcessHandle::new( + ResourceRef::parse("EphemeralProcess/run").unwrap(), + 3, + 4, + 5, + ) + } + + async fn attach_process( + &self, + _process: &EphemeralProcessHandle, + _tty: bool, + _initial_size: Option<(u32, u32)>, + ) -> Result, ExecEstablishError> { + Ok(Arc::new(StubResourceBackend)) + } + } + + #[tokio::test(flavor = "current_thread")] + async fn resource_exec_connector_creates_and_attaches_one_ephemeral_process() { + let port = RecordingResourcePort::default(); + let connector = + ResourceExecConnector::new(port, ResourceRef::parse("Guest/work").unwrap()).unwrap(); + let spec = ExecStartSpec { + vm: "work".to_owned(), + request_id: None, + argv: vec!["true".to_owned()], + tty: false, + detached: false, + env: Vec::new(), + cwd: None, + term_size: None, + }; + let established = connector.establish(&spec).await.unwrap(); + assert_eq!(established.info.stdout_offset, 3); + assert_eq!(established.info.stderr_offset, 4); + assert_eq!(established.control_seq, 5); + assert!(established.caps.output); + assert!(!established.caps.tty); + } + + #[test] + fn resource_exec_connector_rejects_non_execution_targets() { + assert_eq!( + ResourceExecConnector::::new( + RecordingResourcePort::default(), + ResourceRef::parse("Process/not-a-target").unwrap(), + ) + .unwrap_err() + .slug(), + ExecEstablishError::Protocol.slug() + ); + } + + #[test] + fn ephemeral_process_handle_debug_redacts_resource_identity() { + let handle = EphemeralProcessHandle::new( + ResourceRef::parse("EphemeralProcess/secret-command").unwrap(), + 0, + 0, + 0, + ) + .unwrap(); + let rendered = format!("{handle:?}"); + assert!(!rendered.contains("secret-command")); + assert!(rendered.contains("resource_ref")); + } } diff --git a/packages/d2bd-runtime/src/guest_control_bridge.rs b/packages/d2bd-runtime/src/guest_control_bridge.rs index bfdb200c1c..83a932814d 100644 --- a/packages/d2bd-runtime/src/guest_control_bridge.rs +++ b/packages/d2bd-runtime/src/guest_control_bridge.rs @@ -422,7 +422,7 @@ pub fn connect_and_build_client( /// genuine `SocketMissing` transport branch under a non-root tempdir instead of /// tripping the production state-root ownership pre-validation first. The /// `Failed(_) -> TransportIo` mapping is identical to production. -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub(crate) fn connect_and_build_client_for_tests( params: &ProbeParams, budget: AttemptBudget, diff --git a/packages/d2bd-runtime/src/shell_backend.rs b/packages/d2bd-runtime/src/shell_backend.rs index 693abf2646..9ba654ad72 100644 --- a/packages/d2bd-runtime/src/shell_backend.rs +++ b/packages/d2bd-runtime/src/shell_backend.rs @@ -1,6 +1,7 @@ use crate::{ daemon_audit, exec_session::is_unspecified, + exec_session::{ComponentSessionExecClient, ExecOpError, GuestOpError}, guest_control_health::GuestControlHealthError, terminal_session::{OutputStreamSel, TerminalBackend}, typed_error::{GuestControlShellErrorKind, TypedError, UnsafeLocalShellErrorKind}, @@ -29,7 +30,9 @@ pub enum ShellTerminalResponse { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ShellProvider { + /// ComponentSession guest provider or its legacy characterization. GuestControl, + /// Unsafe-local host provider. UnsafeLocal, } @@ -55,6 +58,16 @@ pub trait ShellBackend: Send + Sync { runtime: &tokio::runtime::Handle, control_sequence: &mut u64, ) -> Result; + + /// Reset the named stream when its owning public connection disappears. + /// Backends without a distinct reset operation may close normally. + fn cancel_attachment( + &self, + runtime: &tokio::runtime::Handle, + control_sequence: &mut u64, + ) -> Result { + self.close_attachment(runtime, control_sequence) + } } pub struct EstablishedShell { @@ -66,6 +79,222 @@ pub struct EstablishedShell { pub initial_control_sequence: u64, } +/// Persistent-shell backend over a ComponentSession named stream. +/// +/// Shell lifecycle is still authorized by the ShellSession resource and its +/// Provider controller. This adapter only translates terminal operations to +/// the already-admitted stream; it has no process-spawn or broker authority. +pub struct ComponentSessionShellBackend { + public_session: String, + resolved_name: public_wire::ShellName, + client: ComponentSessionExecClient, +} + +impl fmt::Debug for ComponentSessionShellBackend { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ComponentSessionShellBackend") + .field("public_session", &"") + .field("resolved_name", &"") + .field("client", &self.client) + .finish() + } +} + +impl ComponentSessionShellBackend +where + D: d2b_session::ComponentSessionDriver + 'static, +{ + /// Open the fixed terminal named stream after ShellSession admission. + pub async fn open( + driver: D, + stream_number: u16, + public_session: String, + resolved_name: public_wire::ShellName, + ) -> Result { + let client = ComponentSessionExecClient::open( + driver, + stream_number, + d2b_contracts_zone_session::v3::component_session::MAX_NAMED_STREAM_QUEUE_BYTES, + d2b_contracts_zone_session::v3::component_session::MAX_NAMED_STREAM_QUEUE_BYTES, + ) + .await + .map_err(map_component_session_shell_error)?; + Ok(Self { + public_session, + resolved_name, + client, + }) + } + + /// Reset the stream after owner cancellation or a disconnected peer. + pub fn cancel(&self, runtime: &tokio::runtime::Handle) -> Result<(), TypedError> { + runtime + .block_on(self.client.cancel()) + .map_err(map_component_session_shell_error) + } + + fn ensure_session(&self, session: &str) -> Result<(), TypedError> { + if session == self.public_session { + Ok(()) + } else { + Err(shell_failed( + crate::typed_error::GuestControlShellErrorKind::StaleSession, + )) + } + } +} + +impl ShellBackend for ComponentSessionShellBackend +where + D: d2b_session::ComponentSessionDriver + 'static, +{ + fn handle_op( + &self, + runtime: &tokio::runtime::Handle, + control_sequence: &mut u64, + op: ShellTerminalOp, + ) -> Result, TypedError> { + match op { + ShellTerminalOp::WriteStdin(args) => { + self.ensure_session(&args.session)?; + let data = d2b_core::base64_codec::decode(&args.chunk_base64) + .map_err(|_| shell_protocol_failed())?; + let result = if data.is_empty() && args.eof { + runtime + .block_on( + self.client + .close_stdin(args.offset, SHELL_MANAGEMENT_TIMEOUT), + ) + .map(|()| crate::terminal_session::WriteStdinOutcome { + accepted_len: 0, + next_offset: args.offset, + backpressured: false, + stdin_closed: true, + }) + } else { + runtime.block_on(self.client.write_stdin( + args.offset, + data, + args.eof, + SHELL_MANAGEMENT_TIMEOUT, + )) + } + .map_err(map_component_session_shell_error)?; + Ok(Some(ShellTerminalResponse::WriteStdin( + tw::TerminalWriteStdinResult { + accepted_len: result.accepted_len, + next_offset: result.next_offset, + backpressured: result.backpressured, + stdin_closed: result.stdin_closed, + }, + ))) + } + ShellTerminalOp::ReadOutput(args) => { + self.ensure_session(&args.session)?; + if args.stream != tw::TerminalStream::Stdout { + return Err(shell_protocol_failed()); + } + let (timeout_ms, deadline) = backend_shell_poll_timeout(args.timeout_ms, args.wait); + let result = runtime + .block_on(self.client.read_output( + OutputStreamSel::Stdout, + args.offset, + args.max_len, + args.wait, + timeout_ms, + deadline, + )) + .map_err(map_component_session_shell_error)?; + Ok(Some(ShellTerminalResponse::ReadOutput( + tw::TerminalReadOutputChunk { + data_base64: d2b_core::base64_codec::encode(&result.data), + next_offset: result.next_offset, + eof: result.eof, + dropped_bytes: result.dropped_bytes, + truncated: result.truncated, + timed_out: result.timed_out, + }, + ))) + } + ShellTerminalOp::Resize(args) => { + self.ensure_session(&args.session)?; + *control_sequence = control_sequence.saturating_add(1); + runtime + .block_on(self.client.resize( + *control_sequence, + args.rows, + args.cols, + SHELL_MANAGEMENT_TIMEOUT, + )) + .map_err(map_component_session_shell_error)?; + Ok(Some(ShellTerminalResponse::Delivered)) + } + } + } + + fn close_attachment( + &self, + runtime: &tokio::runtime::Handle, + control_sequence: &mut u64, + ) -> Result { + *control_sequence = control_sequence.saturating_add(1); + runtime + .block_on(self.client.close_stream()) + .map_err(map_component_session_shell_error)?; + Ok(public_wire::ShellDetachResult { + resolved_name: self.resolved_name.clone(), + detached: true, + cause: Some(public_wire::ShellCloseCause::ClientDetach), + }) + } + + fn cancel_attachment( + &self, + runtime: &tokio::runtime::Handle, + control_sequence: &mut u64, + ) -> Result { + *control_sequence = control_sequence.saturating_add(1); + runtime + .block_on(self.client.cancel()) + .map_err(map_component_session_shell_error)?; + Ok(public_wire::ShellDetachResult { + resolved_name: self.resolved_name.clone(), + detached: true, + cause: Some(public_wire::ShellCloseCause::ClientDetach), + }) + } +} + +fn map_component_session_shell_error(error: ExecOpError) -> TypedError { + use crate::typed_error::GuestControlShellErrorKind as Kind; + let kind = match error { + ExecOpError::Transport => Kind::Transport, + ExecOpError::Auth => Kind::Auth, + ExecOpError::StaleSession => Kind::StaleSession, + ExecOpError::Protocol => Kind::Protocol, + ExecOpError::Timeout => Kind::Timeout, + ExecOpError::OldGeneration | ExecOpError::Capability => Kind::Capability, + ExecOpError::DetachedUnavailable => Kind::Capability, + ExecOpError::Guest(GuestOpError::ExecNotFound | GuestOpError::ExecExpired) => { + Kind::NotFound + } + ExecOpError::Guest(GuestOpError::StdinBackpressure) => Kind::Capacity, + ExecOpError::Guest(GuestOpError::OffsetMismatch) => Kind::Protocol, + ExecOpError::Guest(GuestOpError::StdinClosed | GuestOpError::StdinNotOpen) => { + Kind::StaleSession + } + ExecOpError::Guest(GuestOpError::ControlSeqMismatch) => Kind::StaleSession, + ExecOpError::Guest(GuestOpError::RateLimited) => Kind::Capacity, + ExecOpError::Guest(GuestOpError::MaxChunkExceeded | GuestOpError::InvalidProgram) => { + Kind::Protocol + } + ExecOpError::Guest(GuestOpError::ExecAlreadyExited) => Kind::NotFound, + ExecOpError::Guest(GuestOpError::Protocol | GuestOpError::Other) => Kind::GuestError, + }; + shell_failed(kind) +} + impl fmt::Debug for EstablishedShell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("EstablishedShell") @@ -223,6 +452,23 @@ pub fn best_effort_close( } } +pub fn best_effort_cancel( + backend: &dyn ShellBackend, + runtime: &tokio::runtime::Handle, + control_sequence: &mut u64, +) -> daemon_audit::ShellAuditResult { + match backend.cancel_attachment(runtime, control_sequence) { + Ok(_) => daemon_audit::ShellAuditResult::Closed, + Err(TypedError::UnsafeLocalShellFailed { + kind: UnsafeLocalShellErrorKind::Timeout, + }) + | Err(TypedError::GuestControlShellFailed { + kind: crate::typed_error::GuestControlShellErrorKind::Timeout, + }) => daemon_audit::ShellAuditResult::Timeout, + Err(_) => daemon_audit::ShellAuditResult::Error, + } +} + fn shell_operation_timeout() -> Duration { Duration::from_secs(3) } @@ -586,4 +832,26 @@ mod tests { } )); } + + #[test] + fn component_session_shell_errors_stay_in_the_closed_shell_vocabulary() { + assert!(matches!( + map_component_session_shell_error(ExecOpError::Auth), + TypedError::GuestControlShellFailed { + kind: GuestControlShellErrorKind::Auth + } + )); + assert!(matches!( + map_component_session_shell_error(ExecOpError::Guest(GuestOpError::StdinBackpressure)), + TypedError::GuestControlShellFailed { + kind: GuestControlShellErrorKind::Capacity + } + )); + assert!(matches!( + map_component_session_shell_error(ExecOpError::Transport), + TypedError::GuestControlShellFailed { + kind: GuestControlShellErrorKind::Transport + } + )); + } } diff --git a/packages/d2bd-runtime/src/terminal_session.rs b/packages/d2bd-runtime/src/terminal_session.rs index 0cfcaaac72..392b47c5ef 100644 --- a/packages/d2bd-runtime/src/terminal_session.rs +++ b/packages/d2bd-runtime/src/terminal_session.rs @@ -106,6 +106,13 @@ pub trait TerminalBackend: Send + Sync { async fn wait(&self, timeout_ms: u64, timeout: Duration) -> Result; async fn close_stdin(&self, offset: u64, timeout: Duration) -> Result<(), Self::Error>; + + /// Cancel the established process and release any transport-owned + /// attachment. Backends that do not own a cancellable remote process may + /// keep the default no-op implementation. + async fn cancel(&self, _control_seq: u64, _timeout: Duration) -> Result<(), Self::Error> { + Ok(()) + } } #[cfg(test)] diff --git a/packages/d2bd/BUILD.bazel b/packages/d2bd/BUILD.bazel index a73d2ffd26..13e02a4fe3 100644 --- a/packages/d2bd/BUILD.bazel +++ b/packages/d2bd/BUILD.bazel @@ -53,6 +53,7 @@ d2bd_shared_deps = [ "//packages/d2b-provider-system-core:d2b_provider_system_core", "//packages/d2b-provider-system-minijail:d2b_provider_system_minijail", "//packages/d2b-provider-system-systemd:d2b_provider_system_systemd", + "//packages/d2b-provider-shell-terminal:d2b_provider_shell_terminal", "//packages/d2b-provider-transport-azure-relay:d2b_provider_transport_azure_relay", "//packages/d2b-realm-core:d2b_realm_core", "//packages/d2b-resource-api:d2b_resource_api", diff --git a/packages/d2bd/Cargo.toml b/packages/d2bd/Cargo.toml index e2fda6a08b..1f88bdf9bf 100644 --- a/packages/d2bd/Cargo.toml +++ b/packages/d2bd/Cargo.toml @@ -26,6 +26,7 @@ d2b-core-controller = { path = "../d2b-core-controller", version = "0.0.0-bootst d2b-provider-system-core = { path = "../d2b-provider-system-core", version = "0.0.0-bootstrap" } d2b-provider-system-minijail = { path = "../d2b-provider-system-minijail", version = "0.0.0-bootstrap" } d2b-provider-system-systemd = { path = "../d2b-provider-system-systemd", version = "0.0.0-bootstrap" } +d2b-provider-shell-terminal = { path = "../d2b-provider-shell-terminal", version = "0.0.0-bootstrap" } d2b-provider-supervisor = { path = "../d2b-provider-supervisor", version = "0.0.0-bootstrap" } d2b-provider-activation-nixos = { path = "../d2b-provider-activation-nixos", version = "0.0.0-bootstrap" } d2b-provider-volume-local = { path = "../d2b-provider-volume-local", version = "0.0.0-bootstrap" } diff --git a/packages/d2bd/src/composition.rs b/packages/d2bd/src/composition.rs index 253c782704..0ee45ee4ef 100644 --- a/packages/d2bd/src/composition.rs +++ b/packages/d2bd/src/composition.rs @@ -25,6 +25,10 @@ type InteractionRuntime = interaction_composition::InteractionRuntimeSet< InteractionSupervisor, interaction_composition::AuthenticatedGuestFrontendEffects, >; +type DaemonResourceApiClient = d2b_resource_api::ResourceApiClient< + d2b_resource_api::RedbBackend, + d2b_resource_api::service::UnavailableUpgradeDispatcher, +>; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -71,22 +75,18 @@ use d2b_contracts_control::{ guest_proto as pb, public_wire::{self, AuthRole, AuthStatusResponse, DeniedCommandHint, SocketReachability}, }; -use d2b_contracts_zone_session::v3::resource_bundle::ResourceBundle; +use d2b_contracts_resource::resource_proto as resource_wire; +use d2b_contracts_resource::v3::identity::ReconnectGeneration; use d2b_contracts_resource::v3::storage::ZoneStoreId; use d2b_contracts_resource::v3::{ - ResourceBundleGenerationId, - ResourceErrorKind, - ResourceGeneration, - ResourceRef, - ResourceUid, - SchemaFingerprint, - ZoneId, + ResourceBundleGenerationId, ResourceErrorKind, ResourceGeneration, ResourceRef, ResourceUid, + SchemaFingerprint, ZoneId, guest::GuestSpec, network::NetworkSpec, process::ProcessSpec, volume::{VolumeAttachment, VolumeSpec}, }; -use d2b_contracts_resource::v3::identity::ReconnectGeneration; +use d2b_contracts_zone_session::v3::resource_bundle::ResourceBundle; use d2b_core::bundle::Bundle; use d2b_core::bundle_resolver::{ BundleResolver, intent_id_activation, intent_id_bridge_env, intent_id_gc_host, @@ -128,6 +128,10 @@ use d2b_provider_network_local::{ use d2b_provider_runtime_azure_container_apps::gateway::{ AcaConfig, AcaDiskImageSource, AcaSandboxDefaults, AcaWorkloadProvider, }; +use d2b_provider_shell_terminal::{ + DEFAULT_OUTPUT_RING_CAPACITY, ExecutionTarget, PoolSpec, ShellAuthorityLedger, + ShellAuthorityPort, ShellPool, ShellSession, ShellTerminalError, SupervisorProcessResource, +}; use d2b_provider_transport_azure_relay::auth::{DEFAULT_SAS_TTL_SECS, RelayEndpoint}; use d2b_provider_volume_local::diagnostics::storage_lifecycle; use d2b_realm_core::TargetName; @@ -197,27 +201,28 @@ pub use d2bd_runtime::daemon_config::{ shutdown_degraded_report_path, startup_autostart_pre_degraded_vms, storage_lifecycle_report_path, }; -pub use d2bd_runtime::runtime_capability::{ - RuntimeCapabilityGate, ensure_manifest_entry_runtime_capability, -}; -pub use d2bd_runtime::runtime_process::{ - RuntimeIdentity, acquire_state_lock, bind_public_socket, chrono_like_rfc3339, - daemon_version_file_path, days_to_ymd, drop_privileges_if_root, ensure_locks_dir, - resolve_runtime_identity, resolve_unsafe_local_helper_uids, sd_notify_address, - sd_notify_payload, sd_notify_ready, sd_notify_status, validate_lock_parent, - write_daemon_version_file, -}; pub use d2bd_runtime::exec_detached; pub(crate) use d2bd_runtime::exec_owner_io; pub use d2bd_runtime::exec_session; +use d2bd_runtime::exec_session::ExecGuestConnector; +pub use d2bd_runtime::exec_session_real; #[cfg(test)] use d2bd_runtime::exec_support::map_exec_op_error; pub(crate) use d2bd_runtime::exec_support::{ emit_exec_established_event, exec_error_kind_label, exec_metric_into, map_exec_establish_error, map_exec_reserve_error, }; -pub use d2bd_runtime::exec_session_real; pub use d2bd_runtime::guest_control_bridge; +pub use d2bd_runtime::runtime_capability::{ + RuntimeCapabilityGate, ensure_manifest_entry_runtime_capability, +}; +pub use d2bd_runtime::runtime_process::{ + RuntimeIdentity, acquire_state_lock, bind_public_socket, chrono_like_rfc3339, + daemon_version_file_path, days_to_ymd, drop_privileges_if_root, ensure_locks_dir, + resolve_runtime_identity, resolve_unsafe_local_helper_uids, sd_notify_address, + sd_notify_payload, sd_notify_ready, sd_notify_status, validate_lock_parent, + write_daemon_version_file, +}; use d2bd_runtime::runtime_util::{ block_on_future, duplicate_received_fd, hex_bytes, projection_digest_bytes, }; @@ -319,8 +324,7 @@ impl StaticProviderComposition { daemon_uid: u32, limits: d2bd_runtime::target_runtime::AdmissionLimits, ) -> Result { - let deployment = - d2bd_runtime::target_runtime::ProviderDeployment::new(mode, limits)?; + let deployment = d2bd_runtime::target_runtime::ProviderDeployment::new(mode, limits)?; let effects = match mode { d2bd_runtime::target_runtime::DaemonMode::Host => { provider_effects::FixedEffectAdapter::host(broker_socket, daemon_uid) @@ -492,69 +496,1663 @@ struct GatewayDisplayFileConfig { waypipe_socket: Option, } -#[derive(Debug, Clone)] -struct ServerState { - config: DaemonConfig, - daemon_uid: u32, - daemon_state_dir: PathBuf, - pidfd_table: Arc, - broker_reap_log: Arc, - metrics_registry: Arc, - /// Daemon-side audit log for supervisor events (e.g. api-ready - /// timeout) that are not emitted by the broker. - daemon_audit: Arc, - /// In-process exec session table (caps + opaque handles) for - /// `d2b vm exec`. There is no per-VM unit and no broker op: a - /// session is a daemon-held authenticated guest-control client owned by a - /// spawned worker. - exec_sessions: Arc, - /// Gateway display orchestrator state. Persisted for the daemon lifetime so - /// Open/List/Close share the same ledger and resource handles. - gateway_display: Arc, - /// Bounded admission gate for in-flight connection-handler threads. - /// The accept loop performs a non-blocking try-acquire and refuses - /// (typed-busy) on a miss rather than ever blocking `accept()`. - conn_semaphore: d2bd_runtime::concurrency::ConnSemaphore, - /// Per-VM / global in-process op locks. Acquired once on the worker - /// thread inside `dispatch_request` so a mutating lifecycle op cannot - /// race another op on the same VM (or any per-VM op for a global op). - op_locks: d2bd_runtime::concurrency::OpLockManager, - /// Daemon-owned fast list/status read model. This is per ServerState so - /// independent daemon instances/tests never evict each other's snapshots. - public_status_read_model: Arc, - /// Shared v3 Provider registry and lifecycle effect dispatcher. A - /// pre-v3 bundle remains in explicit compatibility mode; a malformed - /// v3 catalog is fail-closed rather than falling back. - provider_runtime: Arc, - /// Daemon-owned production Zone resource plane. It is absent only while - /// trusted bundle/storage admission is incomplete; public resource - /// requests fail closed rather than falling back to the legacy path. - resource_plane: Arc>>>, - /// Daemon-owned interaction Provider composition and its authenticated - /// ComponentSession registrations. - interaction_runtime: Arc>>, - /// Listener loops for the daemon-owned interaction ComponentSession - /// sockets. Their stop handle is retained so shutdown closes admission - /// before runtime finalization. - interaction_listeners: Arc>>, - /// Bounded execution-target bindings for qualified ShellSession resources. - /// The provider may remove a killed session before a retry arrives, so the - /// daemon keeps recent exact target bindings independently of provider - /// lists. - typed_shell_session_targets: - Arc>, - /// Authoritative Zone index for daemon-side coordination state. USBIP - /// reconciliation, force-stop generations, and activation staging all - /// resolve through this index; no process-global lock carries that state. - zone_coordinator: Arc>, - /// Per-VM console session table (ring buffers and drainer tasks) for - /// `d2b console `. Sessions are created on first Attach and persist - /// until the daemon restarts or the VM stops. - console_sessions: Arc>, - #[allow(dead_code)] - security_key_sessions: Arc>, - #[allow(dead_code)] - unsafe_local_helpers: Arc, +#[derive(Debug, Clone)] +struct ServerState { + config: DaemonConfig, + daemon_uid: u32, + daemon_state_dir: PathBuf, + pidfd_table: Arc, + broker_reap_log: Arc, + metrics_registry: Arc, + /// Daemon-side audit log for supervisor events (e.g. api-ready + /// timeout) that are not emitted by the broker. + daemon_audit: Arc, + /// In-process Process-session table (caps + opaque handles) for + /// `d2b vm exec`. There is no per-VM unit and no broker op: a + /// session is a daemon-held authenticated named-stream client owned by a + /// spawned worker. + exec_sessions: Arc, + /// Gateway display orchestrator state. Persisted for the daemon lifetime so + /// Open/List/Close share the same ledger and resource handles. + gateway_display: Arc, + /// Bounded admission gate for in-flight connection-handler threads. + /// The accept loop performs a non-blocking try-acquire and refuses + /// (typed-busy) on a miss rather than ever blocking `accept()`. + conn_semaphore: d2bd_runtime::concurrency::ConnSemaphore, + /// Per-VM / global in-process op locks. Acquired once on the worker + /// thread inside `dispatch_request` so a mutating lifecycle op cannot + /// race another op on the same VM (or any per-VM op for a global op). + op_locks: d2bd_runtime::concurrency::OpLockManager, + /// Daemon-owned fast list/status read model. This is per ServerState so + /// independent daemon instances/tests never evict each other's snapshots. + public_status_read_model: Arc, + /// Shared v3 Provider registry and lifecycle effect dispatcher. A + /// pre-v3 bundle remains in explicit compatibility mode; a malformed + /// v3 catalog is fail-closed rather than falling back. + provider_runtime: Arc, + /// Daemon-owned production Zone resource plane. It is absent only while + /// trusted bundle/storage admission is incomplete; public resource + /// requests fail closed rather than falling back to the legacy path. + resource_plane: Arc>>>, + #[cfg(not(test))] + /// Persistent daemon-owned ShellSession Process authority. + shell_authority: Arc, + /// Daemon-owned interaction Provider composition and its authenticated + /// ComponentSession registrations. + interaction_runtime: Arc>>, + /// Listener loops for the daemon-owned interaction ComponentSession + /// sockets. Their stop handle is retained so shutdown closes admission + /// before runtime finalization. + interaction_listeners: Arc>>, + /// Bounded execution-target bindings for qualified ShellSession resources. + /// The provider may remove a killed session before a retry arrives, so the + /// daemon keeps recent exact target bindings independently of provider + /// lists. + typed_shell_session_targets: + Arc>, + /// Authoritative Zone index for daemon-side coordination state. USBIP + /// reconciliation, force-stop generations, and activation staging all + /// resolve through this index; no process-global lock carries that state. + zone_coordinator: Arc>, + /// Per-VM console session table (ring buffers and drainer tasks) for + /// `d2b console `. Sessions are created on first Attach and persist + /// until the daemon restarts or the VM stops. + console_sessions: Arc>, + #[allow(dead_code)] + security_key_sessions: Arc>, + #[allow(dead_code)] + unsafe_local_helpers: Arc, +} +const PROCESS_RUNTIME_FINALIZER: &str = "process-runtime.d2bus.org/cleanup"; + +/// The production ShellSession authority. Session grants and attachment +/// accounting remain daemon-owned, while each supervisor is represented by a +/// durable target-local Process resource in the Zone store. +#[derive(Clone)] +struct DaemonShellAuthority { + ledger: Arc, + resource_plane: Arc>>>, +} + +impl std::fmt::Debug for DaemonShellAuthority { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("DaemonShellAuthority()") + } +} + +impl DaemonShellAuthority { + fn new(resource_plane: Arc>>>) -> Self { + Self { + ledger: Arc::new(ShellAuthorityLedger::new()), + resource_plane, + } + } + + fn client_for_session( + &self, + session: &ShellSession, + ) -> Result<(ZoneId, Arc), ShellTerminalError> { + let zone = ZoneId::parse(session.zone()).map_err(|_| ShellTerminalError::WrongZone)?; + let client = self.client_for_zone(&zone)?; + Ok((zone, client)) + } + + fn client_for_zone( + &self, + zone: &ZoneId, + ) -> Result, ShellTerminalError> { + let plane = self + .resource_plane + .lock() + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)? + .clone() + .ok_or(ShellTerminalError::SupervisorAmbiguous)?; + let runtime = plane + .zone(&zone) + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + let client = runtime + .process_resource_client() + .ok_or(ShellTerminalError::SupervisorAmbiguous)?; + Ok(client) + } + + fn get_process( + &self, + client: &DaemonResourceApiClient, + zone: &ZoneId, + process: &SupervisorProcessResource, + ) -> Result, ShellTerminalError> { + Self::get_resource(client, zone, process.resource_ref(), "shell-process-get") + } + + fn get_resource( + client: &DaemonResourceApiClient, + zone: &ZoneId, + resource_ref: &ResourceRef, + operation: &str, + ) -> Result, ShellTerminalError> { + let target = Self::shell_resource_identity(zone, resource_ref); + let mut projection = resource_wire::Projection::new(); + projection.kind = + protobuf::EnumOrUnknown::new(resource_wire::ProjectionKind::PROJECTION_KIND_FULL); + let mut request = resource_wire::GetRequest::new(); + request.meta = protobuf::MessageField::some(Self::shell_resource_request_meta(operation)); + request.target = protobuf::MessageField::some(target); + request.projection = protobuf::MessageField::some(projection); + let response = block_on_future(client.get(request)); + let Some(resource) = response.resource.as_ref() else { + return match response + .error + .as_ref() + .and_then(|error| error.kind.enum_value().ok()) + { + Some(resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_NOT_FOUND) => { + Ok(None) + } + _ => Err(ShellTerminalError::SupervisorAmbiguous), + }; + }; + d2b_contracts_resource::v3::ResourceEnvelope::from_json(&resource.canonical_json) + .map(Some) + .map_err(|_| ShellTerminalError::SupervisorAmbiguous) + } + + fn create_resource( + &self, + client: &DaemonResourceApiClient, + zone: &ZoneId, + resource_ref: &ResourceRef, + owner_ref: Option<&ResourceRef>, + payload: Vec, + operation: &str, + ) -> Result<(), ShellTerminalError> { + let target = Self::shell_resource_identity(zone, resource_ref); + let mut body = resource_wire::ResourceEnvelopeBytes::new(); + body.identity = protobuf::MessageField::some(target.clone()); + body.canonical_json = payload.clone(); + body.payload_digest = d2b_contracts_resource::v3::canonical_digest( + d2b_contracts_resource::v3::RESOURCE_ENVELOPE_DOMAIN_TAG, + &payload, + ); + let mut precondition = resource_wire::Precondition::new(); + precondition.kind = protobuf::EnumOrUnknown::new( + resource_wire::PreconditionKind::PRECONDITION_KIND_CREATE_ABSENT, + ); + let mut mutation = resource_wire::Mutation::new(); + mutation.kind = + protobuf::EnumOrUnknown::new(resource_wire::MutationKind::MUTATION_KIND_CREATE); + mutation.target = protobuf::MessageField::some(target); + mutation.precondition = protobuf::MessageField::some(precondition); + mutation.resource = protobuf::MessageField::some(body); + if let Some(owner_ref) = owner_ref { + mutation.owner = + protobuf::MessageField::some(Self::shell_resource_identity(zone, owner_ref)); + } + let mut request = resource_wire::CreateRequest::new(); + request.meta = protobuf::MessageField::some(Self::shell_resource_request_meta(operation)); + request.mutation = protobuf::MessageField::some(mutation); + let response = block_on_future(client.create(request)); + if response.error.is_some() { + return Err(ShellTerminalError::SupervisorAmbiguous); + } + Ok(()) + } + + fn process_matches( + envelope: &d2b_contracts_resource::v3::ResourceEnvelope, + process: &SupervisorProcessResource, + ) -> bool { + if envelope.resource_type().as_str() != "Process" + || envelope.metadata().owner_ref() != Some(process.owner_ref()) + || envelope + .spec() + .provider_ref() + .map(ResourceRef::to_canonical_string) + != Some("Provider/system-systemd".to_owned()) + || envelope.status().phase() == d2b_contracts_resource::v3::ResourcePhase::Deleted + { + return false; + } + let expected = serde_json::to_vec(process.spec()).ok(); + let actual = envelope.spec().base().to_canonical_bytes(); + expected.is_some_and(|expected| { + serde_json::from_slice::(&expected).ok() + == serde_json::from_slice::(&actual).ok() + }) + } + + fn process_has_finalizer(envelope: &d2b_contracts_resource::v3::ResourceEnvelope) -> bool { + serde_json::to_value(envelope.metadata()) + .ok() + .and_then(|metadata| metadata.get("finalizers").cloned()) + .and_then(|finalizers| finalizers.as_array().cloned()) + .is_some_and(|finalizers| { + finalizers + .iter() + .any(|finalizer| finalizer.as_str() == Some(PROCESS_RUNTIME_FINALIZER)) + }) + } + + fn add_process_finalizer( + &self, + client: &DaemonResourceApiClient, + zone: &ZoneId, + process: &SupervisorProcessResource, + envelope: &d2b_contracts_resource::v3::ResourceEnvelope, + ) -> Result<(), ShellTerminalError> { + let mut mutation = resource_wire::Mutation::new(); + mutation.kind = protobuf::EnumOrUnknown::new( + resource_wire::MutationKind::MUTATION_KIND_UPDATE_FINALIZERS, + ); + mutation.target = + protobuf::MessageField::some(Self::shell_resource_identity_with_revision( + zone, + process.resource_ref(), + envelope.metadata().uid(), + envelope.metadata().generation().get(), + envelope.metadata().revision().get(), + )); + mutation.precondition = protobuf::MessageField::some({ + let mut precondition = resource_wire::Precondition::new(); + precondition.kind = protobuf::EnumOrUnknown::new( + resource_wire::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION, + ); + precondition.expected_revision = Some(envelope.metadata().revision().get()); + precondition + }); + mutation + .add_finalizers + .push(PROCESS_RUNTIME_FINALIZER.to_owned()); + let mut request = resource_wire::UpdateFinalizersRequest::new(); + let operation = format!( + "shell-process-finalizer-{}", + shell_ref_digest(&[process.resource_ref().name().as_str()]) + ); + request.meta = protobuf::MessageField::some(Self::shell_resource_request_meta(&operation)); + request.mutation = protobuf::MessageField::some(mutation); + let response = block_on_future(client.update_finalizers(request)); + if response.error.is_some() { + return Err(ShellTerminalError::SupervisorAmbiguous); + } + Ok(()) + } + + fn create_process( + &self, + client: &DaemonResourceApiClient, + zone: &ZoneId, + process: &SupervisorProcessResource, + ) -> Result<(), ShellTerminalError> { + let mut spec = serde_json::to_value(process.spec()) + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + spec.as_object_mut() + .ok_or(ShellTerminalError::SupervisorAmbiguous)? + .insert( + "providerRef".to_owned(), + Value::String("Provider/system-systemd".to_owned()), + ); + let payload = Self::shell_process_resource_payload( + zone, + process.resource_ref(), + Some(process.owner_ref()), + spec, + )?; + let operation = format!( + "shell-process-create-{}", + shell_ref_digest(&[process.resource_ref().name().as_str()]) + ); + self.create_resource( + client, + zone, + process.resource_ref(), + Some(process.owner_ref()), + payload, + &operation, + ) + } + + fn delete_process( + &self, + client: &DaemonResourceApiClient, + zone: &ZoneId, + process: &SupervisorProcessResource, + envelope: &d2b_contracts_resource::v3::ResourceEnvelope, + ) -> Result<(), ShellTerminalError> { + let mut mutation = resource_wire::Mutation::new(); + mutation.kind = + protobuf::EnumOrUnknown::new(resource_wire::MutationKind::MUTATION_KIND_DELETE); + mutation.target = + protobuf::MessageField::some(Self::shell_resource_identity_with_revision( + zone, + process.resource_ref(), + envelope.metadata().uid(), + envelope.metadata().generation().get(), + envelope.metadata().revision().get(), + )); + let mut precondition = resource_wire::Precondition::new(); + precondition.kind = protobuf::EnumOrUnknown::new( + resource_wire::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION, + ); + precondition.expected_revision = Some(envelope.metadata().revision().get()); + mutation.precondition = protobuf::MessageField::some(precondition); + let mut request = resource_wire::DeleteRequest::new(); + let operation = format!( + "shell-process-delete-{}", + shell_ref_digest(&[process.resource_ref().name().as_str()]) + ); + request.meta = protobuf::MessageField::some(Self::shell_resource_request_meta(&operation)); + request.mutation = protobuf::MessageField::some(mutation); + let response = block_on_future(client.delete(request)); + match response + .error + .as_ref() + .and_then(|error| error.kind.enum_value().ok()) + { + None + | Some(resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_NOT_FOUND) => { + Ok(()) + } + Some(_) => Err(ShellTerminalError::SupervisorAmbiguous), + } + } + + fn shell_resource_request_meta(operation: &str) -> resource_wire::RequestMeta { + let mut meta = resource_wire::RequestMeta::new(); + meta.operation_id = operation.to_owned(); + meta.idempotency_key = operation.to_owned(); + meta.correlation_id = operation.to_owned(); + meta.trace_id = operation.to_owned(); + meta.deadline_ms = 30_000; + meta + } + + fn shell_resource_identity( + zone: &ZoneId, + resource_ref: &ResourceRef, + ) -> resource_wire::ResourceIdentity { + let mut identity = resource_wire::ResourceIdentity::new(); + identity.zone = zone.as_str().to_owned(); + identity.resource_type = resource_ref.resource_type().as_str().to_owned(); + identity.name = resource_ref.name().as_str().to_owned(); + identity + } + + fn shell_resource_identity_with_revision( + zone: &ZoneId, + resource_ref: &ResourceRef, + uid: &ResourceUid, + generation: u64, + revision: u64, + ) -> resource_wire::ResourceIdentity { + let mut identity = Self::shell_resource_identity(zone, resource_ref); + identity.uid = Some(uid.as_str().to_owned()); + identity.generation = Some(generation); + identity.revision = Some(revision); + identity + } + + fn shell_process_resource_payload( + zone: &ZoneId, + resource_ref: &ResourceRef, + owner_ref: Option<&ResourceRef>, + spec: Value, + ) -> Result, ShellTerminalError> { + Self::shell_resource_payload( + zone, + resource_ref, + owner_ref, + spec, + &[PROCESS_RUNTIME_FINALIZER], + ) + } + + fn shell_resource_payload( + zone: &ZoneId, + resource_ref: &ResourceRef, + owner_ref: Option<&ResourceRef>, + spec: Value, + finalizers: &[&str], + ) -> Result, ShellTerminalError> { + let metadata = json!({ + "name": resource_ref.name().as_str(), + "zone": zone.as_str(), + "generation": 1, + "revision": 1, + "ownerRef": owner_ref.map(ResourceRef::to_canonical_string), + "finalizers": finalizers, + "deletionRequestedAt": null, + "createdAt": "1970-01-01T00:00:00.000Z", + "updatedAt": "1970-01-01T00:00:00.000Z", + "managedBy": "controller", + }); + let value = json!({ + "apiVersion": "resources.d2bus.org/v3", + "type": resource_ref.resource_type().as_str(), + "metadata": metadata, + "spec": spec, + "status": { + "completedAt": null, + "conditions": [], + "lastReconciledAt": null, + "observedGeneration": 0, + "outcome": null, + "phase": "Pending", + "resource": {}, + "startedAt": null, + "update": { + "dependencies": {"count": 0, "refs": []}, + "disruption": "None", + "lastAssessedAt": null, + "observedGeneration": 0, + "operationId": null, + "owned": {"count": 0, "refs": []}, + "preserveState": true, + "reasons": [], + "state": "Unknown", + "targetGeneration": 1 + } + } + }); + let bytes = + serde_json::to_vec(&value).map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + let mut validation = value; + validation + .get_mut("metadata") + .and_then(Value::as_object_mut) + .ok_or(ShellTerminalError::SupervisorAmbiguous)? + .insert( + "uid".to_owned(), + Value::String("00000000-0000-4000-8000-000000000000".to_owned()), + ); + let validation = + serde_json::to_vec(&validation).map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + d2b_contracts_resource::v3::ResourceEnvelope::from_json(&validation) + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + d2b_contracts_resource::v3::CanonicalJsonValue::parse(&bytes) + .map(|value| value.to_canonical_bytes()) + .map_err(|_| ShellTerminalError::SupervisorAmbiguous) + } +} + +impl DaemonShellAuthority { + fn restore_pool( + &self, + pool: &ShellPool, + attached_streams: u32, + ) -> Result<(), ShellTerminalError> { + self.ledger.restore_pool(pool, attached_streams) + } + + fn open_session( + &self, + session: &ShellSession, + ) -> Result { + self.ledger.open_session(session) + } + + fn ensure_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError> { + self.ledger.validate_session(session)?; + self.ensure_supervisor_process_resource(session) + } + + fn ensure_supervisor_process_resource( + &self, + session: &ShellSession, + ) -> Result<(), ShellTerminalError> { + let (zone, client) = self.client_for_session(session)?; + self.ensure_shell_session_anchor(&client, &zone, session)?; + let process = SupervisorProcessResource::for_session(session); + match self.get_process(&client, &zone, &process)? { + Some(envelope) if Self::process_matches(&envelope, &process) => { + if Self::process_has_finalizer(&envelope) { + Ok(()) + } else { + self.add_process_finalizer(&client, &zone, &process, &envelope) + } + } + Some(_) => Err(ShellTerminalError::SupervisorAmbiguous), + None => match self.create_process(&client, &zone, &process) { + Ok(()) => Ok(()), + Err(_) => match self.get_process(&client, &zone, &process)? { + Some(envelope) if Self::process_matches(&envelope, &process) => { + if Self::process_has_finalizer(&envelope) { + Ok(()) + } else { + self.add_process_finalizer(&client, &zone, &process, &envelope) + } + } + _ => Err(ShellTerminalError::SupervisorAmbiguous), + }, + }, + } + } + + fn ensure_shell_session_anchor( + &self, + client: &DaemonResourceApiClient, + zone: &ZoneId, + session: &ShellSession, + ) -> Result<(), ShellTerminalError> { + let session_ref = + ResourceRef::parse(&format!("{}/{}", session.resource_type(), session.name())) + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + if Self::get_resource(client, zone, &session_ref, "shell-session-anchor-get")?.is_some() { + return Ok(()); + } + let spec = json!({ + "providerRef": "Provider/shell-terminal", + "executionRef": session.supervisor_execution_ref().to_canonical_string(), + "userRef": session.supervisor_user_ref().to_canonical_string(), + "loginShellRef": session.login_shell_ref(), + "sessionName": session.session_name(), + "outputRingCapacity": session.output_ring_capacity(), + "desiredLifecycle": "running", + }); + let operation = format!( + "shell-session-anchor-create-{}", + shell_ref_digest(&[session.name()]) + ); + let payload = Self::shell_resource_payload(zone, &session_ref, None, spec, &[])?; + match self.create_resource(client, zone, &session_ref, None, payload, &operation) { + Ok(()) => Ok(()), + Err(_) => Self::get_resource(client, zone, &session_ref, &operation) + .map(|resource| resource.is_some()) + .and_then(|present| { + present + .then_some(()) + .ok_or(ShellTerminalError::SupervisorAmbiguous) + }), + } + } + + fn remove_supervisor_process_resource( + &self, + session: &ShellSession, + ) -> Result<(), ShellTerminalError> { + let (zone, client) = self.client_for_session(session)?; + let process = SupervisorProcessResource::for_session(session); + let Some(envelope) = self.get_process(&client, &zone, &process)? else { + return Ok(()); + }; + if envelope.status().phase() == d2b_contracts_resource::v3::ResourcePhase::Deleted { + return Ok(()); + } + if !Self::process_matches(&envelope, &process) { + return Err(ShellTerminalError::SupervisorAmbiguous); + } + self.delete_process(&client, &zone, &process, &envelope) + } + + fn ensure_shell_resources( + &self, + pool: &ShellPool, + session: &ShellSession, + ) -> Result { + let zone = ZoneId::parse(pool.zone()).map_err(|_| ShellTerminalError::WrongZone)?; + if session.zone() != pool.zone() + || session.execution_target() != pool.execution_target() + || session.workload_user() != pool.workload_user() + || session.login_shell_ref() != pool.spec().login_shell_ref() + { + return Err(ShellTerminalError::SupervisorAmbiguous); + } + let client = self.client_for_zone(&zone)?; + let pool_ref = ResourceRef::parse(&format!("{}/{}", pool.resource_type(), pool.name())) + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + let pool_spec = json!({ + "providerRef": "Provider/shell-terminal", + "executionRef": format!( + "{}/{}", + if pool.execution_target().is_host() { "Host" } else { "Guest" }, + pool.execution_target().name() + ), + "userRef": format!("User/{}", pool.workload_user()), + "loginShellRef": pool.spec().login_shell_ref(), + "maxSessions": pool.spec().max_sessions(), + "maxAttached": pool.spec().max_attached(), + "outputRingCapacity": pool.spec().output_ring_capacity(), + }); + let pool_operation = format!("shell-pool-create-{}", shell_ref_digest(&[pool.name()])); + let _ = + self.ensure_resource(&client, &zone, &pool_ref, None, pool_spec, &pool_operation)?; + + let session_ref = + ResourceRef::parse(&format!("{}/{}", session.resource_type(), session.name())) + .map_err(|_| ShellTerminalError::SupervisorAmbiguous)?; + let session_spec = json!({ + "providerRef": "Provider/shell-terminal", + "poolRef": pool_ref.to_canonical_string(), + "executionRef": session.supervisor_execution_ref().to_canonical_string(), + "userRef": session.supervisor_user_ref().to_canonical_string(), + "loginShellRef": session.login_shell_ref(), + "sessionName": session.session_name(), + "outputRingCapacity": session.output_ring_capacity(), + "desiredLifecycle": "running", + }); + let session_operation = format!( + "shell-session-create-{}", + shell_ref_digest(&[session.name()]) + ); + let session_created = self.ensure_resource( + &client, + &zone, + &session_ref, + Some(&pool_ref), + session_spec, + &session_operation, + )?; + Ok(session_created) + } + + fn ensure_resource( + &self, + client: &DaemonResourceApiClient, + zone: &ZoneId, + resource_ref: &ResourceRef, + owner_ref: Option<&ResourceRef>, + spec: Value, + operation: &str, + ) -> Result { + let expected_spec = spec.clone(); + let matches = |envelope: &d2b_contracts_resource::v3::ResourceEnvelope| { + envelope.resource_type() == resource_ref.resource_type() + && envelope.metadata().name() == resource_ref.name() + && envelope.metadata().owner_ref() == owner_ref + && envelope.status().phase() != d2b_contracts_resource::v3::ResourcePhase::Deleted + && serde_json::from_slice::(&envelope.spec().base().to_canonical_bytes()) + .ok() + .is_some_and(|actual| actual == expected_spec) + }; + match Self::get_resource(client, zone, resource_ref, operation) { + Ok(Some(envelope)) if matches(&envelope) => return Ok(false), + Ok(Some(_)) => return Err(ShellTerminalError::SupervisorAmbiguous), + Ok(None) => {} + Err(error) => return Err(error), + } + let payload = Self::shell_resource_payload(zone, resource_ref, owner_ref, spec, &[])?; + match self.create_resource(client, zone, resource_ref, owner_ref, payload, operation) { + Ok(()) => Ok(true), + Err(_) => match Self::get_resource(client, zone, resource_ref, operation)? { + Some(envelope) if matches(&envelope) => Ok(false), + _ => Err(ShellTerminalError::SupervisorAmbiguous), + }, + } + } +} + +/// Production Process resource adapter used by attached and detached +/// execution owners. Resource creation is durable; stream I/O is carried by +/// the already enrolled target-local ComponentSession. +struct ComponentSessionProcessResourcePort { + client: Arc, + driver: d2b_session::SessionDriverHandle, + zone: ZoneId, + execution_ref: ResourceRef, + resource_ref: Option, + domain: String, + user_ref: Option, + template: String, + allow_create: bool, +} + +impl std::fmt::Debug for ComponentSessionProcessResourcePort { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("ComponentSessionProcessResourcePort()") + } +} + +impl ComponentSessionProcessResourcePort { + fn ephemeral_spec( + &self, + execution_ref: &ResourceRef, + detached: bool, + ) -> Result { + if !matches!(execution_ref.resource_type().as_str(), "Host" | "Guest") { + return Err(exec_session::ExecEstablishError::Protocol); + } + let mut spec = serde_json::json!({ + "providerRef": "Provider/system-systemd", + "executionRef": execution_ref.to_canonical_string(), + "domain": self.domain, + "processClass": "worker", + "template": self.template, + "startDeadline": "60s", + "runtimeDeadline": if detached { "24h" } else { "6h" }, + "successfulTtl": "1h", + "failedTtl": "24h", + "incidentHold": false, + }); + let object = spec + .as_object_mut() + .ok_or(exec_session::ExecEstablishError::Protocol)?; + object.insert( + "userRef".to_owned(), + self.user_ref + .as_ref() + .map_or(Value::Null, |user_ref| Value::String(user_ref.clone())), + ); + Ok(spec) + } + + fn create_request( + &self, + resource_ref: &ResourceRef, + spec: Value, + ) -> Result { + let payload = DaemonShellAuthority::shell_process_resource_payload( + &self.zone, + resource_ref, + None, + spec, + ) + .map_err(|_| exec_session::ExecEstablishError::Protocol)?; + let target = DaemonShellAuthority::shell_resource_identity(&self.zone, resource_ref); + let mut body = resource_wire::ResourceEnvelopeBytes::new(); + body.identity = protobuf::MessageField::some(target.clone()); + body.canonical_json = payload.clone(); + body.payload_digest = d2b_contracts_resource::v3::canonical_digest( + d2b_contracts_resource::v3::RESOURCE_ENVELOPE_DOMAIN_TAG, + &payload, + ); + let mut precondition = resource_wire::Precondition::new(); + precondition.kind = protobuf::EnumOrUnknown::new( + resource_wire::PreconditionKind::PRECONDITION_KIND_CREATE_ABSENT, + ); + let mut mutation = resource_wire::Mutation::new(); + mutation.kind = + protobuf::EnumOrUnknown::new(resource_wire::MutationKind::MUTATION_KIND_CREATE); + mutation.target = protobuf::MessageField::some(target); + mutation.precondition = protobuf::MessageField::some(precondition); + mutation.resource = protobuf::MessageField::some(body); + let mut request = resource_wire::CreateRequest::new(); + let operation = format!( + "process-resource-create-{}", + shell_ref_digest(&[resource_ref.name().as_str()]) + ); + request.meta = protobuf::MessageField::some( + DaemonShellAuthority::shell_resource_request_meta(&operation), + ); + request.mutation = protobuf::MessageField::some(mutation); + Ok(request) + } +} + +#[async_trait] +impl exec_session_real::ProcessResourcePort for ComponentSessionProcessResourcePort { + async fn create_ephemeral_process( + &self, + execution_ref: &ResourceRef, + spec: &exec_session::ExecStartSpec, + ) -> Result { + let Some(resource_ref) = self.resource_ref.as_ref() else { + return Err(exec_session::ExecEstablishError::Capability); + }; + let expected_spec = self.ephemeral_spec(execution_ref, spec.detached)?; + match self.get_resource(resource_ref).await { + Ok(Some(existing)) => { + let existing_execution = + existing + .spec() + .base() + .get("executionRef") + .and_then(|value| match value { + d2b_contracts_resource::v3::CanonicalJsonValue::String(value) => { + Some(value.as_str()) + } + _ => None, + }); + let expected_execution = execution_ref.to_canonical_string(); + let existing_spec = + serde_json::from_slice::(&existing.spec().base().to_canonical_bytes()) + .ok(); + if existing_execution != Some(expected_execution.as_str()) + || existing_spec.as_ref() != Some(&expected_spec) + { + return Err(exec_session::ExecEstablishError::Capability); + } + return exec_session_real::EphemeralProcessHandle::new( + ResourceRef::parse(&format!( + "EphemeralProcess/{}", + existing.metadata().name().as_str() + )) + .map_err(|_| exec_session::ExecEstablishError::Protocol)?, + 0, + 0, + 0, + ); + } + Ok(None) => {} + Err(error) => return Err(resource_error_to_establish(&error)), + } + if !self.allow_create { + return Err(exec_session::ExecEstablishError::Capability); + } + let request = self.create_request(resource_ref, expected_spec)?; + let response = self.client.create(request).await; + if let Some(error) = response.error.as_ref() { + return Err(resource_error_to_establish(error)); + } + let resource_ref = response + .resource + .as_ref() + .and_then(|resource| resource.identity.as_ref()) + .and_then(|identity| { + let value = format!("{}/{}", identity.resource_type, identity.name); + ResourceRef::parse(&value).ok() + }) + .ok_or(exec_session::ExecEstablishError::Protocol)?; + exec_session_real::EphemeralProcessHandle::new(resource_ref, 0, 0, 0) + } + + async fn attach_process( + &self, + process: &exec_session_real::EphemeralProcessHandle, + _tty: bool, + _initial_size: Option<(u32, u32)>, + ) -> Result, exec_session::ExecEstablishError> { + match self.get_resource(process.resource_ref()).await { + Ok(Some(_)) => {} + Ok(None) => return Err(exec_session::ExecEstablishError::Capability), + Err(error) => return Err(resource_error_to_establish(&error)), + } + exec_session_real::open_component_session_process( + self.driver.clone(), + next_component_session_stream_number(), + ) + .await + } +} + +#[async_trait] +impl exec_detached::DetachedProcessResourcePort for ComponentSessionProcessResourcePort { + async fn create_ephemeral_process( + &self, + execution_ref: &ResourceRef, + spec: &exec_session::ExecStartSpec, + ) -> Result { + let Some(resource_ref) = self.resource_ref.as_ref() else { + return Err(exec_session::ExecOpError::Capability); + }; + let expected_spec = self + .ephemeral_spec(execution_ref, spec.detached) + .map_err(establish_error_as_exec_error)?; + match self.get_resource(resource_ref).await { + Ok(Some(existing)) => { + let existing_execution = + existing + .spec() + .base() + .get("executionRef") + .and_then(|value| match value { + d2b_contracts_resource::v3::CanonicalJsonValue::String(value) => { + Some(value.as_str()) + } + _ => None, + }); + let expected_execution = execution_ref.to_canonical_string(); + let existing_spec = + serde_json::from_slice::(&existing.spec().base().to_canonical_bytes()) + .ok(); + if existing_execution != Some(expected_execution.as_str()) + || existing_spec.as_ref() != Some(&expected_spec) + { + return Err(exec_session::ExecOpError::Capability); + } + let bytes = existing + .canonical_bytes() + .map_err(|_| exec_session::ExecOpError::Protocol)?; + let state = detached_status_from_resource(&bytes) + .map_err(|_| exec_session::ExecOpError::Protocol)? + .state; + return Ok(public_wire::ExecDetachedCreateResult { + exec_id: resource_ref.name().as_str().to_owned(), + state, + }); + } + Ok(None) => {} + Err(error) => return Err(resource_error_to_op(&error)), + } + let request = self + .create_request(resource_ref, expected_spec) + .map_err(establish_error_as_exec_error)?; + let response = self.client.create(request).await; + if let Some(error) = response.error.as_ref() { + return Err(resource_error_to_op(error)); + } + let exec_id = response + .resource + .as_ref() + .and_then(|resource| resource.identity.as_ref()) + .map(|identity| identity.name.clone()) + .ok_or(exec_session::ExecOpError::Protocol)?; + Ok(public_wire::ExecDetachedCreateResult { + exec_id, + state: d2b_contracts_control::guest_wire::ExecState::Running, + }) + } + + async fn list_ephemeral_processes( + &self, + execution_ref: &ResourceRef, + ) -> Result { + let mut request = resource_wire::ListRequest::new(); + let operation = format!( + "process-resource-list-{}", + shell_ref_digest(&[execution_ref.name().as_str()]) + ); + request.meta = protobuf::MessageField::some( + DaemonShellAuthority::shell_resource_request_meta(&operation), + ); + request.resource_types.push("EphemeralProcess".to_owned()); + let mut filter = resource_wire::ListFilter::new(); + filter.field = "spec.executionRef".to_owned(); + filter.values.push(execution_ref.to_canonical_string()); + request.filters.push(filter); + request.page_size = 256; + let mut projection = resource_wire::Projection::new(); + projection.kind = + protobuf::EnumOrUnknown::new(resource_wire::ProjectionKind::PROJECTION_KIND_FULL); + request.projection = protobuf::MessageField::some(projection); + let response = self.client.list(request).await; + if let Some(error) = response.error.as_ref() { + return Err(resource_error_to_op(error)); + } + let execs = response + .resources + .iter() + .filter_map(|resource| { + detached_status_from_resource(&resource.canonical_json) + .ok() + .map(|status| public_wire::ExecDetachedListEntry { + exec_id: status.exec_id, + state: status.state, + started_at: String::new(), + start_offset: status.start_offset, + end_offset: status.end_offset, + exit_code: status.exit_code, + signal: status.signal, + stdout_start_offset: status.start_offset, + stdout_end_offset: status.end_offset, + stderr_start_offset: status.start_offset, + stderr_end_offset: status.end_offset, + dropped_bytes: status.dropped_bytes, + stdout_dropped_bytes: 0, + stderr_dropped_bytes: 0, + truncated: status.truncated, + stdout_truncated: status.truncated, + stderr_truncated: status.truncated, + }) + }) + .collect(); + Ok(public_wire::ExecDetachedListResult { execs }) + } + + async fn status_ephemeral_process( + &self, + process_ref: &ResourceRef, + ) -> Result { + let envelope = self + .get_resource(process_ref) + .await + .map_err(|error| resource_error_to_op(&error))? + .ok_or(exec_session::ExecOpError::Guest( + exec_session::GuestOpError::ExecNotFound, + ))?; + let bytes = envelope + .canonical_bytes() + .map_err(|_| exec_session::ExecOpError::Protocol)?; + detached_status_from_resource(&bytes).map_err(|_| exec_session::ExecOpError::Protocol) + } + + async fn logs_ephemeral_process( + &self, + process_ref: &ResourceRef, + stdout_offset: Option, + stderr_offset: Option, + max_len: Option, + ) -> Result { + if self + .get_resource(process_ref) + .await + .map_err(|error| resource_error_to_op(&error))? + .is_none() + { + return Err(exec_session::ExecOpError::Guest( + exec_session::GuestOpError::ExecNotFound, + )); + } + let client = exec_session_real::open_component_session_process( + self.driver.clone(), + next_component_session_stream_number(), + ) + .await + .map_err(establish_error_as_exec_error)?; + let max_len = max_len.unwrap_or(u64::from(public_wire::EXEC_MAX_CHUNK_BYTES)); + let stdout_offset = stdout_offset.unwrap_or(0); + let stderr_offset = stderr_offset.unwrap_or(0); + let result = async { + let stdout = client + .read_output( + d2bd_runtime::terminal_session::OutputStreamSel::Stdout, + stdout_offset, + max_len, + false, + 0, + SHELL_MANAGEMENT_TIMEOUT, + ) + .await + .map_err(|_| exec_session::ExecOpError::Transport)?; + let stderr = client + .read_output( + d2bd_runtime::terminal_session::OutputStreamSel::Stderr, + stderr_offset, + max_len.saturating_sub(stdout.data.len() as u64), + false, + 0, + SHELL_MANAGEMENT_TIMEOUT, + ) + .await + .map_err(|_| exec_session::ExecOpError::Transport)?; + Ok::<_, exec_session::ExecOpError>((stdout, stderr)) + } + .await; + let _ = client.cancel(0, SHELL_MANAGEMENT_TIMEOUT).await; + let (stdout, stderr) = result?; + Ok(public_wire::ExecDetachedLogsResult { + exec_id: process_ref.name().as_str().to_owned(), + stdout_base64: d2b_core::base64_codec::encode(&stdout.data), + stderr_base64: d2b_core::base64_codec::encode(&stderr.data), + start_offset: stdout_offset, + end_offset: stdout.next_offset, + dropped_bytes: stdout.dropped_bytes.saturating_add(stderr.dropped_bytes), + truncated: stdout.truncated || stderr.truncated, + stdout_start_offset: stdout_offset, + stdout_end_offset: stdout.next_offset, + stdout_next_offset: stdout.next_offset, + stdout_eof: stdout.eof, + stdout_dropped_bytes: stdout.dropped_bytes, + stdout_truncated: stdout.truncated, + stderr_start_offset: stderr_offset, + stderr_end_offset: stderr.next_offset, + stderr_next_offset: stderr.next_offset, + stderr_eof: stderr.eof, + stderr_dropped_bytes: stderr.dropped_bytes, + stderr_truncated: stderr.truncated, + }) + } + + async fn kill_ephemeral_process( + &self, + process_ref: &ResourceRef, + ) -> Result { + let Some(envelope) = self + .get_resource(process_ref) + .await + .map_err(|error| resource_error_to_op(&error))? + else { + return Ok(public_wire::ExecDetachedKillResult { + exec_id: process_ref.name().as_str().to_owned(), + result: public_wire::ExecDetachedKillOutcome::AlreadyTerminal, + state: d2b_contracts_control::guest_wire::ExecState::Exited, + }); + }; + let envelope_bytes = envelope + .canonical_bytes() + .map_err(|_| exec_session::ExecOpError::Protocol)?; + let state = detached_status_from_resource(&envelope_bytes) + .map_err(|_| exec_session::ExecOpError::Protocol)? + .state; + if !matches!( + state, + d2b_contracts_control::guest_wire::ExecState::Created + | d2b_contracts_control::guest_wire::ExecState::Running + ) { + return Ok(public_wire::ExecDetachedKillResult { + exec_id: process_ref.name().as_str().to_owned(), + result: public_wire::ExecDetachedKillOutcome::AlreadyTerminal, + state, + }); + } + let mut mutation = resource_wire::Mutation::new(); + mutation.kind = + protobuf::EnumOrUnknown::new(resource_wire::MutationKind::MUTATION_KIND_DELETE); + mutation.target = protobuf::MessageField::some( + DaemonShellAuthority::shell_resource_identity_with_revision( + &self.zone, + process_ref, + envelope.metadata().uid(), + envelope.metadata().generation().get(), + envelope.metadata().revision().get(), + ), + ); + let mut precondition = resource_wire::Precondition::new(); + precondition.kind = protobuf::EnumOrUnknown::new( + resource_wire::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION, + ); + precondition.expected_revision = Some(envelope.metadata().revision().get()); + mutation.precondition = protobuf::MessageField::some(precondition); + let mut request = resource_wire::DeleteRequest::new(); + let operation = format!( + "process-resource-delete-{}", + shell_ref_digest(&[process_ref.name().as_str()]) + ); + request.meta = protobuf::MessageField::some( + DaemonShellAuthority::shell_resource_request_meta(&operation), + ); + request.mutation = protobuf::MessageField::some(mutation); + let response = self.client.delete(request).await; + if let Some(error) = response.error.as_ref() + && error.kind.enum_value().ok() + != Some(resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_NOT_FOUND) + { + return Err(resource_error_to_op(error)); + } + Ok(public_wire::ExecDetachedKillResult { + exec_id: process_ref.name().as_str().to_owned(), + result: public_wire::ExecDetachedKillOutcome::Cancelling, + state, + }) + } +} + +impl ComponentSessionProcessResourcePort { + async fn get_resource( + &self, + process_ref: &ResourceRef, + ) -> Result, resource_wire::ResourceError> + { + let mut target = DaemonShellAuthority::shell_resource_identity(&self.zone, process_ref); + target.resource_type = "EphemeralProcess".to_owned(); + let mut projection = resource_wire::Projection::new(); + projection.kind = + protobuf::EnumOrUnknown::new(resource_wire::ProjectionKind::PROJECTION_KIND_FULL); + let mut request = resource_wire::GetRequest::new(); + let operation = format!( + "process-resource-get-{}", + shell_ref_digest(&[process_ref.name().as_str()]) + ); + request.meta = protobuf::MessageField::some( + DaemonShellAuthority::shell_resource_request_meta(&operation), + ); + request.target = protobuf::MessageField::some(target); + request.projection = protobuf::MessageField::some(projection); + let response = self.client.get(request).await; + let Some(resource) = response.resource.as_ref() else { + if let Some(error) = response.error.as_ref() { + if error.kind.enum_value().ok() + == Some( + resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_NOT_FOUND, + ) + { + return Ok(None); + } + return Err(error.clone()); + } + return Err(resource_wire::ResourceError::new()); + }; + let envelope = + d2b_contracts_resource::v3::ResourceEnvelope::from_json(&resource.canonical_json) + .map_err(|_| resource_wire::ResourceError::new())?; + let execution = envelope + .spec() + .base() + .get("executionRef") + .and_then(|value| match value { + d2b_contracts_resource::v3::CanonicalJsonValue::String(value) => { + ResourceRef::parse(value).ok() + } + _ => None, + }); + if execution.as_ref() != Some(&self.execution_ref) { + return Err(resource_wire::ResourceError::new()); + } + Ok(Some(envelope)) + } +} + +impl ShellAuthorityPort for DaemonShellAuthority { + fn restore_pool( + &self, + pool: &ShellPool, + attached_streams: u32, + ) -> Result<(), ShellTerminalError> { + DaemonShellAuthority::restore_pool(self, pool, attached_streams) + } + + fn open_session( + &self, + session: &ShellSession, + ) -> Result { + DaemonShellAuthority::open_session(self, session) + } + + fn ensure_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError> { + DaemonShellAuthority::ensure_supervisor_process(self, session) + } + + fn remove_supervisor_process(&self, session: &ShellSession) -> Result<(), ShellTerminalError> { + self.ledger.validate_session(session)?; + if self.ledger.has_active_attachments(session)? { + return Err(ShellTerminalError::CapacityExceeded); + } + self.remove_supervisor_process_resource(session) + } + + fn verify_recovery( + &self, + session: &ShellSession, + identity: &d2b_provider_shell_terminal::SupervisorIdentity, + ) -> Result { + self.ledger.verify_recovery(session, identity) + } + + fn advance_session( + &self, + session: &ShellSession, + retired_identity: Option<&d2b_provider_shell_terminal::SupervisorIdentity>, + ) -> Result { + self.ledger.advance_session(session, retired_identity) + } + + fn claim_supervisor( + &self, + session: &ShellSession, + identity: &d2b_provider_shell_terminal::SupervisorIdentity, + ) -> Result<(), ShellTerminalError> { + self.ledger.claim_supervisor(session, identity) + } + + fn validate_session( + &self, + session: &ShellSession, + identity: &d2b_provider_shell_terminal::SupervisorIdentity, + ) -> Result<(), ShellTerminalError> { + ShellAuthorityPort::validate_session(self.ledger.as_ref(), session, identity) + } + + fn consume_capability( + &self, + session: &ShellSession, + identity: &d2b_provider_shell_terminal::SupervisorIdentity, + capability: &d2b_provider_shell_terminal::SessionCapability, + ) -> Result<(), ShellTerminalError> { + self.ledger + .consume_capability(session, identity, capability) + } + + fn admit_capability_attachment( + &self, + session: &ShellSession, + identity: &d2b_provider_shell_terminal::SupervisorIdentity, + capability: &d2b_provider_shell_terminal::SessionCapability, + ) -> Result { + self.ledger + .admit_capability_attachment(session, identity, capability) + } + + fn reserve_attachment( + &self, + session: &ShellSession, + identity: &d2b_provider_shell_terminal::SupervisorIdentity, + ) -> Result { + self.ledger.reserve_attachment(session, identity) + } + + fn release_attachment( + &self, + session: &ShellSession, + attachment: &d2b_provider_shell_terminal::Attachment, + ) -> Result<(), ShellTerminalError> { + self.ledger.release_attachment(session, attachment) + } + + fn reconcile_pool_attachments( + &self, + pool: &ShellPool, + attached_streams: u32, + ) -> Result<(), ShellTerminalError> { + self.ledger + .reconcile_pool_attachments(pool, attached_streams) + } + + fn retire_proven_stale( + &self, + pool: &ShellPool, + stale_attachments: &[d2b_provider_shell_terminal::Attachment], + attached_streams: u32, + ) -> Result<(), ShellTerminalError> { + self.ledger + .retire_proven_stale(pool, stale_attachments, attached_streams) + } + + fn finalize_session( + &self, + session: &ShellSession, + identity: Option<&d2b_provider_shell_terminal::SupervisorIdentity>, + ) -> Result<(), ShellTerminalError> { + self.ledger.finalize_session(session, identity) + } +} + +/// A single production routing decision for Process-family owners. `None` +/// means that either the target-local Resource API or its enrolled +/// ComponentSession is genuinely absent, in which case callers must use the +/// existing typed unavailable port. +struct RoutedProcessResourcePort(Option); + +impl std::fmt::Debug for RoutedProcessResourcePort { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("RoutedProcessResourcePort()") + } +} + +#[async_trait] +impl exec_session_real::ProcessResourcePort for RoutedProcessResourcePort { + async fn create_ephemeral_process( + &self, + execution_ref: &ResourceRef, + spec: &exec_session::ExecStartSpec, + ) -> Result { + let Some(port) = self.0.as_ref() else { + let _ = (execution_ref, spec); + return Err(exec_session::ExecEstablishError::Capability); + }; + port.create_ephemeral_process(execution_ref, spec).await + } + + async fn attach_process( + &self, + process: &exec_session_real::EphemeralProcessHandle, + tty: bool, + initial_size: Option<(u32, u32)>, + ) -> Result, exec_session::ExecEstablishError> { + let Some(port) = self.0.as_ref() else { + let _ = (process, tty, initial_size); + return Err(exec_session::ExecEstablishError::Capability); + }; + port.attach_process(process, tty, initial_size).await + } +} + +#[async_trait] +impl exec_detached::DetachedProcessResourcePort for RoutedProcessResourcePort { + async fn create_ephemeral_process( + &self, + execution_ref: &ResourceRef, + spec: &exec_session::ExecStartSpec, + ) -> Result { + let Some(port) = self.0.as_ref() else { + let _ = (execution_ref, spec); + return Err(exec_session::ExecOpError::DetachedUnavailable); + }; + port.create_ephemeral_process(execution_ref, spec).await + } + + async fn list_ephemeral_processes( + &self, + execution_ref: &ResourceRef, + ) -> Result { + let Some(port) = self.0.as_ref() else { + let _ = execution_ref; + return Err(exec_session::ExecOpError::DetachedUnavailable); + }; + port.list_ephemeral_processes(execution_ref).await + } + + async fn status_ephemeral_process( + &self, + process_ref: &ResourceRef, + ) -> Result { + let Some(port) = self.0.as_ref() else { + let _ = process_ref; + return Err(exec_session::ExecOpError::DetachedUnavailable); + }; + port.status_ephemeral_process(process_ref).await + } + + async fn logs_ephemeral_process( + &self, + process_ref: &ResourceRef, + stdout_offset: Option, + stderr_offset: Option, + max_len: Option, + ) -> Result { + let Some(port) = self.0.as_ref() else { + let _ = (process_ref, stdout_offset, stderr_offset, max_len); + return Err(exec_session::ExecOpError::DetachedUnavailable); + }; + port.logs_ephemeral_process(process_ref, stdout_offset, stderr_offset, max_len) + .await + } + + async fn kill_ephemeral_process( + &self, + process_ref: &ResourceRef, + ) -> Result { + let Some(port) = self.0.as_ref() else { + let _ = process_ref; + return Err(exec_session::ExecOpError::DetachedUnavailable); + }; + port.kill_ephemeral_process(process_ref).await + } +} + +fn production_process_resource_port( + state: &ServerState, + request: &Value, + execution_ref: &ResourceRef, +) -> RoutedProcessResourcePort { + let resource_ref = process_resource_ref(request); + if resource_ref.is_none() && request.get("method").and_then(Value::as_str) != Some("List") { + return RoutedProcessResourcePort(None); + } + let Some(zone) = request + .get("zoneRef") + .and_then(Value::as_str) + .and_then(|value| value.strip_prefix("Zone/")) + .and_then(|value| ZoneId::parse(value.to_owned()).ok()) + else { + return RoutedProcessResourcePort(None); + }; + let Some(client) = state + .resource_plane + .lock() + .ok() + .and_then(|plane| plane.clone()) + .and_then(|plane| plane.zone(&zone).ok()) + .and_then(|runtime| runtime.process_resource_client()) + else { + return RoutedProcessResourcePort(None); + }; + let Some(driver) = production_component_session_driver_for_service( + state, + interaction_composition::PROCESS_ATTACH_SERVICE, + execution_ref, + ) else { + return RoutedProcessResourcePort(None); + }; + let template = request + .get("template") + .and_then(Value::as_str) + .unwrap_or("shell-terminal") + .to_owned(); + if d2b_contracts_resource::v3::execution_policy::BoundedToken::parse(&template).is_err() { + return RoutedProcessResourcePort(None); + } + let domain = request + .get("domain") + .and_then(Value::as_str) + .unwrap_or("user") + .to_owned(); + if !matches!(domain.as_str(), "system" | "user") { + return RoutedProcessResourcePort(None); + } + let requested_user_ref = request + .get("userRef") + .and_then(Value::as_str) + .map(ToOwned::to_owned); + let needs_execution_spec = matches!( + request.get("method").and_then(Value::as_str), + Some("Create" | "Attach") + ); + let user_ref = if domain == "user" { + let trusted_user_ref = resolve_process_user_ref(state, execution_ref); + if requested_user_ref + .as_deref() + .is_some_and(|requested| Some(requested) != trusted_user_ref.as_deref()) + { + return RoutedProcessResourcePort(None); + } + trusted_user_ref + } else { + requested_user_ref + }; + if domain == "user" && needs_execution_spec && user_ref.is_none() { + return RoutedProcessResourcePort(None); + } + if let Some(user_ref) = user_ref.as_deref() + && ResourceRef::parse(user_ref) + .ok() + .is_none_or(|value| value.resource_type().as_str() != "User") + { + return RoutedProcessResourcePort(None); + } + RoutedProcessResourcePort(Some(ComponentSessionProcessResourcePort { + client, + driver, + zone, + execution_ref: execution_ref.clone(), + resource_ref, + domain, + user_ref, + template, + allow_create: request.get("method").and_then(Value::as_str) != Some("Attach"), + })) +} + +fn resolve_process_user_ref(state: &ServerState, execution_ref: &ResourceRef) -> Option { + if execution_ref.resource_type().as_str() != "Guest" { + return None; + } + let manifest = load_manifest(&state.config.artifacts.public_manifest_path).ok()?; + let user = manifest + .get(execution_ref.name().as_str())? + .get("sshUser") + .and_then(Value::as_str)?; + ResourceRef::parse(&format!("User/{user}")) + .ok() + .map(|value| value.to_canonical_string()) +} + +fn production_component_session_driver( + state: &ServerState, + target: &str, +) -> Option { + let target = ResourceRef::parse(&format!("Guest/{target}")).ok()?; + production_component_session_driver_for_service( + state, + interaction_composition::SHELL_SESSION_SERVICE, + &target, + ) +} + +fn production_component_session_driver_for_service( + state: &ServerState, + service: &str, + target: &ResourceRef, +) -> Option { + interaction_composition::blocking_component_session_driver_for_service( + &state.interaction_runtime, + service, + target, + ) +} + +fn resource_error_to_establish( + error: &resource_wire::ResourceError, +) -> exec_session::ExecEstablishError { + match error.kind.enum_value().ok() { + Some(resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_AUTHORIZATION_DENIED) => { + exec_session::ExecEstablishError::Auth + } + Some( + resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_PROVIDER_UNAVAILABLE + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_PLANE_UNAVAILABLE + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_TIMEOUT + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_BACKPRESSURE, + ) => exec_session::ExecEstablishError::Transport, + Some( + resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_UNSUPPORTED_CAPABILITY + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_NOT_FOUND + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_ALREADY_EXISTS + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_CONFLICT, + ) => exec_session::ExecEstablishError::Capability, + _ => exec_session::ExecEstablishError::Protocol, + } +} + +fn resource_error_to_op(error: &resource_wire::ResourceError) -> exec_session::ExecOpError { + match resource_error_to_establish(error) { + exec_session::ExecEstablishError::Transport => exec_session::ExecOpError::Transport, + exec_session::ExecEstablishError::Auth => exec_session::ExecOpError::Auth, + exec_session::ExecEstablishError::Capability => exec_session::ExecOpError::Capability, + exec_session::ExecEstablishError::Protocol => exec_session::ExecOpError::Protocol, + exec_session::ExecEstablishError::Timeout => exec_session::ExecOpError::Timeout, + exec_session::ExecEstablishError::OldGeneration => exec_session::ExecOpError::OldGeneration, + exec_session::ExecEstablishError::Guest(error) => exec_session::ExecOpError::Guest(error), + } +} + +fn detached_status_from_resource( + bytes: &[u8], +) -> Result { + let envelope = + d2b_contracts_resource::v3::ResourceEnvelope::from_json(bytes).map_err(|_| ())?; + if envelope.resource_type().as_str() != "EphemeralProcess" { + return Err(()); + } + let value: Value = serde_json::from_slice(bytes).map_err(|_| ())?; + let phase = value + .pointer("/status/phase") + .and_then(Value::as_str) + .ok_or(())?; + let state = match phase { + "Pending" | "Ready" | "Degraded" => d2b_contracts_control::guest_wire::ExecState::Running, + "Succeeded" | "Deleted" => d2b_contracts_control::guest_wire::ExecState::Exited, + "Failed" => d2b_contracts_control::guest_wire::ExecState::ProtocolError, + _ => d2b_contracts_control::guest_wire::ExecState::ProtocolError, + }; + let reason = value + .pointer("/status/outcome/code") + .and_then(Value::as_str) + .map(ToOwned::to_owned); + Ok(public_wire::ExecDetachedStatusResult { + exec_id: envelope.metadata().name().as_str().to_owned(), + state, + reason, + exit_code: None, + signal: None, + start_offset: 0, + end_offset: 0, + dropped_bytes: 0, + truncated: false, + }) +} + +fn next_component_session_stream_number() -> u16 { + u16::try_from(next_internal_process_stream_id() % u64::from(u16::MAX)) + .unwrap_or(1) + .max(1) +} + +fn next_internal_process_stream_id() -> u64 { + static STREAM_ID: AtomicU64 = AtomicU64::new(1); + loop { + let value = STREAM_ID.fetch_add(1, Ordering::Relaxed); + if value != 0 { + return value; + } + } } impl d2bd_runtime::exec_detached::DetachedExecContext for ServerState { @@ -908,6 +2506,8 @@ pub async fn serve(options: ServeOptions) -> Result<(), TypedError> { runtime_identity.daemon_uid.as_raw(), unsafe_local_helper_uids, )); + #[cfg(not(test))] + let resource_plane = Arc::new(Mutex::new(None)); let state = ServerState { daemon_uid: runtime_identity.daemon_uid.as_raw(), config, @@ -941,7 +2541,12 @@ pub async fn serve(options: ServeOptions) -> Result<(), TypedError> { detail: "provider lifecycle state unavailable".to_owned(), })?, ), + #[cfg(not(test))] + resource_plane: Arc::clone(&resource_plane), + #[cfg(test)] resource_plane: Arc::new(Mutex::new(None)), + #[cfg(not(test))] + shell_authority: Arc::new(DaemonShellAuthority::new(Arc::clone(&resource_plane))), interaction_runtime: Arc::new(tokio::sync::Mutex::new(None)), interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), @@ -1111,7 +2716,7 @@ pub async fn serve(options: ServeOptions) -> Result<(), TypedError> { .expect("interaction listener lock") = Some(listeners); tracing::info!( listener_count = paths.len(), - "interaction Provider ComponentSession listeners ready", + "ComponentSession listeners ready", ); } } else { @@ -1491,20 +3096,19 @@ pub async fn serve_guest(options: GuestServeOptions) -> Result<(), TypedError> { } else { PathBuf::from(d2bd_runtime::guest_mode::KERNEL_BOOT_ID_PATH) }; - let boot = d2bd_runtime::guest_mode::BootIdentity::read(&boot_id_path) - .map_err(|_| TypedError::InternalConfig { - detail: "guest kernel boot identity unavailable".to_owned(), - })?; - let guest_ref = ResourceRef::parse(&options.guest_ref).map_err(|_| { + let boot = d2bd_runtime::guest_mode::BootIdentity::read(&boot_id_path).map_err(|_| { TypedError::InternalConfig { - detail: "guest identity reference is invalid".to_owned(), + detail: "guest kernel boot identity unavailable".to_owned(), } })?; - let guest_uid = ResourceUid::parse(options.guest_uid).map_err(|_| { - TypedError::InternalConfig { + let guest_ref = + ResourceRef::parse(&options.guest_ref).map_err(|_| TypedError::InternalConfig { + detail: "guest identity reference is invalid".to_owned(), + })?; + let guest_uid = + ResourceUid::parse(options.guest_uid).map_err(|_| TypedError::InternalConfig { detail: "guest identity UID is invalid".to_owned(), - } - })?; + })?; let zone = ZoneId::parse(options.zone).map_err(|_| TypedError::InternalConfig { detail: "guest Zone identity is invalid".to_owned(), })?; @@ -1553,20 +3157,24 @@ pub async fn serve_guest(options: GuestServeOptions) -> Result<(), TypedError> { fs::create_dir_all(&options.state_dir).map_err(|_| TypedError::InternalConfig { detail: "guest state root unavailable".to_owned(), })?; - let local_private_path = options - .local_private_key_path - .ok_or_else(|| TypedError::InternalConfig { - detail: "guest ComponentSession private key is unavailable".to_owned(), - })?; - let parent_public_path = options - .parent_public_key_path - .ok_or_else(|| TypedError::InternalConfig { - detail: "parent Zone ComponentSession key is unavailable".to_owned(), - })?; + let local_private_path = + options + .local_private_key_path + .ok_or_else(|| TypedError::InternalConfig { + detail: "guest ComponentSession private key is unavailable".to_owned(), + })?; + let parent_public_path = + options + .parent_public_key_path + .ok_or_else(|| TypedError::InternalConfig { + detail: "parent Zone ComponentSession key is unavailable".to_owned(), + })?; let parent_public = read_public_key32(&parent_public_path)?; - let mut listener = runtime.bind_listener().map_err(|error| TypedError::InternalConfig { - detail: error.to_string(), - })?; + let mut listener = runtime + .bind_listener() + .map_err(|error| TypedError::InternalConfig { + detail: error.to_string(), + })?; let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .map_err(|_| TypedError::InternalIo { context: "install Guest SIGTERM handler".to_owned(), @@ -2433,12 +4041,9 @@ fn handle_connection_authorized( } } } - // Exec takes over the connection as the long-lived owner connection. - // Admin (SO_PEERCRED) is verified here, BEFORE any session work; then - // the connection + a cheap ServerState clone move to a SPAWNED owner - // handler. The admission permit moves with it so the in-flight slot - // is held for the lifetime of the exec session, not just the read. - if let d2bd_runtime::wire::Request::Exec(op) = &request { + if let d2bd_runtime::wire::Request::Resource(resource) = &request + && process_resource_owner_request(&resource.value()) + { if !matches!(peer.role, PeerRole::Admin) { let error = TypedError::AuthzNotAdmin { verb: "exec".to_owned(), @@ -2446,42 +4051,47 @@ fn handle_connection_authorized( let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); continue; } - if matches!(op, public_wire::ExecOp::Start(_)) { - // Recover the establishing op's envelope `opId` so the Start - // reply (and any establish error) echoes it for client - // correlation. Detached Start is also handled by the owner body, - // but returns after one ExecCreate instead of reserving a - // session slot or entering the attached FSM. - let first_op_id = d2bd_runtime::wire::exec_op_id(&frame); - let owner_state = state.clone(); - let owner_peer = peer.clone(); - let op = op.clone(); - // The exec owner blocks on the PTY indefinitely; clear the - // request read deadline before handing off the socket. - set_frame_read_deadline(&stream, None); - let owner_permit = permit; - match std::thread::Builder::new() - .name("d2b-exec-owner".to_owned()) - .spawn(move || { - run_exec_owner( - stream, - owner_state, - owner_peer, - first_op_id, - op, - owner_permit, - ); - }) { - Ok(_) => return Ok(()), - Err(err) => { - return Err(TypedError::InternalIo { - context: "spawn exec owner handler".to_owned(), - detail: err.to_string(), - }); - } + set_frame_read_deadline(&stream, None); + let owner_state = state.clone(); + let owner_peer = peer.clone(); + let owner_request = resource.value(); + let owner_permit = permit; + match std::thread::Builder::new() + .name("d2b-process-owner".to_owned()) + .spawn(move || { + run_process_resource_owner( + stream, + owner_state, + owner_peer, + owner_request, + owner_permit, + ); + }) { + Ok(_) => return Ok(()), + Err(err) => { + return Err(TypedError::InternalIo { + context: "spawn Process resource owner".to_owned(), + detail: err.to_string(), + }); } } } + // The retired feature-specific Exec wire is not a production route. + // Attached and detached execution must enter through the typed + // Process/EphemeralProcess resource path; retaining this refusal keeps + // old clients from reaching the guest-control bridge. + if let d2bd_runtime::wire::Request::Exec(_) = &request { + if !matches!(peer.role, PeerRole::Admin) { + let error = TypedError::AuthzNotAdmin { + verb: "exec".to_owned(), + }; + let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); + continue; + } + let error = retired_exec_wire_error(); + let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); + continue; + } // Gateway display operations can perform provider/relay orchestration. // Hand them off the serial accept loop just like exec owner sessions. if let d2bd_runtime::wire::Request::GatewayDisplay(op) = &request { @@ -2669,34 +4279,410 @@ fn dispatch_request_locked( d2bd_runtime::wire::Request::HostReconcile(req) => { dispatch_broker_host_reconcile_as(state, req, broker_caller_role_for_peer(peer)) } - d2bd_runtime::wire::Request::HostCutover(req) => cutover::dispatch(state, peer, req), - d2bd_runtime::wire::Request::ReadGuestConfig(req) => { - dispatch_read_guest_config(state, broker_caller_role_for_peer(peer), req) + d2bd_runtime::wire::Request::HostCutover(req) => cutover::dispatch(state, peer, req), + d2bd_runtime::wire::Request::ReadGuestConfig(req) => { + dispatch_read_guest_config(state, broker_caller_role_for_peer(peer), req) + } + // The feature-specific Exec wire is retained only by unit tests that + // characterize the retired response DTO. Production connections + // reject it before this dispatcher and never reach a guest-control + // caller. + d2bd_runtime::wire::Request::Exec(op) => { + #[cfg(test)] + { + dispatch_exec_management(state, peer, op) + } + #[cfg(not(test))] + { + let _ = op; + Err(retired_exec_wire_error()) + } + } + d2bd_runtime::wire::Request::Console(op) => dispatch_console(state, peer, op), + d2bd_runtime::wire::Request::GatewayDisplay(op) => { + dispatch_gateway_display(state, peer, op) + } + d2bd_runtime::wire::Request::Workload(op) => dispatch_workload(state, peer, op), + d2bd_runtime::wire::Request::Audio(op) => { + if !matches!(op, public_wire::AudioOp::Status(_)) + && !matches!(peer.role, PeerRole::Admin) + { + return Err(TypedError::AuthzNotAdmin { + verb: "audio".to_owned(), + }); + } + audio_dispatch::dispatch_audio(state, broker_caller_role_for_peer(peer), op) + } + d2bd_runtime::wire::Request::Resource(request) => { + dispatch_resource_request(state, peer, request) + } + } +} + +fn process_resource_owner_request(request: &Value) -> bool { + request.get("resourceType").and_then(Value::as_str) == Some("EphemeralProcess") + && matches!( + request.get("method").and_then(Value::as_str), + Some("Create" | "Attach") + ) + && request.get("sessionVerb").and_then(Value::as_str) == Some("attach") +} + +fn process_resource_ref(request: &Value) -> Option { + request + .get("resourceRef") + .and_then(Value::as_str) + .and_then(|value| ResourceRef::parse(value).ok()) + .filter(|resource| resource.resource_type().as_str() == "EphemeralProcess") + .or_else(|| { + request + .get("opId") + .and_then(Value::as_u64) + .and_then(|op_id| { + ResourceRef::parse(&format!("EphemeralProcess/exec-{op_id}")).ok() + }) + }) + .or_else(|| { + request + .get("requestId") + .and_then(Value::as_str) + .and_then(|request_id| { + let name = format!("EphemeralProcess/exec-{}", shell_ref_digest(&[request_id])); + ResourceRef::parse(&name).ok() + }) + }) + .or_else(|| { + request + .get("name") + .and_then(Value::as_str) + .and_then(|name| ResourceRef::parse(&format!("EphemeralProcess/{name}")).ok()) + }) + .or_else(|| { + request + .get("command") + .and_then(Value::as_array) + .and_then(|command| { + let command = command.iter().filter_map(Value::as_str).collect::>(); + let name = format!("EphemeralProcess/exec-{}", shell_ref_digest(&command)); + ResourceRef::parse(&name).ok() + }) + }) +} + +fn retired_exec_wire_error() -> TypedError { + TypedError::WireUnsupportedRequest { + request_type: "exec".to_owned(), + } +} + +fn process_resource_management_request(request: &Value) -> bool { + request.get("resourceType").and_then(Value::as_str) == Some("EphemeralProcess") + && matches!( + request.get("method").and_then(Value::as_str), + Some("List" | "Status" | "Logs" | "Kill") + ) +} + +fn process_resource_detached_create_request(request: &Value) -> bool { + request.get("resourceType").and_then(Value::as_str) == Some("EphemeralProcess") + && request.get("method").and_then(Value::as_str) == Some("Create") + && request.get("sessionVerb").and_then(Value::as_str) != Some("attach") +} + +fn map_resource_exec_error(error: exec_session::ExecOpError, vm: &str) -> TypedError { + match error { + exec_session::ExecOpError::Auth => TypedError::AuthzNotAdmin { + verb: "exec".to_owned(), + }, + exec_session::ExecOpError::Capability | exec_session::ExecOpError::DetachedUnavailable => { + TypedError::RuntimeCapabilityUnsupported { + vm: vm.to_owned(), + runtime_kind: "component-session".to_owned(), + capability: "process-attach".to_owned(), + verb: "exec".to_owned(), + } + } + exec_session::ExecOpError::Timeout => TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Timeout, + }, + exec_session::ExecOpError::Transport => TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Transport, + }, + exec_session::ExecOpError::StaleSession | exec_session::ExecOpError::OldGeneration => { + TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::OldGeneration, + } + } + exec_session::ExecOpError::Protocol | exec_session::ExecOpError::Guest(_) => { + TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + } + } + } +} + +fn establish_error_as_exec_error( + error: exec_session::ExecEstablishError, +) -> exec_session::ExecOpError { + match error { + exec_session::ExecEstablishError::Transport => exec_session::ExecOpError::Transport, + exec_session::ExecEstablishError::Auth => exec_session::ExecOpError::Auth, + exec_session::ExecEstablishError::Protocol => exec_session::ExecOpError::Protocol, + exec_session::ExecEstablishError::Timeout => exec_session::ExecOpError::Timeout, + exec_session::ExecEstablishError::OldGeneration => exec_session::ExecOpError::OldGeneration, + exec_session::ExecEstablishError::Capability => exec_session::ExecOpError::Capability, + exec_session::ExecEstablishError::Guest(inner) => exec_session::ExecOpError::Guest(inner), + } +} + +fn process_resource_execution_ref(request: &Value) -> Result<(String, ResourceRef), TypedError> { + let execution_ref = request + .get("executionRef") + .and_then(Value::as_str) + .ok_or_else(|| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + let execution_ref = + ResourceRef::parse(execution_ref).map_err(|_| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + let vm = execution_ref.name().as_str().to_owned(); + if execution_ref.resource_type().as_str() != "Guest" { + return Err(TypedError::RuntimeCapabilityUnsupported { + vm, + runtime_kind: "component-session".to_owned(), + capability: "process-attach".to_owned(), + verb: "exec".to_owned(), + }); + } + Ok((vm, execution_ref)) +} + +fn process_resource_execution_ref_from_resource( + state: &ServerState, + request: &Value, +) -> Result<(String, ResourceRef), TypedError> { + let zone = request + .get("zoneRef") + .and_then(Value::as_str) + .and_then(|value| value.strip_prefix("Zone/")) + .and_then(|value| ZoneId::parse(value.to_owned()).ok()) + .ok_or_else(|| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + let process_ref = request + .get("resourceRef") + .and_then(Value::as_str) + .and_then(|value| ResourceRef::parse(value).ok()) + .filter(|value| value.resource_type().as_str() == "EphemeralProcess") + .ok_or_else(|| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + let client = state + .resource_plane + .lock() + .ok() + .and_then(|plane| plane.clone()) + .and_then(|plane| plane.zone(&zone).ok()) + .and_then(|runtime| runtime.process_resource_client()) + .ok_or_else(|| TypedError::RuntimeCapabilityUnsupported { + vm: process_ref.name().as_str().to_owned(), + runtime_kind: "component-session".to_owned(), + capability: "process-attach".to_owned(), + verb: "exec".to_owned(), + })?; + let mut target = DaemonShellAuthority::shell_resource_identity(&zone, &process_ref); + target.resource_type = "EphemeralProcess".to_owned(); + let mut projection = resource_wire::Projection::new(); + projection.kind = + protobuf::EnumOrUnknown::new(resource_wire::ProjectionKind::PROJECTION_KIND_FULL); + let mut get = resource_wire::GetRequest::new(); + get.meta = protobuf::MessageField::some(DaemonShellAuthority::shell_resource_request_meta( + "process-resource-resolve-target", + )); + get.target = protobuf::MessageField::some(target); + get.projection = protobuf::MessageField::some(projection); + let response = block_on_future(client.get(get)); + let resource = response.resource.as_ref().ok_or_else(|| { + response + .error + .as_ref() + .map(|error| { + map_resource_exec_error(resource_error_to_op(error), process_ref.name().as_str()) + }) + .unwrap_or_else(|| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + }) + })?; + let envelope = d2b_contracts_resource::v3::ResourceEnvelope::from_json( + &resource.canonical_json, + ) + .map_err(|_| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + if envelope.resource_type().as_str() != "EphemeralProcess" { + return Err(TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + }); + } + let spec = envelope.spec().base().to_canonical_bytes(); + let spec: Value = + serde_json::from_slice(&spec).map_err(|_| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + let execution_ref = spec + .get("executionRef") + .and_then(Value::as_str) + .and_then(|value| ResourceRef::parse(value).ok()) + .filter(|value| matches!(value.resource_type().as_str(), "Host" | "Guest")) + .ok_or_else(|| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + Ok((execution_ref.name().as_str().to_owned(), execution_ref)) +} + +fn dispatch_resource_exec_request( + state: &ServerState, + peer: &PeerIdentity, + request: &Value, +) -> Result { + if !matches!(peer.role, PeerRole::Admin) { + return Err(TypedError::AuthzNotAdmin { + verb: "exec".to_owned(), + }); + } + let (vm, execution_ref) = if request.get("executionRef").is_some() { + process_resource_execution_ref(request)? + } else { + process_resource_execution_ref_from_resource(state, request)? + }; + let client = exec_detached::ResourceDetachedClient::new( + production_process_resource_port(state, request, &execution_ref), + execution_ref, + ) + .map_err(|error| map_resource_exec_error(error, &vm))?; + let process_ref = request + .get("resourceRef") + .and_then(Value::as_str) + .and_then(|value| ResourceRef::parse(value).ok()); + let method = request.get("method").and_then(Value::as_str); + let response = match method { + Some("List") => { + let result = block_on_future(client.list()) + .map_err(|error| map_resource_exec_error(error, &vm))?; + public_wire::ExecOpResponse::List(result) + } + + Some("Status") => { + let process_ref = process_ref.ok_or_else(|| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + let result = block_on_future(client.status(&process_ref)) + .map_err(|error| map_resource_exec_error(error, &vm))?; + public_wire::ExecOpResponse::Status(result) } - // Attached Exec::Start is intercepted in `handle_connection` (it takes - // over the connection as the owner connection and is handled on a - // spawned worker off the serial accept loop). Detached management ops - // are ordinary one-shot requests. - d2bd_runtime::wire::Request::Exec(op) => dispatch_exec_management(state, peer, op), - d2bd_runtime::wire::Request::Console(op) => dispatch_console(state, peer, op), - d2bd_runtime::wire::Request::GatewayDisplay(op) => { - dispatch_gateway_display(state, peer, op) + Some("Logs") => { + let process_ref = process_ref.ok_or_else(|| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + let stdout_offset = request.get("stdoutOffset").and_then(Value::as_u64); + let stderr_offset = request.get("stderrOffset").and_then(Value::as_u64); + let max_len = request.get("maxLen").and_then(Value::as_u64); + let result = + block_on_future(client.logs(&process_ref, stdout_offset, stderr_offset, max_len)) + .map_err(|error| map_resource_exec_error(error, &vm))?; + public_wire::ExecOpResponse::Logs(result) } - d2bd_runtime::wire::Request::Workload(op) => dispatch_workload(state, peer, op), - d2bd_runtime::wire::Request::Audio(op) => { - if !matches!(op, public_wire::AudioOp::Status(_)) - && !matches!(peer.role, PeerRole::Admin) - { - return Err(TypedError::AuthzNotAdmin { - verb: "audio".to_owned(), - }); - } - audio_dispatch::dispatch_audio(state, broker_caller_role_for_peer(peer), op) + Some("Kill") => { + let process_ref = process_ref.ok_or_else(|| TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + })?; + let result = block_on_future(client.kill(&process_ref)) + .map_err(|error| map_resource_exec_error(error, &vm))?; + public_wire::ExecOpResponse::Kill(result) } - d2bd_runtime::wire::Request::Resource(request) => { - dispatch_resource_request(state, peer, request) + _ => { + return Err(TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + }); } + }; + Ok(d2bd_runtime::wire::exec_response(&response)) +} + +fn dispatch_resource_exec_create_request( + state: &ServerState, + peer: &PeerIdentity, + request: &Value, +) -> Result { + if !matches!(peer.role, PeerRole::Admin | PeerRole::Launcher) { + return Err(TypedError::AuthzNotAdmin { + verb: "exec".to_owned(), + }); } + let (vm, execution_ref) = process_resource_execution_ref(request)?; + let spec = exec_session::ExecStartSpec { + vm: vm.clone(), + request_id: request + .get("opId") + .and_then(Value::as_u64) + .map(|value| value.to_string()) + .or_else(|| { + request + .get("requestId") + .and_then(Value::as_str) + .map(ToOwned::to_owned) + }), + argv: request + .get("argv") + .and_then(Value::as_array) + .map(|argv| { + argv.iter() + .filter_map(Value::as_str) + .map(ToOwned::to_owned) + .collect() + }) + .or_else(|| { + request + .get("command") + .and_then(Value::as_array) + .map(|argv| { + argv.iter() + .filter_map(Value::as_str) + .map(ToOwned::to_owned) + .collect() + }) + }) + .unwrap_or_default(), + tty: false, + detached: true, + env: request + .get("env") + .and_then(Value::as_array) + .map(|env| { + env.iter() + .filter_map(Value::as_str) + .filter_map(|value| value.split_once('=')) + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect() + }) + .unwrap_or_default(), + cwd: request + .get("cwd") + .and_then(Value::as_str) + .map(ToOwned::to_owned), + term_size: None, + }; + let client = exec_detached::ResourceDetachedClient::new( + production_process_resource_port(state, request, &execution_ref), + execution_ref, + ) + .map_err(|error| map_resource_exec_error(error, &vm))?; + let result = block_on_future(client.create(&spec)) + .map_err(|error| map_resource_exec_error(error, &vm))?; + Ok(d2bd_runtime::wire::exec_response( + &public_wire::ExecOpResponse::DetachedCreate(result), + )) } fn dispatch_resource_request( @@ -2743,6 +4729,12 @@ fn dispatch_resource_request( } return result; } + if process_resource_detached_create_request(&request.value()) { + return dispatch_resource_exec_create_request(state, peer, &request.value()); + } + if process_resource_management_request(&request.value()) { + return dispatch_resource_exec_request(state, peer, &request.value()); + } let typed_shell = typed_shell_resource_request(&request.value()); let runtime = match if typed_shell { resolve_typed_shell_runtime(state, &request.value()) @@ -4307,6 +6299,68 @@ fn typed_shell_resource_error_frame(error: &TypedError) -> Value { }) } +fn write_named_process_stream_error( + stream: &Socket, + request_id: u64, + error: TypedError, +) -> Result<(), TypedError> { + use d2bd_runtime::typed_error::{ + GuestControlShellErrorKind as Guest, UnsafeLocalShellErrorKind as UnsafeHost, + }; + let kind = match &error { + TypedError::AuthzNotAdmin { .. } => public_wire::NamedProcessStreamErrorKind::Authorization, + TypedError::GuestControlShellFailed { + kind: Guest::StaleSession, + } + | TypedError::UnsafeLocalShellFailed { + kind: UnsafeHost::StaleSession, + } => public_wire::NamedProcessStreamErrorKind::StaleSession, + TypedError::GuestControlShellFailed { + kind: Guest::NotFound, + } + | TypedError::UnsafeLocalShellFailed { + kind: UnsafeHost::NotFound, + } => public_wire::NamedProcessStreamErrorKind::NotFound, + TypedError::GuestControlShellFailed { + kind: Guest::Capacity, + } + | TypedError::UnsafeLocalShellFailed { + kind: UnsafeHost::QueueFull, + } => public_wire::NamedProcessStreamErrorKind::Backpressure, + TypedError::GuestControlShellFailed { + kind: Guest::Timeout, + } + | TypedError::UnsafeLocalShellFailed { + kind: UnsafeHost::Timeout, + } => public_wire::NamedProcessStreamErrorKind::Timeout, + TypedError::GuestControlShellFailed { + kind: Guest::Transport, + } + | TypedError::UnsafeLocalShellFailed { + kind: + UnsafeHost::HelperUnavailable + | UnsafeHost::HelperStale + | UnsafeHost::UserManagerUnavailable + | UnsafeHost::EnvironmentInvalid + | UnsafeHost::ExecutableUnavailable + | UnsafeHost::ScopeCreateFailed + | UnsafeHost::ScopeIdentityMismatch + | UnsafeHost::GraphicalSessionInactive + | UnsafeHost::WaylandUnavailable + | UnsafeHost::ProxyUnavailable + | UnsafeHost::FirstClientTimeout, + } => public_wire::NamedProcessStreamErrorKind::Disconnected, + _ => public_wire::NamedProcessStreamErrorKind::Protocol, + }; + let response = public_wire::NamedProcessStreamResponseFrame::new( + request_id, + public_wire::NamedProcessStreamResponse::Error(public_wire::NamedProcessStreamError { + kind, + }), + ); + write_json_frame(stream, &response) +} + fn dispatch_workload( state: &ServerState, peer: &PeerIdentity, @@ -4654,15 +6708,6 @@ fn dispatch_local_vm_launcher( resolved: &workload_dispatch::ResolvedExec, ) -> Result<(public_wire::LauncherExecDisposition, Option), TypedError> { ensure_vm_runtime_capability(state, vm, RuntimeCapabilityGate::Exec, "launch")?; - let request = public_wire::ExecStartArgs { - vm: vm.to_owned(), - argv: resolved.argv.as_slice().to_vec(), - tty: false, - detached: true, - env: None, - cwd: None, - term_size: None, - }; let mut fingerprint = Sha256::new(); fingerprint.update(requester_uid.to_le_bytes()); fingerprint.update(operation_id.as_str().as_bytes()); @@ -4672,12 +6717,35 @@ fn dispatch_local_vm_launcher( "workload-launch:{}", hex_bytes(&fingerprint.finalize()[..16]) ); - let result = exec_detached::create_idempotent_as( - state, - &request, - request_id, - BrokerCallerRole::LauncherUid { uid: requester_uid }, - )?; + let spec = exec_session::ExecStartSpec { + vm: vm.to_owned(), + request_id: Some(request_id.clone()), + argv: resolved.argv.as_slice().to_vec(), + tty: false, + detached: true, + env: Vec::new(), + cwd: None, + term_size: None, + }; + let execution_ref = ResourceRef::parse(&format!("Guest/{vm}")).map_err(|_| { + TypedError::RuntimeCapabilityUnsupported { + vm: vm.to_owned(), + runtime_kind: "component-session".to_owned(), + capability: "process-create".to_owned(), + verb: "launch".to_owned(), + } + })?; + let resource_request = serde_json::json!({ + "zoneRef": "Zone/local-root", + "requestId": request_id, + }); + let client = exec_detached::ResourceDetachedClient::new( + production_process_resource_port(state, &resource_request, &execution_ref), + execution_ref, + ) + .map_err(|error| map_resource_exec_error(error, vm))?; + let result = block_on_future(client.create(&spec)) + .map_err(|error| map_resource_exec_error(error, vm))?; emit_detached_create_audit(state, requester_uid, vm, &result.exec_id); Ok(( public_wire::LauncherExecDisposition::Committed, @@ -6391,6 +8459,7 @@ impl IdSource for DaemonGatewayIds { } } +#[cfg(test)] fn dispatch_exec_management( state: &ServerState, peer: &PeerIdentity, @@ -6429,6 +8498,7 @@ fn dispatch_exec_management( Ok(d2bd_runtime::wire::exec_response(&response)) } +#[cfg(test)] fn exec_op_vm(op: &public_wire::ExecOp) -> Option<&str> { match op { public_wire::ExecOp::Start(args) => Some(args.vm.as_str()), @@ -6931,14 +9001,16 @@ fn usbip_lifecycle_claim_for_intent( .vms .get(vm) .and_then(|entry| entry.usbipd_host_ip.clone())?; - Some(d2b_provider_device_usbip::reconcile_state::UsbipLifecycleClaim { - vm: vm.to_owned(), - env: intent.env.clone(), - bus_id: intent.bus_id.clone(), - host, - claim_ref: intent.intent_id.clone(), - required: true, - }) + Some( + d2b_provider_device_usbip::reconcile_state::UsbipLifecycleClaim { + vm: vm.to_owned(), + env: intent.env.clone(), + bus_id: intent.bus_id.clone(), + host, + claim_ref: intent.intent_id.clone(), + required: true, + }, + ) } fn read_usbip_claim_lock_owner_path(lock_path: &Path) -> io::Result { @@ -7352,9 +9424,9 @@ fn usbip_lifecycle_report_summary( .claims .iter() .filter(|claim| { - claim - .completed - .contains(&d2b_provider_device_usbip::reconcile_state::UsbipLifecycleStep::GuestImport) + claim.completed.contains( + &d2b_provider_device_usbip::reconcile_state::UsbipLifecycleStep::GuestImport, + ) }) .count(); let preserved = report @@ -7402,9 +9474,9 @@ fn new_usbip_reconcile_attempt( let correlation_id = d2b_provider_device_usbip::reconcile_state::UsbipReconcileCorrelationId::new(&bounded) .or_else(|| { - d2b_provider_device_usbip::reconcile_state::UsbipReconcileCorrelationId::new(format!( - "usb-{phase}" - )) + d2b_provider_device_usbip::reconcile_state::UsbipReconcileCorrelationId::new( + format!("usb-{phase}"), + ) }) .expect("static USBIP reconcile correlation id is valid"); d2b_provider_device_usbip::reconcile_state::UsbipReconcileAttemptContext { correlation_id } @@ -9181,6 +11253,280 @@ enum ShellManagementOp { }, } +fn daemon_shell_authority(state: &ServerState) -> Arc { + #[cfg(not(test))] + { + Arc::clone(&state.shell_authority) + } + #[cfg(test)] + { + Arc::new(DaemonShellAuthority::new(Arc::clone(&state.resource_plane))) + } +} + +fn shell_resource_client( + state: &ServerState, +) -> Result<(ZoneId, Arc), TypedError> { + let zone = ZoneId::parse("local-root").map_err(|_| shell_protocol_failed())?; + let authority = daemon_shell_authority(state); + let client = authority + .client_for_zone(&zone) + .map_err(|_| shell_capability_failed())?; + Ok((zone, client)) +} + +fn require_guest_component_session( + state: &ServerState, + vm: &str, +) -> Result { + production_component_session_driver(state, vm).ok_or_else(|| { + TypedError::RuntimeCapabilityUnsupported { + vm: vm.to_owned(), + runtime_kind: "component-session".to_owned(), + capability: "persistent-shell".to_owned(), + verb: "shell".to_owned(), + } + }) +} + +fn shell_resource_get( + client: &DaemonResourceApiClient, + zone: &ZoneId, + name: &public_wire::ShellName, +) -> Result, TypedError> { + let resource_ref = ResourceRef::parse(&format!( + "shell-terminal.d2bus.org.ShellSession/{}", + name.as_str() + )) + .map_err(|_| shell_protocol_failed())?; + let mut request = resource_wire::GetRequest::new(); + request.meta = protobuf::MessageField::some(DaemonShellAuthority::shell_resource_request_meta( + "shell-session-get", + )); + request.target = protobuf::MessageField::some(DaemonShellAuthority::shell_resource_identity( + zone, + &resource_ref, + )); + let mut projection = resource_wire::Projection::new(); + projection.kind = + protobuf::EnumOrUnknown::new(resource_wire::ProjectionKind::PROJECTION_KIND_FULL); + request.projection = protobuf::MessageField::some(projection); + let response = block_on_future(client.get(request)); + let Some(resource) = response.resource.as_ref() else { + return match response + .error + .as_ref() + .and_then(|error| error.kind.enum_value().ok()) + { + Some(resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_NOT_FOUND) => { + Ok(None) + } + Some(_) | None => Err(response + .error + .as_ref() + .map(shell_resource_api_error) + .unwrap_or_else(shell_protocol_failed)), + }; + }; + d2b_contracts_resource::v3::ResourceEnvelope::from_json(&resource.canonical_json) + .map(Some) + .map_err(|_| shell_protocol_failed()) +} + +fn shell_resource_api_error(error: &resource_wire::ResourceError) -> TypedError { + match error.kind.enum_value().ok() { + Some(resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_AUTHORIZATION_DENIED) => { + TypedError::AuthzNotAdmin { + verb: "shell".to_owned(), + } + } + Some(resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_TIMEOUT) => { + shell_failed(d2bd_runtime::typed_error::GuestControlShellErrorKind::Timeout) + } + Some( + resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_PROVIDER_UNAVAILABLE + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_PLANE_UNAVAILABLE, + ) => shell_transport_failed(), + Some(resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_BACKPRESSURE) => { + shell_failed(d2bd_runtime::typed_error::GuestControlShellErrorKind::Capacity) + } + Some( + resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_NOT_FOUND + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_ALREADY_EXISTS + | resource_wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_CONFLICT, + ) => shell_failed(d2bd_runtime::typed_error::GuestControlShellErrorKind::NotFound), + _ => shell_protocol_failed(), + } +} + +fn map_shell_authority_error(error: ShellTerminalError) -> TypedError { + match error { + ShellTerminalError::CapacityExceeded => { + shell_failed(d2bd_runtime::typed_error::GuestControlShellErrorKind::Capacity) + } + ShellTerminalError::StaleSessionGeneration + | ShellTerminalError::CapabilityReused + | ShellTerminalError::CapabilitySessionMismatch + | ShellTerminalError::AttachmentUnknown => { + shell_failed(d2bd_runtime::typed_error::GuestControlShellErrorKind::StaleSession) + } + ShellTerminalError::NotAuthorized => TypedError::AuthzNotAdmin { + verb: "shell".to_owned(), + }, + ShellTerminalError::WrongZone + | ShellTerminalError::InvalidName + | ShellTerminalError::CapacityOutOfRange + | ShellTerminalError::InvalidLoginShell + | ShellTerminalError::RelayHostUserDomainDenied + | ShellTerminalError::WorkloadIdentityMismatch + | ShellTerminalError::GuestUserDomainUnsupported + | ShellTerminalError::SupervisorAmbiguous => shell_protocol_failed(), + } +} + +fn shell_session_entry( + bytes: &[u8], + default_name: &public_wire::ShellName, +) -> Result { + let value: Value = serde_json::from_slice(bytes).map_err(|_| shell_protocol_failed())?; + let name = value + .pointer("/metadata/name") + .and_then(Value::as_str) + .and_then(|name| public_wire::ShellName::new(name).ok()) + .ok_or_else(shell_protocol_failed)?; + let attached = value + .pointer("/status/resource/attached") + .and_then(Value::as_bool) + .or_else(|| value.pointer("/status/attached").and_then(Value::as_bool)) + .unwrap_or(false); + let state = match value.pointer("/status/phase").and_then(Value::as_str) { + Some("Failed") | Some("Degraded") => public_wire::ShellSessionState::FeatureDisabled, + Some("Deleted") => public_wire::ShellSessionState::Killed, + _ if attached => public_wire::ShellSessionState::Attached, + _ => public_wire::ShellSessionState::Detached, + }; + Ok(public_wire::ShellListEntry { + is_default: name == *default_name, + name, + state, + attached, + }) +} + +fn production_guest_shell_list( + state: &ServerState, + vm: &str, +) -> Result { + let _driver = require_guest_component_session(state, vm)?; + let (zone, client) = shell_resource_client(state)?; + let target = ResourceRef::parse(&format!("Guest/{vm}")).map_err(|_| shell_protocol_failed())?; + let default_name = public_wire::ShellName::new("primary").expect("valid default shell name"); + let mut request = resource_wire::ListRequest::new(); + request.meta = protobuf::MessageField::some(DaemonShellAuthority::shell_resource_request_meta( + "shell-session-list", + )); + request + .resource_types + .push("shell-terminal.d2bus.org.ShellSession".to_owned()); + let mut filter = resource_wire::ListFilter::new(); + filter.field = "spec.executionRef".to_owned(); + filter.values.push(target.to_canonical_string()); + request.filters.push(filter); + request.page_size = 256; + let mut projection = resource_wire::Projection::new(); + projection.kind = + protobuf::EnumOrUnknown::new(resource_wire::ProjectionKind::PROJECTION_KIND_FULL); + request.projection = protobuf::MessageField::some(projection); + let response = block_on_future(client.list(request)); + if let Some(error) = response.error.as_ref() { + return Err(shell_resource_api_error(error)); + } + let sessions = response + .resources + .iter() + .map(|resource| shell_session_entry(&resource.canonical_json, &default_name)) + .collect::, _>>()?; + let _ = zone; + Ok(public_wire::ShellListResult { + default_name, + sessions, + }) +} + +fn production_guest_shell_detach( + state: &ServerState, + vm: &str, + name: Option, +) -> Result { + let _driver = require_guest_component_session(state, vm)?; + let (zone, client) = shell_resource_client(state)?; + let name = name.unwrap_or_else(|| public_wire::ShellName::new("primary").expect("default")); + let Some(envelope) = shell_resource_get(&client, &zone, &name)? else { + return Err(TypedError::WorkloadTargetNotFound { + target: format!("shell-terminal.d2bus.org.ShellSession/{}", name.as_str()), + }); + }; + let _ = envelope; + Ok(public_wire::ShellDetachResult { + resolved_name: name, + detached: true, + cause: Some(public_wire::ShellCloseCause::ClientDetach), + }) +} + +fn production_guest_shell_kill( + state: &ServerState, + vm: &str, + name: public_wire::ShellName, +) -> Result { + let _driver = require_guest_component_session(state, vm)?; + let (zone, client) = shell_resource_client(state)?; + let Some(envelope) = shell_resource_get(&client, &zone, &name)? else { + return Err(TypedError::WorkloadTargetNotFound { + target: format!("shell-terminal.d2bus.org.ShellSession/{}", name.as_str()), + }); + }; + let session = shell_session_from_resource(&envelope)?; + daemon_shell_authority(state) + .remove_supervisor_process_resource(&session) + .map_err(map_shell_authority_error)?; + let resource_ref = ResourceRef::parse(&format!( + "shell-terminal.d2bus.org.ShellSession/{}", + name.as_str() + )) + .map_err(|_| shell_protocol_failed())?; + let mut mutation = resource_wire::Mutation::new(); + mutation.kind = protobuf::EnumOrUnknown::new(resource_wire::MutationKind::MUTATION_KIND_DELETE); + mutation.target = + protobuf::MessageField::some(DaemonShellAuthority::shell_resource_identity_with_revision( + &zone, + &resource_ref, + envelope.metadata().uid(), + envelope.metadata().generation().get(), + envelope.metadata().revision().get(), + )); + let mut precondition = resource_wire::Precondition::new(); + precondition.kind = protobuf::EnumOrUnknown::new( + resource_wire::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION, + ); + precondition.expected_revision = Some(envelope.metadata().revision().get()); + mutation.precondition = protobuf::MessageField::some(precondition); + let mut request = resource_wire::DeleteRequest::new(); + request.meta = protobuf::MessageField::some(DaemonShellAuthority::shell_resource_request_meta( + "shell-session-kill", + )); + request.mutation = protobuf::MessageField::some(mutation); + let response = block_on_future(client.delete(request)); + if let Some(error) = response.error.as_ref() { + return Err(shell_resource_api_error(error)); + } + Ok(public_wire::ShellKillResult { + name, + killed: true, + state: public_wire::ShellSessionState::Killed, + }) +} + fn dispatch_shell_management( state: &ServerState, peer: &PeerIdentity, @@ -9203,28 +11549,7 @@ fn dispatch_shell_management( })?; let (result, provider, target, operation_digest) = match resolved.route.clone() { WorkloadRoute::LocalVm { vm } => { - let result = run_guest_shell_management( - state, - &vm, - broker_caller_role_for_peer(peer), - |client, metadata| { - let mut request = pb::ShellListRequest::new(); - request.metadata = protobuf::MessageField::some(metadata); - async move { - let response: pb::ShellListResponse = client - .unary_with_timeout( - "ShellList", - request, - SHELL_MANAGEMENT_TIMEOUT, - ) - .await - .map_err(map_shell_health_error)?; - shell_error_to_typed(response.error.as_ref())?; - map_shell_list_response(response) - } - }, - ) - .inspect_err(|error| { + let result = production_guest_shell_list(state, &vm).inspect_err(|error| { record_shell_dispatch_failure( state, peer.uid, @@ -9322,39 +11647,18 @@ fn dispatch_shell_management( })?; let (result, provider, target, operation_digest) = match resolved.route.clone() { WorkloadRoute::LocalVm { vm } => { - let result = run_guest_shell_management( - state, - &vm, - broker_caller_role_for_peer(peer), - |client, metadata| { - let mut request = pb::ShellDetachRequest::new(); - request.metadata = protobuf::MessageField::some(metadata); - request.name = name.map(|name| name.as_str().to_owned()); - async move { - let response: pb::ShellDetachResponse = client - .unary_with_timeout( - "ShellDetach", - request, - SHELL_MANAGEMENT_TIMEOUT, - ) - .await - .map_err(map_shell_health_error)?; - shell_error_to_typed(response.error.as_ref())?; - map_shell_detach_response(response) - } - }, - ) - .inspect_err(|error| { - record_shell_dispatch_failure( - state, - peer.uid, - &vm, - d2bd_runtime::shell_backend::ShellProvider::GuestControl, - "detach", - None, - error, - ); - })?; + let result = production_guest_shell_detach(state, &vm, name.clone()) + .inspect_err(|error| { + record_shell_dispatch_failure( + state, + peer.uid, + &vm, + d2bd_runtime::shell_backend::ShellProvider::GuestControl, + "detach", + None, + error, + ); + })?; ( result, d2bd_runtime::shell_backend::ShellProvider::GuestControl, @@ -9439,40 +11743,18 @@ fn dispatch_shell_management( })?; let (result, provider, target, operation_digest) = match resolved.route.clone() { WorkloadRoute::LocalVm { vm } => { - let guest_name = requested_name.clone(); - let result = run_guest_shell_management( - state, - &vm, - broker_caller_role_for_peer(peer), - |client, metadata| { - let mut request = pb::ShellKillRequest::new(); - request.metadata = protobuf::MessageField::some(metadata); - request.name = guest_name.as_str().to_owned(); - async move { - let response: pb::ShellKillResponse = client - .unary_with_timeout( - "ShellKill", - request, - SHELL_MANAGEMENT_TIMEOUT, - ) - .await - .map_err(map_shell_health_error)?; - shell_error_to_typed(response.error.as_ref())?; - map_shell_kill_response(response) - } - }, - ) - .inspect_err(|error| { - record_shell_dispatch_failure( - state, - peer.uid, - &vm, - d2bd_runtime::shell_backend::ShellProvider::GuestControl, - "kill", - None, - error, - ); - })?; + let result = production_guest_shell_kill(state, &vm, requested_name.clone()) + .inspect_err(|error| { + record_shell_dispatch_failure( + state, + peer.uid, + &vm, + d2bd_runtime::shell_backend::ShellProvider::GuestControl, + "kill", + None, + error, + ); + })?; ( result, d2bd_runtime::shell_backend::ShellProvider::GuestControl, @@ -9845,6 +12127,121 @@ fn typed_shell_execution_target(value: &str) -> Result { } } +fn guest_shell_session( + state: &ServerState, + vm: &str, + name: &public_wire::ShellName, +) -> Result<(ShellPool, ShellSession), TypedError> { + let resolver = load_bundle_resolver(state)?; + let entry = + resolver + .manifest + .vms + .get(vm) + .ok_or_else(|| TypedError::WorkloadTargetNotFound { + target: vm.to_owned(), + })?; + let shell = entry + .shell + .as_ref() + .filter(|shell| shell.enabled) + .ok_or_else(shell_capability_failed)?; + let workload_user = entry + .ssh_user + .as_deref() + .ok_or_else(shell_capability_failed)?; + let pool_name = format!("{vm}-shell"); + let pool = ShellPool::new( + pool_name, + "local-root", + PoolSpec::new( + ExecutionTarget::guest(vm.to_owned()), + workload_user, + "artifact://shells/bash-login", + shell.max_sessions, + shell.max_attached, + DEFAULT_OUTPUT_RING_CAPACITY, + ) + .map_err(|_| shell_protocol_failed())?, + ) + .map_err(|_| shell_protocol_failed())?; + let session = + ShellSession::from_pool_with_resource_name(&pool, name.as_str(), name.as_str(), None) + .map_err(|_| shell_protocol_failed())?; + Ok((pool, session)) +} + +fn shell_session_from_resource( + envelope: &d2b_contracts_resource::v3::ResourceEnvelope, +) -> Result { + if envelope.resource_type().as_str() != "shell-terminal.d2bus.org.ShellSession" { + return Err(shell_protocol_failed()); + } + let spec = serde_json::from_slice::(&envelope.spec().base().to_canonical_bytes()) + .map_err(|_| shell_protocol_failed())?; + let execution_ref = spec + .get("executionRef") + .and_then(Value::as_str) + .and_then(|value| ResourceRef::parse(value).ok()) + .filter(|value| matches!(value.resource_type().as_str(), "Host" | "Guest")) + .ok_or_else(shell_protocol_failed)?; + let user_ref = spec + .get("userRef") + .and_then(Value::as_str) + .and_then(|value| ResourceRef::parse(value).ok()) + .filter(|value| value.resource_type().as_str() == "User") + .ok_or_else(shell_protocol_failed)?; + let pool_name = spec + .get("poolRef") + .and_then(Value::as_str) + .and_then(|value| ResourceRef::parse(value).ok()) + .filter(|value| value.resource_type().as_str() == "shell-terminal.d2bus.org.ShellPool") + .map(|value| value.name().as_str().to_owned()) + .unwrap_or_else(|| "shell".to_owned()); + let login_shell_ref = spec + .get("loginShellRef") + .and_then(Value::as_str) + .ok_or_else(shell_protocol_failed)?; + let target = if execution_ref.resource_type().as_str() == "Host" { + ExecutionTarget::host(execution_ref.name().as_str().to_owned()) + } else { + ExecutionTarget::guest(execution_ref.name().as_str().to_owned()) + }; + let pool = ShellPool::new( + pool_name, + envelope.metadata().zone().as_str(), + PoolSpec::new( + target, + user_ref.name().as_str().to_owned(), + login_shell_ref.to_owned(), + spec.get("maxSessions") + .and_then(Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .unwrap_or(8), + spec.get("maxAttached") + .and_then(Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .unwrap_or(1), + spec.get("outputRingCapacity") + .and_then(Value::as_u64) + .unwrap_or(DEFAULT_OUTPUT_RING_CAPACITY), + ) + .map_err(|_| shell_protocol_failed())?, + ) + .map_err(|_| shell_protocol_failed())?; + let session_name = spec + .get("sessionName") + .and_then(Value::as_str) + .unwrap_or(envelope.metadata().name().as_str()); + ShellSession::from_pool_with_resource_name( + &pool, + envelope.metadata().name().as_str(), + session_name, + spec.get("outputRingCapacity").and_then(Value::as_u64), + ) + .map_err(|_| shell_protocol_failed()) +} + fn configured_shell_targets(state: &ServerState) -> Result, TypedError> { let resolver = load_bundle_resolver(state)?; let mut targets = std::collections::BTreeSet::new(); @@ -10108,6 +12505,317 @@ fn spawn_typed_shell_owner( }) } +fn process_stream_response( + runtime: &tokio::runtime::Handle, + client: &dyn exec_session::ExecGuestClient, + request: public_wire::NamedProcessStreamRequest, + control_sequence: &mut u64, +) -> Result<(public_wire::NamedProcessStreamResponse, bool), exec_session::ExecOpError> { + use d2bd_runtime::terminal_session::OutputStreamSel; + match request { + public_wire::NamedProcessStreamRequest::Stdin { + offset, + chunk_base64, + eof, + } => { + let data = d2b_core::base64_codec::decode(&chunk_base64) + .map_err(|_| exec_session::ExecOpError::Protocol)?; + let result = runtime.block_on(client.write_stdin( + offset, + data, + eof, + SHELL_MANAGEMENT_TIMEOUT, + ))?; + Ok(( + public_wire::NamedProcessStreamResponse::Stdin(public_wire::ExecWriteStdinResult { + accepted_len: result.accepted_len, + next_offset: result.next_offset, + backpressured: result.backpressured, + stdin_closed: result.stdin_closed, + }), + false, + )) + } + public_wire::NamedProcessStreamRequest::Read { + stream, + offset, + max_len, + wait, + timeout_ms, + } => { + let result = runtime.block_on(client.read_output( + match stream { + public_wire::ExecStream::Stdout => OutputStreamSel::Stdout, + public_wire::ExecStream::Stderr => OutputStreamSel::Stderr, + }, + offset, + max_len, + wait, + timeout_ms, + SHELL_MANAGEMENT_TIMEOUT, + ))?; + Ok(( + public_wire::NamedProcessStreamResponse::Output( + public_wire::ExecReadOutputResult { + data_base64: d2b_core::base64_codec::encode(&result.data), + next_offset: result.next_offset, + eof: result.eof, + dropped_bytes: result.dropped_bytes, + truncated: result.truncated, + timed_out: result.timed_out, + }, + ), + false, + )) + } + public_wire::NamedProcessStreamRequest::Signal { control_seq, signo } => { + if control_seq == 0 || control_seq <= *control_sequence { + return Err(exec_session::ExecOpError::OldGeneration); + } + runtime.block_on(client.signal(control_seq, signo, SHELL_MANAGEMENT_TIMEOUT))?; + *control_sequence = control_seq; + Ok(( + public_wire::NamedProcessStreamResponse::Delivered( + public_wire::ExecControlResult { delivered: true }, + ), + false, + )) + } + public_wire::NamedProcessStreamRequest::Resize { + control_seq, + rows, + cols, + } => { + if control_seq == 0 || control_seq <= *control_sequence { + return Err(exec_session::ExecOpError::OldGeneration); + } + runtime.block_on(client.resize(control_seq, rows, cols, SHELL_MANAGEMENT_TIMEOUT))?; + *control_sequence = control_seq; + Ok(( + public_wire::NamedProcessStreamResponse::Delivered( + public_wire::ExecControlResult { delivered: true }, + ), + false, + )) + } + public_wire::NamedProcessStreamRequest::CloseStdin { offset } => { + runtime.block_on(client.close_stdin(offset, SHELL_MANAGEMENT_TIMEOUT))?; + Ok(( + public_wire::NamedProcessStreamResponse::Closed(public_wire::ExecCloseResult { + stdin_closed: true, + }), + false, + )) + } + public_wire::NamedProcessStreamRequest::Wait { timeout_ms } => { + let result = runtime.block_on(client.wait(timeout_ms, SHELL_MANAGEMENT_TIMEOUT))?; + let terminal_status = result.terminal.map(|terminal| match terminal { + d2bd_runtime::terminal_session::TerminalKind::Exited(code) => { + public_wire::ExecTerminalStatus::Exited { code } + } + d2bd_runtime::terminal_session::TerminalKind::Signaled(signal) => { + public_wire::ExecTerminalStatus::Signaled { signal } + } + d2bd_runtime::terminal_session::TerminalKind::Error(slug) => { + public_wire::ExecTerminalStatus::Error { + slug: slug.to_owned(), + } + } + }); + Ok(( + public_wire::NamedProcessStreamResponse::Wait(public_wire::ExecWaitResult { + running: result.running, + terminal_status, + }), + false, + )) + } + public_wire::NamedProcessStreamRequest::Cancel => { + runtime.block_on(client.cancel(0, SHELL_MANAGEMENT_TIMEOUT))?; + Ok(( + public_wire::NamedProcessStreamResponse::Closed(public_wire::ExecCloseResult { + stdin_closed: true, + }), + true, + )) + } + public_wire::NamedProcessStreamRequest::Close => { + runtime.block_on(client.cancel(0, SHELL_MANAGEMENT_TIMEOUT))?; + Ok(( + public_wire::NamedProcessStreamResponse::Closed(public_wire::ExecCloseResult { + stdin_closed: true, + }), + true, + )) + } + } +} + +fn run_process_resource_owner( + stream: Socket, + state: ServerState, + peer: PeerIdentity, + request: Value, + _conn_permit: Option, +) { + #[cfg(test)] + { + if let Some(hook) = owner_connection_test_hook::active() { + hook(); + drop(stream); + return; + } + } + let (vm, execution_ref) = match if request.get("executionRef").is_some() { + process_resource_execution_ref(&request) + } else { + process_resource_execution_ref_from_resource(&state, &request) + } { + Ok(value) => value, + Err(error) => { + let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); + return; + } + }; + let _slot = match state.exec_sessions.reserve(peer.uid, &vm) { + Ok(slot) => slot, + Err(error) => { + let error = map_exec_reserve_error(error); + let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); + return; + } + }; + let spec = exec_session::ExecStartSpec { + vm: vm.clone(), + request_id: request + .get("opId") + .and_then(Value::as_u64) + .map(|value| value.to_string()), + argv: request + .get("argv") + .and_then(Value::as_array) + .map(|argv| { + argv.iter() + .filter_map(Value::as_str) + .map(ToOwned::to_owned) + .collect() + }) + .unwrap_or_default(), + tty: request.get("tty").and_then(Value::as_bool).unwrap_or(false), + detached: request + .get("detached") + .and_then(Value::as_bool) + .unwrap_or(false), + env: Vec::new(), + cwd: request + .get("cwd") + .and_then(Value::as_str) + .map(ToOwned::to_owned), + term_size: request + .get("initialSize") + .and_then(Value::as_object) + .and_then(|size| { + size.get("rows") + .and_then(Value::as_u64) + .zip(size.get("cols").and_then(Value::as_u64)) + .map(|(rows, cols)| (rows as u32, cols as u32)) + }), + }; + let resource_ref = process_resource_ref(&request); + let Some(resource_ref) = resource_ref else { + let error = TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Protocol, + }; + let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); + return; + }; + let connector = match exec_session_real::ResourceExecConnector::new( + production_process_resource_port(&state, &request, &execution_ref), + execution_ref, + ) { + Ok(connector) => connector, + Err(error) => { + let error = map_resource_exec_error(establish_error_as_exec_error(error), &vm); + let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); + return; + } + }; + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(_) => { + let error = TypedError::GuestControlExecFailed { + kind: d2bd_runtime::typed_error::GuestControlExecErrorKind::Internal, + }; + let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); + return; + } + }; + let established = match rt.block_on(connector.establish(&spec)) { + Ok(established) => established, + Err(error) => { + let error = map_resource_exec_error(establish_error_as_exec_error(error), &vm); + let _ = write_json_frame(&stream, &d2bd_runtime::wire::error_frame(&error)); + return; + } + }; + let initial = serde_json::json!({ + "attached": true, + "resourceRef": resource_ref.to_canonical_string(), + "tty": spec.tty, + }); + if write_json_frame(&stream, &initial).is_err() { + let _ = rt.block_on(established.client.cancel( + established.control_seq.saturating_add(1), + SHELL_MANAGEMENT_TIMEOUT, + )); + return; + } + let mut control_sequence = established.control_seq; + let client = established.client; + while let Ok(frame) = read_frame(&stream) { + let request: public_wire::NamedProcessStreamRequestFrame = + match serde_json::from_slice(&frame) { + Ok(request) => request, + Err(_) => { + let _ = write_named_process_stream_error(&stream, 0, shell_protocol_failed()); + break; + } + }; + let request_id = request.request_id; + let result = process_stream_response( + rt.handle(), + client.as_ref(), + request.request, + &mut control_sequence, + ); + let (response, terminal) = match result { + Ok(result) => result, + Err(error) => { + let typed = map_resource_exec_error(error, &vm); + let _ = write_named_process_stream_error(&stream, request_id, typed); + continue; + } + }; + let frame = public_wire::NamedProcessStreamResponseFrame::new(request_id, response); + if write_json_frame( + &stream, + &serde_json::to_value(frame).unwrap_or_else(|_| serde_json::json!({})), + ) + .is_err() + { + break; + } + if terminal { + break; + } + } + let _ = + rt.block_on(client.cancel(control_sequence.saturating_add(1), SHELL_MANAGEMENT_TIMEOUT)); +} + fn run_typed_shell_owner( stream: Socket, state: ServerState, @@ -10232,11 +12940,52 @@ fn run_typed_shell_owner( .unwrap_or(false), initial_terminal_size: size, }; + if matches!( + resolve_shell_target(&state, &target) + .map(|resolved| resolved.route) + .as_ref(), + Ok(workload_dispatch::WorkloadRoute::LocalVm { .. }) + ) { + let (pool, session) = match guest_shell_session(&state, &target, &name) { + Ok(value) => value, + Err(error) => { + let _ = write_json_frame(&stream, &typed_shell_resource_error_frame(&error)); + return; + } + }; + let authority = daemon_shell_authority(&state); + let session_created = match authority.ensure_shell_resources(&pool, &session) { + Ok(created) => created, + Err(error) => { + let error = map_shell_authority_error(error); + let _ = write_json_frame(&stream, &typed_shell_resource_error_frame(&error)); + return; + } + }; + if creating && session_created { + if let Err(error) = authority.restore_pool(&pool, 0) { + let error = map_shell_authority_error(error); + let _ = write_json_frame(&stream, &typed_shell_resource_error_frame(&error)); + return; + } + if let Err(error) = authority.open_session(&session) { + let error = map_shell_authority_error(error); + let _ = write_json_frame(&stream, &typed_shell_resource_error_frame(&error)); + return; + } + } + if let Err(error) = authority.ensure_supervisor_process_resource(&session) { + let error = map_shell_authority_error(error); + let _ = write_json_frame(&stream, &typed_shell_resource_error_frame(&error)); + return; + } + } let established = match rt.block_on(establish_shell_backend( &state, peer.uid, broker_caller_role_for_peer(&peer), &attach, + production_component_session_driver(&state, &target), )) { Ok(value) => value, Err(error) => { @@ -10327,7 +13076,7 @@ fn run_typed_shell_owner( .is_err() { let mut control_sequence = established.initial_control_sequence; - d2bd_runtime::shell_backend::best_effort_close( + d2bd_runtime::shell_backend::best_effort_cancel( established.backend.as_ref(), rt.handle(), &mut control_sequence, @@ -10361,96 +13110,190 @@ fn run_typed_shell_owner( let mut control_sequence = established.initial_control_sequence; let mut close_result = None; while let Ok(frame) = read_frame(&stream) { - let value: Value = match serde_json::from_slice(&frame) { - Ok(value) => value, - Err(_) => break, - }; - let kind = value.get("type").and_then(Value::as_str).unwrap_or(""); - if matches!(kind, "namedStreamClose" | "namedStreamCancel") { + let request: public_wire::NamedProcessStreamRequestFrame = + match serde_json::from_slice(&frame) { + Ok(request) => request, + Err(_) => { + let _ = write_named_process_stream_error( + &stream, + 0, + d2bd_runtime::shell_backend::shell_protocol_failed(), + ); + break; + } + }; + let request_id = request.request_id; + let close_stdin = matches!( + &request.request, + public_wire::NamedProcessStreamRequest::CloseStdin { .. } + ); + let close = matches!( + &request.request, + public_wire::NamedProcessStreamRequest::Close + | public_wire::NamedProcessStreamRequest::Cancel + ); + if close { let result = established .backend .close_attachment(rt.handle(), &mut control_sequence); match result { Ok(_) => { close_result = Some(d2bd_runtime::daemon_audit::ShellAuditResult::Closed); - let _ = write_json_frame(&stream, &json!({"closed": true})); + let response = public_wire::NamedProcessStreamResponseFrame::new( + request_id, + public_wire::NamedProcessStreamResponse::Closed( + public_wire::ExecCloseResult { stdin_closed: true }, + ), + ); + let _ = write_json_frame( + &stream, + &serde_json::to_value(response).unwrap_or_else(|_| json!({})), + ); } Err(error) => { close_result = Some(d2bd_runtime::daemon_audit::ShellAuditResult::Refused); - let _ = write_json_frame(&stream, &typed_shell_resource_error_frame(&error)); + let _ = write_named_process_stream_error(&stream, request_id, error); } } break; } - let op = match kind { - "namedStreamSend" => { - let data = value - .get("dataBase64") - .and_then(Value::as_str) - .and_then(|data| d2b_core::base64_codec::decode(data).ok()) - .unwrap_or_default(); + let op = match request.request { + public_wire::NamedProcessStreamRequest::Stdin { + offset, + chunk_base64, + eof, + } => { + if d2b_core::base64_codec::decode(&chunk_base64).is_err() { + let _ = write_named_process_stream_error( + &stream, + request_id, + shell_protocol_failed(), + ); + continue; + } + d2bd_runtime::shell_backend::ShellTerminalOp::WriteStdin( + d2b_contracts_control::terminal_wire::TerminalWriteStdin { + session: session.clone(), + offset, + chunk_base64, + eof, + }, + ) + } + public_wire::NamedProcessStreamRequest::CloseStdin { offset } => { d2bd_runtime::shell_backend::ShellTerminalOp::WriteStdin( d2b_contracts_control::terminal_wire::TerminalWriteStdin { session: session.clone(), - offset: value.get("offset").and_then(Value::as_u64).unwrap_or(0), - chunk_base64: d2b_core::base64_codec::encode(&data), - eof: false, + offset, + chunk_base64: String::new(), + eof: true, }, ) } - "namedStreamReceive" => d2bd_runtime::shell_backend::ShellTerminalOp::ReadOutput( + public_wire::NamedProcessStreamRequest::Read { + stream: public_wire::ExecStream::Stdout, + offset, + max_len, + wait, + timeout_ms, + } => d2bd_runtime::shell_backend::ShellTerminalOp::ReadOutput( d2b_contracts_control::terminal_wire::TerminalReadOutput { session: session.clone(), stream: d2b_contracts_control::terminal_wire::TerminalStream::Stdout, - offset: value.get("offset").and_then(Value::as_u64).unwrap_or(0), - max_len: value.get("maxLen").and_then(Value::as_u64).unwrap_or(65536), - wait: true, - timeout_ms: 50, + offset, + max_len, + wait, + timeout_ms, }, ), - "namedStreamResize" => d2bd_runtime::shell_backend::ShellTerminalOp::Resize( - d2b_contracts_control::terminal_wire::TerminalResize { - session: session.clone(), - op_id: control_sequence.saturating_add(1), - rows: value.get("rows").and_then(Value::as_u64).unwrap_or(24) as u32, - cols: value.get("cols").and_then(Value::as_u64).unwrap_or(80) as u32, - }, - ), - _ => { - let _ = write_json_frame( - &stream, - &typed_shell_resource_error_frame(&shell_protocol_failed()), - ); + public_wire::NamedProcessStreamRequest::Read { .. } + | public_wire::NamedProcessStreamRequest::Signal { .. } + | public_wire::NamedProcessStreamRequest::Wait { .. } => { + let _ = + write_named_process_stream_error(&stream, request_id, shell_protocol_failed()); continue; } + public_wire::NamedProcessStreamRequest::Resize { + control_seq, + rows, + cols, + } => { + if control_seq == 0 || control_seq <= control_sequence { + let _ = write_named_process_stream_error( + &stream, + request_id, + shell_failed( + d2bd_runtime::typed_error::GuestControlShellErrorKind::StaleSession, + ), + ); + continue; + } + control_sequence = control_seq.saturating_sub(1); + d2bd_runtime::shell_backend::ShellTerminalOp::Resize( + d2b_contracts_control::terminal_wire::TerminalResize { + session: session.clone(), + op_id: control_seq, + rows, + cols, + }, + ) + } + public_wire::NamedProcessStreamRequest::Close + | public_wire::NamedProcessStreamRequest::Cancel => unreachable!(), }; - match established + let response = match established .backend .handle_op(rt.handle(), &mut control_sequence, op) { Ok(Some(d2bd_runtime::shell_backend::ShellTerminalResponse::WriteStdin(result))) => { - let _ = write_json_frame( - &stream, - &serde_json::to_value(result).unwrap_or_else(|_| json!({})), - ); + if close_stdin { + public_wire::NamedProcessStreamResponse::Closed(public_wire::ExecCloseResult { + stdin_closed: result.stdin_closed, + }) + } else { + public_wire::NamedProcessStreamResponse::Stdin( + public_wire::ExecWriteStdinResult { + accepted_len: result.accepted_len, + next_offset: result.next_offset, + backpressured: result.backpressured, + stdin_closed: result.stdin_closed, + }, + ) + } } Ok(Some(d2bd_runtime::shell_backend::ShellTerminalResponse::ReadOutput(result))) => { - let _ = write_json_frame( - &stream, - &serde_json::to_value(result).unwrap_or_else(|_| json!({})), - ); + public_wire::NamedProcessStreamResponse::Output(public_wire::ExecReadOutputResult { + data_base64: result.data_base64, + next_offset: result.next_offset, + eof: result.eof, + dropped_bytes: result.dropped_bytes, + truncated: result.truncated, + timed_out: result.timed_out, + }) } - Ok(Some(_)) => { - let _ = write_json_frame(&stream, &json!({"delivered": true})); + Ok(Some(d2bd_runtime::shell_backend::ShellTerminalResponse::Delivered)) => { + public_wire::NamedProcessStreamResponse::Delivered(public_wire::ExecControlResult { + delivered: true, + }) } Ok(None) => break, Err(error) => { - let _ = write_json_frame(&stream, &typed_shell_resource_error_frame(&error)); + let _ = write_named_process_stream_error(&stream, request_id, error); + continue; } + }; + let response = public_wire::NamedProcessStreamResponseFrame::new(request_id, response); + if write_json_frame( + &stream, + &serde_json::to_value(response).unwrap_or_else(|_| json!({})), + ) + .is_err() + { + break; } } let close_result = close_result.unwrap_or_else(|| { - d2bd_runtime::shell_backend::best_effort_close( + d2bd_runtime::shell_backend::best_effort_cancel( established.backend.as_ref(), rt.handle(), &mut control_sequence, @@ -10466,61 +13309,12 @@ fn run_typed_shell_owner( shell_metric_for_provider(&state, established.provider, "close", "closed", "none"); } -struct GuestShellBackend { - client: Arc, - vm: String, - session_id: String, - guest_boot_id: String, -} - -impl std::fmt::Debug for GuestShellBackend { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("GuestShellBackend") - .field("vm", &self.vm) - .field("session_id", &"") - .field("guest_boot_id", &"") - .finish_non_exhaustive() - } -} - -impl d2bd_runtime::shell_backend::ShellBackend for GuestShellBackend { - fn handle_op( - &self, - runtime: &tokio::runtime::Handle, - control_sequence: &mut u64, - op: d2bd_runtime::shell_backend::ShellTerminalOp, - ) -> Result, TypedError> { - handle_shell_owner_op( - runtime, - self.client.as_ref(), - &self.vm, - &self.session_id, - &self.guest_boot_id, - control_sequence, - op, - ) - } - - fn close_attachment( - &self, - runtime: &tokio::runtime::Handle, - _control_sequence: &mut u64, - ) -> Result { - shell_close_attach_with_runtime( - runtime, - self.client.as_ref(), - &self.vm, - &self.session_id, - &self.guest_boot_id, - ) - } -} - async fn establish_shell_backend( state: &ServerState, peer_uid: u32, caller_role: BrokerCallerRole, attach: &public_wire::ShellAttachArgs, + component_session_driver: Option, ) -> Result { use workload_dispatch::WorkloadRoute; @@ -10528,37 +13322,40 @@ async fn establish_shell_backend( match resolved.route { WorkloadRoute::LocalVm { vm } => { - let mut guest_attach = attach.clone(); - guest_attach.vm = vm.clone(); - let (client, guest_boot_id, response) = - establish_guest_shell_owner_async(state, &guest_attach, caller_role.clone()) - .await?; - shell_error_to_typed(response.error.as_ref())?; - let session_id = response.session_id.clone().ok_or_else(|| { - tracing::warn!( - vm = %vm, - resolved_name_valid = %public_wire::ShellName::new(&response.resolved_name).is_ok(), - state = ?response.state.enum_value(), - force_evicted = response.force_evicted, - error_present = response.error.is_some(), - "shell attach response missing session id" - ); - shell_protocol_failed() - })?; - let initial_control_sequence = response.control_seq; - let attach = map_shell_attach_response(response)?; + let Some(driver) = component_session_driver else { + let _ = (state, peer_uid, caller_role, attach); + return Err(TypedError::RuntimeCapabilityUnsupported { + vm, + runtime_kind: "component-session".to_owned(), + capability: "persistent-shell".to_owned(), + verb: "shell".to_owned(), + }); + }; + let public_session = new_public_shell_session_handle()?; + let resolved_name = attach + .name + .clone() + .unwrap_or_else(|| public_wire::ShellName::new("primary").expect("valid name")); + let backend = d2bd_runtime::shell_backend::ComponentSessionShellBackend::open( + driver, + next_component_session_stream_number(), + public_session.clone(), + resolved_name.clone(), + ) + .await + .map_err(|_| shell_transport_failed())?; Ok(d2bd_runtime::shell_backend::EstablishedShell { - backend: Arc::new(GuestShellBackend { - client, - vm: vm.clone(), - session_id, - guest_boot_id, - }), - attach, + backend: Arc::new(backend), + attach: public_wire::ShellAttachResult { + session: public_session, + resolved_name, + state: public_wire::ShellSessionState::Attached, + force_evicted: false, + }, target: vm, provider: d2bd_runtime::shell_backend::ShellProvider::GuestControl, operation_digest: None, - initial_control_sequence, + initial_control_sequence: 0, }) } WorkloadRoute::UnsafeLocal => { @@ -10777,6 +13574,8 @@ fn map_shell_helper_registry_error( } } +#[cfg(test)] +#[allow(dead_code)] async fn establish_guest_shell_owner_async( state: &ServerState, attach: &public_wire::ShellAttachArgs, @@ -10850,6 +13649,8 @@ async fn establish_guest_shell_owner_async( Ok((Arc::new(client), evidence.guest_boot_id, response)) } +#[cfg(test)] +#[allow(dead_code)] fn shell_request_metadata(vm: &str) -> pb::RequestMetadata { let mut metadata = pb::RequestMetadata::new(); metadata.vm_id = vm.to_owned(); @@ -10858,6 +13659,8 @@ fn shell_request_metadata(vm: &str) -> pb::RequestMetadata { metadata } +#[cfg(test)] +#[allow(dead_code)] fn shell_terminal_metadata( vm: &str, session_id: &str, @@ -10871,6 +13674,7 @@ fn shell_terminal_metadata( metadata } +#[cfg(test)] fn ensure_shell_owner_session( request_session: &str, owner_session: &str, @@ -10884,6 +13688,8 @@ fn ensure_shell_owner_session( } } +#[cfg(test)] +#[allow(dead_code)] fn handle_shell_owner_op( rt: &tokio::runtime::Handle, client: &d2bd_runtime::guest_control_health::TtrpcGuestControlClient, @@ -11011,6 +13817,8 @@ fn handle_shell_owner_op( } } +#[cfg(test)] +#[allow(dead_code)] fn shell_close_attach_with_runtime( rt: &tokio::runtime::Handle, client: &d2bd_runtime::guest_control_health::TtrpcGuestControlClient, @@ -11028,68 +13836,6 @@ fn shell_close_attach_with_runtime( map_shell_detach_response(response) } -fn run_guest_shell_management( - state: &ServerState, - vm: &str, - caller_role: BrokerCallerRole, - f: F, -) -> Result -where - F: FnOnce( - Arc, - pb::RequestMetadata, - ) -> Fut, - Fut: Future>, -{ - ensure_vm_runtime_capability(state, vm, RuntimeCapabilityGate::GuestControl, "shell")?; - let resolver = load_bundle_resolver(state)?; - let params = resolve_guest_control_probe_params(state, &resolver, vm).map_err(|detail| { - tracing::warn!( - kind = "critical", - subsystem = "guest-control-shell", - error_kind = "transport-io", - "guest-control shell: probe params unresolved: {detail}" - ); - shell_transport_failed() - })?; - let broker_path = broker_socket_path(state); - block_on_future(async move { - let budget = d2bd_runtime::guest_control_health::AttemptBudget::from_now( - SHELL_MANAGEMENT_TIMEOUT, - guest_control_bridge::GUEST_CONTROL_ATTEMPT_CAP, - ); - let signer = guest_control_bridge::BrokerSigner::with_caller_role( - broker_path, - budget, - caller_role.clone(), - ); - let nonce = guest_control_bridge::host_nonce().map_err(|_| shell_transport_failed())?; - let client = guest_control_bridge::connect_and_build_client(¶ms, budget) - .map_err(map_shell_health_error)?; - let evidence = d2bd_runtime::guest_control_health::probe_guest_control_health( - ¶ms.vm_id, - Some(guest_control_bridge::VMADDR_CID_HOST), - nonce, - &client, - &signer, - ) - .await - .map_err(map_shell_health_error)?; - if !guest_advertises_capability( - &evidence.health.capabilities, - pb::GuestCapability::GUEST_CAPABILITY_SHELL_MANAGEMENT, - ) { - return Err(shell_capability_failed()); - } - let mut metadata = pb::RequestMetadata::new(); - metadata.vm_id = params.vm_id.clone(); - metadata.request_id = "guest-control-shell".to_owned(); - metadata.protocol_version = - d2b_contracts_control::guest_wire::GUEST_CONTROL_PROTOCOL_VERSION; - f(Arc::new(client), metadata).await - }) -} - /// Increment the closed-label exec outcome counter. `outcome` and `error_kind` /// are the only labels besides the constant subsystem; all three are drawn /// from a hard allowlist (no vm/uid/handle/argv ever becomes a label). @@ -11163,6 +13909,7 @@ fn emit_detached_kill_audit( /// command channel, which returns the worker, drops the runtime, and drops the /// authenticated client - prompting the guest `close_connection` and PTY /// teardown. The slot is released when its RAII guard drops on return. +#[cfg(test)] fn run_exec_owner( stream: Socket, state: ServerState, @@ -22136,6 +24883,358 @@ mod detached_exec_routing_tests { ) } + #[test] + fn retired_exec_wire_is_rejected_before_any_guest_control_call() { + let error = retired_exec_wire_error(); + assert_eq!(error.kind(), "wire-unsupported-request"); + assert_eq!(error.exit_code(), 54); + let frame = serde_json::to_value(d2bd_runtime::wire::error_frame(&error)) + .expect("error frame serializes"); + assert_eq!(frame["error"]["kind"], "wire-unsupported-request"); + assert_eq!(frame["error"]["message"], "unsupported request type exec"); + } + + #[test] + fn production_connection_dispatch_has_no_retired_exec_or_guest_shell_caller() { + let source = include_str!("composition.rs"); + let connection_start = source + .find("fn handle_connection_authorized(") + .expect("authorized connection handler"); + let connection_end = source[connection_start..] + .find("\nfn run_gateway_display_owner(") + .map(|offset| connection_start + offset) + .expect("authorized connection handler end"); + let connection = &source[connection_start..connection_end]; + assert!(connection.contains("run_process_resource_owner")); + assert!(!connection.contains("run_exec_owner")); + assert!(!connection.contains("establish_guest_shell_owner_async")); + assert!(!connection.contains("GuestShellBackend")); + + let shell_start = source + .find("async fn establish_shell_backend(") + .expect("shell backend resolver"); + let shell_end = source[shell_start..] + .find("\nfn resolve_shell_target(") + .map(|offset| shell_start + offset) + .expect("shell backend resolver end"); + let shell = &source[shell_start..shell_end]; + assert!(shell.contains("ComponentSessionShellBackend::open")); + assert!(!shell.contains("establish_guest_shell_owner_async")); + assert!(!shell.contains("GuestShellBackend")); + + let process_start = source + .find("fn run_process_resource_owner(") + .expect("Process owner handler"); + let process_end = source[process_start..] + .find("\nfn run_typed_shell_owner(") + .map(|offset| process_start + offset) + .expect("Process owner handler end"); + let process = &source[process_start..process_end]; + assert!(process.contains("ResourceExecConnector::new")); + assert!(process.contains("production_process_resource_port")); + assert!(!process.contains("UnavailableProcessResourcePort")); + assert!(source.contains("ResourceDetachedClient::new")); + assert!(source.contains("ShellAuthorityLedger")); + assert!(source.contains("ensure_shell_resources")); + assert!(source.contains("remove_supervisor_process_resource")); + let production = source + .split_once("#[cfg(test)]") + .map_or(source, |(production, _)| production); + assert!(!production.contains("InMemoryShellAuthority")); + } + + #[test] + fn process_resource_ref_covers_all_create_request_identity_forms() { + let explicit = serde_json::json!({ + "resourceRef": "EphemeralProcess/explicit", + "opId": 7, + "requestId": "request", + "name": "named", + "command": ["true"], + }); + assert_eq!( + process_resource_ref(&explicit) + .expect("explicit resource ref") + .to_canonical_string(), + "EphemeralProcess/explicit" + ); + + let op_id = serde_json::json!({"opId": 7}); + assert_eq!( + process_resource_ref(&op_id) + .expect("operation resource ref") + .to_canonical_string(), + "EphemeralProcess/exec-7" + ); + + let request_id = serde_json::json!({"requestId": "request"}); + let request_id_ref = process_resource_ref(&request_id).expect("request resource ref"); + assert_eq!( + request_id_ref.to_canonical_string(), + format!("EphemeralProcess/exec-{}", shell_ref_digest(&["request"])) + ); + + let command = serde_json::json!({"command": ["true", "--version"]}); + let command_ref = process_resource_ref(&command).expect("command resource ref"); + assert_eq!( + command_ref.to_canonical_string(), + format!( + "EphemeralProcess/exec-{}", + shell_ref_digest(&["true", "--version"]) + ) + ); + } + + #[test] + fn shell_supervisor_process_intent_is_owner_and_finalizer_bound() { + let zone = ZoneId::parse("dev").expect("zone"); + let pool = ShellPool::new( + "guest-alice", + zone.as_str(), + PoolSpec::new( + ExecutionTarget::guest("work"), + "alice", + "artifact://shells/bash-login", + 1, + 1, + 4096, + ) + .expect("pool spec"), + ) + .expect("pool"); + let session = + ShellSession::from_pool(&pool, "guest-alice-main", "main", None).expect("session"); + let process = SupervisorProcessResource::for_session(&session); + let mut spec = serde_json::to_value(process.spec()).expect("process spec"); + spec.as_object_mut().expect("process spec object").insert( + "providerRef".to_owned(), + Value::String("Provider/system-systemd".to_owned()), + ); + let payload = DaemonShellAuthority::shell_process_resource_payload( + &zone, + process.resource_ref(), + Some(process.owner_ref()), + spec, + ) + .expect("resource payload"); + let value: Value = serde_json::from_slice(&payload).expect("payload JSON"); + assert_eq!( + value.pointer("/metadata/ownerRef").and_then(Value::as_str), + Some("shell-terminal.d2bus.org.ShellSession/guest-alice-main") + ); + assert_eq!( + value + .pointer("/metadata/finalizers/0") + .and_then(Value::as_str), + Some(PROCESS_RUNTIME_FINALIZER) + ); + assert_eq!( + value.pointer("/spec/providerRef").and_then(Value::as_str), + Some("Provider/system-systemd") + ); + assert_eq!( + value.pointer("/spec/executionRef").and_then(Value::as_str), + Some("Guest/work") + ); + assert_eq!( + value.pointer("/spec/template").and_then(Value::as_str), + Some("shell-supervisor-main") + ); + let mut stored = value; + stored + .get_mut("metadata") + .and_then(Value::as_object_mut) + .expect("metadata") + .insert( + "uid".to_owned(), + Value::String("123e4567-e89b-42d3-a456-426614174000".to_owned()), + ); + let stored = serde_json::to_vec(&stored).expect("stored process"); + let envelope = + d2b_contracts_resource::v3::ResourceEnvelope::from_json(&stored).expect("envelope"); + assert!(DaemonShellAuthority::process_matches(&envelope, &process)); + assert!(DaemonShellAuthority::process_has_finalizer(&envelope)); + } + + #[test] + fn shell_session_resource_anchor_precedes_supervisor_process() { + let zone = ZoneId::parse("local-root").expect("zone"); + let pool = ShellPool::new( + "work-shell", + zone.as_str(), + PoolSpec::new( + ExecutionTarget::guest("work"), + "alice", + "artifact://shells/bash-login", + 8, + 1, + 4096, + ) + .expect("pool spec"), + ) + .expect("pool"); + let session = ShellSession::from_pool_with_resource_name(&pool, "primary", "primary", None) + .expect("session"); + let pool_ref = + ResourceRef::parse("shell-terminal.d2bus.org.ShellPool/work-shell").expect("pool ref"); + let spec = serde_json::json!({ + "providerRef": "Provider/shell-terminal", + "poolRef": pool_ref.to_canonical_string(), + "executionRef": "Guest/work", + "userRef": "User/alice", + "loginShellRef": "artifact://shells/bash-login", + "sessionName": "primary", + "outputRingCapacity": 4096, + "desiredLifecycle": "running", + }); + let session_ref = + ResourceRef::parse("shell-terminal.d2bus.org.ShellSession/primary").unwrap(); + let payload = DaemonShellAuthority::shell_resource_payload( + &zone, + &session_ref, + Some(&pool_ref), + spec, + &[], + ) + .expect("session payload"); + let value: Value = serde_json::from_slice(&payload).expect("payload JSON"); + assert_eq!( + value.pointer("/metadata/ownerRef").and_then(Value::as_str), + Some(pool_ref.to_canonical_string().as_str()) + ); + assert!( + value + .pointer("/metadata/finalizers") + .and_then(Value::as_array) + .is_some_and(Vec::is_empty) + ); + assert_eq!( + value.pointer("/spec/executionRef").and_then(Value::as_str), + Some( + session + .supervisor_execution_ref() + .to_canonical_string() + .as_str() + ) + ); + } + + #[test] + fn process_resource_defaults_user_domain_from_guest_manifest() { + let state = test_state(exec_session::ExecSessionCaps::default()); + let execution_ref = ResourceRef::parse("Guest/work").expect("guest target"); + assert_eq!( + resolve_process_user_ref(&state, &execution_ref).as_deref(), + Some("User/alice") + ); + } + + #[test] + fn process_resource_create_denies_launcher_before_resource_port() { + let _env = PeerOverrideEnv::launcher(); + let mut state = test_state(exec_session::ExecSessionCaps::default()); + state.config.launcher_users = vec!["launcher".to_owned()]; + let (daemon, client) = seqpacket_pair(); + let run_state = state.clone(); + let handle = std::thread::spawn(move || handle_connection(daemon, &run_state, None)); + + write_frame(&client, &hello_frame()).expect("client sends hello"); + assert_eq!(recv_reply(&client)["type"], "helloOk"); + write_frame( + &client, + &serde_json::to_vec(&serde_json::json!({ + "type": "resourceRequest", + "method": "Create", + "service": "d2b.resource.v3", + "sessionVerb": "attach", + "zoneRef": "Zone/local-root", + "resourceType": "EphemeralProcess", + "resourceRef": "EphemeralProcess/exec-1", + "executionRef": "Guest/work", + "tty": false, + "detached": false, + "argv": ["true"], + })) + .unwrap(), + ) + .expect("client sends Process create"); + let reply = recv_reply(&client); + assert_eq!(reply["error"]["kind"], "authz-not-admin"); + drop(client); + handle + .join() + .expect("daemon thread joins") + .expect("connection exits after client EOF"); + } + + #[test] + fn admin_process_resource_create_uses_resource_connector_and_fails_closed_without_session() { + let state = test_state(exec_session::ExecSessionCaps::default()); + let (daemon, client) = seqpacket_pair(); + let owner_state = state.clone(); + run_process_resource_owner( + daemon, + owner_state, + admin_peer(), + serde_json::json!({ + "resourceType": "EphemeralProcess", + "resourceRef": "EphemeralProcess/exec-1", + "executionRef": "Guest/work", + "tty": false, + "detached": false, + "argv": ["true"], + }), + None, + ); + let reply = recv_reply(&client); + assert_eq!(reply["type"], "error"); + assert_eq!(reply["error"]["kind"], "runtime-capability-unsupported"); + assert!( + reply["error"]["message"] + .as_str() + .is_some_and(|message| message.contains("component-session")) + ); + assert_eq!(state.exec_sessions.len(), 0); + } + + #[test] + fn process_resource_management_keeps_admin_only_authorization() { + let state = test_state(exec_session::ExecSessionCaps::default()); + let request = serde_json::json!({ + "resourceType": "EphemeralProcess", + "method": "List", + "executionRef": "Guest/work", + }); + assert_eq!( + dispatch_resource_exec_request(&state, &launcher_peer(), &request) + .unwrap_err() + .kind(), + "authz-not-admin" + ); + let error = dispatch_resource_exec_request(&state, &admin_peer(), &request).unwrap_err(); + assert_eq!(error.kind(), "runtime-capability-unsupported"); + } + + #[test] + fn detached_process_resource_create_allows_launcher_role_before_provider_admission() { + let state = test_state(exec_session::ExecSessionCaps::default()); + let request = serde_json::json!({ + "resourceType": "EphemeralProcess", + "method": "Create", + "sessionVerb": "invoke", + "executionRef": "Guest/work", + "detached": true, + "argv": ["true"], + }); + let error = + dispatch_resource_exec_create_request(&state, &launcher_peer(), &request).unwrap_err(); + assert_eq!(error.kind(), "runtime-capability-unsupported"); + assert!( + !error.message().contains("not authorized"), + "launcher admission must reach the resource Provider boundary" + ); + } + fn test_state(caps: exec_session::ExecSessionCaps) -> ServerState { let broker_reap_log = BrokerReapLog::new(); let temp_root = tempfile::Builder::new() @@ -22452,7 +25551,9 @@ mod detached_exec_routing_tests { .expect("local VM launcher function end"); let body = &source[start..end]; - assert!(body.contains("exec_detached::create_idempotent")); + assert!(body.contains("ResourceDetachedClient::new")); + assert!(body.contains("production_process_resource_port")); + assert!(!body.contains("UnavailableDetachedProcessResourcePort")); assert!(body.contains("emit_detached_create_audit")); assert!(body.contains("&result.exec_id")); } @@ -22923,13 +26024,13 @@ mod detached_exec_routing_tests { } /// The public.sock accept loop is serial: it accepts one connection, runs -/// `handle_connection`, then accepts the next. An exec session's owner -/// connection is long-lived, so `handle_connection` MUST hand the exec session -/// off to a spawned owner thread and return immediately - otherwise the single -/// accept loop would be pinned for the entire lifetime of one exec session and -/// no other client could be served. These hermetic tests drive +/// `handle_connection`, then accepts the next. A Process resource owner +/// connection is long-lived, so `handle_connection` MUST hand it off to a +/// spawned owner thread and return immediately - otherwise the single accept +/// loop would be pinned for the lifetime of one process attachment and no +/// other client could be served. These hermetic tests drive /// `handle_connection` over a real `SOCK_SEQPACKET` pair (no live VM) and prove -/// the exec branch spawns-and-returns while a second request is still served. +/// the Process branch spawns-and-returns while a second request is still served. #[cfg(test)] mod accept_loop_concurrency_tests { use super::*; @@ -22938,7 +26039,6 @@ mod accept_loop_concurrency_tests { use std::sync::mpsc; use std::time::Duration; - use d2b_contracts_control::public_wire::{ExecOp, ExecStartArgs}; use serde_json::json; use tempfile::TempDir; @@ -23029,21 +26129,22 @@ mod accept_loop_concurrency_tests { .expect("encode workload list frame") } - fn exec_start_frame(op_id: u64) -> Vec { - let op = ExecOp::Start(ExecStartArgs { - vm: "work".to_owned(), - argv: vec!["true".to_owned()], - tty: false, - detached: false, - env: None, - cwd: None, - term_size: None, - }); - let mut value = serde_json::to_value(&op).expect("encode exec op"); - let object = value.as_object_mut().expect("exec op object"); - object.insert("type".to_owned(), json!("exec")); - object.insert("opId".to_owned(), json!(op_id)); - serde_json::to_vec(&value).expect("serialize exec frame") + fn process_resource_start_frame(op_id: u64) -> Vec { + serde_json::to_vec(&json!({ + "type": "resourceRequest", + "method": "Create", + "service": "d2b.resource.v3", + "sessionVerb": "attach", + "zoneRef": "Zone/local-root", + "resourceType": "EphemeralProcess", + "resourceRef": format!("EphemeralProcess/exec-{op_id}"), + "executionRef": "Guest/work", + "tty": false, + "detached": false, + "argv": ["true"], + "opId": op_id, + })) + .expect("serialize Process resource frame") } /// Scoped guard for the test SO_PEERCRED override so a panic still clears @@ -23168,17 +26269,17 @@ mod accept_loop_concurrency_tests { } #[test] - fn exec_dispatch_returns_to_the_accept_loop_and_a_second_request_is_served() { + fn process_dispatch_returns_to_the_accept_loop_and_a_second_request_is_served() { use std::sync::{Condvar, Mutex}; let _env = PeerOverrideEnv::admin(); let (state, _state_dir) = admin_exec_state(); let state = Arc::new(state); - // Install an owner-body hook that genuinely HOLDS the exec owner session + // Install an owner-body hook that genuinely HOLDS the Process owner session // open: it flags `running`, signals `entered`, then blocks until the // test releases it. Because the hook runs at the top of - // `run_exec_owner`, a hypothetical inline `handle_connection` would block + // `run_process_resource_owner`, a hypothetical inline `handle_connection` would block // HERE on the accept-loop thread and never return - the watchdog below // would fire. An off-loop spawn (the real behaviour) returns Ok promptly // while this body is still blocked. @@ -23223,7 +26324,8 @@ mod accept_loop_concurrency_tests { // SOCK_SEQPACKET preserves message boundaries, so both datagrams can be // buffered before the daemon reads them. write_frame(&client_a, &hello_frame()).expect("client A sends hello"); - write_frame(&client_a, &exec_start_frame(1)).expect("client A sends exec start"); + write_frame(&client_a, &process_resource_start_frame(1)) + .expect("client A sends Process create"); // The client deliberately keeps its end OPEN for the session's lifetime. let state_a = Arc::clone(&state); @@ -23233,7 +26335,7 @@ mod accept_loop_concurrency_tests { let _ = done_tx.send(result.is_ok()); }); - // The owner body must actually be entered (the exec branch dispatched a + // The owner body must actually be entered (the Process branch dispatched a // real, blocking owner session - not a fast-failed stub). { let (lock, cv) = &*shared; @@ -23243,14 +26345,14 @@ mod accept_loop_concurrency_tests { while !s.entered { let remaining = deadline .checked_sub(start.elapsed()) - .expect("exec owner body was not entered within the deadline"); + .expect("Process owner body was not entered within the deadline"); let (guard, timeout) = cv.wait_timeout(s, remaining).expect("hook entered wait"); s = guard; - assert!(!timeout.timed_out(), "exec owner body was not entered"); + assert!(!timeout.timed_out(), "Process owner body was not entered"); } } - // handle_connection must have returned Ok PROMPTLY even though the owner + // handle_connection must have returned Ok PROMPTLY even though the Process owner // body is still blocked in the hook. An inline implementation would be // stuck in the hook on this very thread's predecessor and never send. let returned = done_rx.recv_timeout(Duration::from_secs(10)); diff --git a/packages/d2bd/src/interaction_composition.rs b/packages/d2bd/src/interaction_composition.rs index 4ddeb99520..cf20333aba 100644 --- a/packages/d2bd/src/interaction_composition.rs +++ b/packages/d2bd/src/interaction_composition.rs @@ -30,22 +30,17 @@ use d2b_bus::{ }; use d2b_contracts::types::{BundleOpId, RoleId, VmId}; use d2b_contracts_broker::broker_wire::{BrokerCallerRole, RunnerRole, SandboxLaunchPlan}; -use d2b_contracts_zone_session::v3::{ - component_session::{AttachmentKind, AttachmentPolicy, AttachmentPolicyKind, AttachmentPurpose, EndpointPolicy, EndpointPurpose, EndpointRole, IdentityEvidenceRequirement, LimitProfile, Locality as TransportLocality, NoiseProfile, PurposeClass, ServicePackage, TransportBinding, TransportClass}, -}; +use d2b_contracts_resource::v3::identity::{EvidenceClass, ServiceName}; use d2b_contracts_resource::v3::{ - ControllerGeneration, - ResourceGeneration, - ResourceRef, - ResourceUid, - ZoneId, - ZoneRevision, + ControllerGeneration, ResourceGeneration, ResourceRef, ResourceUid, ZoneId, ZoneRevision, execution_policy::{BoundedToken, ExecutionDomain}, process::{CapabilityClass, EnvironmentClass, MappingClass, NamespaceClass, UserNamespaceSpec}, }; -use d2b_contracts_resource::v3::identity::{ - EvidenceClass, - ServiceName, +use d2b_contracts_zone_session::v3::component_session::{ + AttachmentKind, AttachmentPolicy, AttachmentPolicyKind, AttachmentPurpose, EndpointPolicy, + EndpointPurpose, EndpointRole, IdentityEvidenceRequirement, LimitProfile, + Locality as TransportLocality, NoiseProfile, PurposeClass, ServicePackage, TransportBinding, + TransportClass, }; use d2b_core::{ bundle_resolver::{BundleResolver, ResolvedRunnerIntent}, @@ -85,8 +80,8 @@ use d2b_resource_api::authz::{ }; use d2b_resource_store::PolicySnapshot; use d2b_session::{ - AuthenticatedSessionRouteBinding, OwnedAttachment, OwnedTransport, SessionAcceptor, - SessionEngine, TransportEvidence, operation_catalog_entry, ttrpc_stream_id, + AuthenticatedSessionRouteBinding, ComponentSessionDriver, OwnedAttachment, OwnedTransport, + SessionAcceptor, SessionEngine, TransportEvidence, operation_catalog_entry, ttrpc_stream_id, }; use d2b_session_unix::{ CreditPool, CreditScopeSet, PeerIdentityPolicy, SeqpacketSocket, UnixSeqpacketTransport, @@ -116,7 +111,18 @@ pub enum InteractionAdmissionError { ServiceUnavailable, } -const INTERACTION_SERVICES: &[(&str, ServicePackage)] = &[ +/// The fixed ComponentSession service identities accepted by the daemon. +/// +/// Process attachment uses the generic Provider package on the wire, while +/// persistent shell attachment uses its provider-defined supervisor identity. +/// The listener identity is retained separately from the package so these +/// sessions cannot be confused with ordinary interaction Providers. +/// Generic Provider package used by Process/EphemeralProcess attachment. +pub(crate) const PROCESS_ATTACH_SERVICE: &str = ServicePackage::ProviderV3.as_str(); +/// Per-session shell supervisor service used by ShellSession attachment. +pub(crate) const SHELL_SESSION_SERVICE: &str = d2b_provider_shell_terminal::SUPERVISOR_SERVICE; + +const COMPONENT_SESSION_SERVICES: &[(&str, ServicePackage)] = &[ ( d2b_provider_display_wayland::SERVICE_PACKAGE, ServicePackage::DisplayV3, @@ -137,13 +143,15 @@ const INTERACTION_SERVICES: &[(&str, ServicePackage)] = &[ d2b_provider_notification_desktop::SERVICE_PACKAGE, ServicePackage::NotificationV3, ), + (PROCESS_ATTACH_SERVICE, ServicePackage::ProviderV3), + (SHELL_SESSION_SERVICE, ServicePackage::ProviderV3), ]; -/// Return the exact ComponentSession policy for one daemon-owned interaction -/// listener. Each service has a distinct Unix socket so the handshake offer +/// Return the exact ComponentSession policy for one daemon-owned service +/// listener. Each service has a distinct Unix socket so the handshake offer /// cannot select a different Provider after the listener policy is chosen. pub fn interaction_endpoint_policy(service: &str, generation: u64) -> Option { - let (_, package) = INTERACTION_SERVICES + let (_, package) = COMPONENT_SESSION_SERVICES .iter() .find(|(candidate, _)| *candidate == service)?; let attachment_policy = AttachmentPolicy { @@ -196,6 +204,7 @@ fn binding_digest(policy: &EndpointPolicy) -> d2b_contracts_resource::v3::identi pub struct RegisteredInteractionSession { ingress: BusIngress, route: AuthenticatedSessionRouteBinding, + service_identity: String, } impl RegisteredInteractionSession { @@ -214,20 +223,35 @@ impl RegisteredInteractionSession { self.route.service() } + /// Return the fixed listener service identity admitted for this session. + pub fn service_identity(&self) -> &str { + &self.service_identity + } + /// Return the stable daemon-local key for this authenticated session. pub fn session_key(&self) -> String { - interaction_session_key(&self.route) + interaction_session_key(&self.service_identity, &self.route) } /// Clone the daemon-owned request receiver demultiplexed by the bus. pub fn request_receiver(&self) -> ComponentRequestReceiver { self.ingress.component_request_receiver() } + + /// Clone the authenticated ComponentSession driver for daemon-owned + /// target-local named-stream composition. + pub fn component_session_driver(&self) -> Option { + self.ingress.component_session_driver() + } } -fn interaction_session_key(route: &AuthenticatedSessionRouteBinding) -> String { +fn interaction_session_key( + service_identity: &str, + route: &AuthenticatedSessionRouteBinding, +) -> String { format!( - "{}|{}|{}|{}", + "{}|{}|{}|{}|{}", + service_identity, route.zone().as_str(), route.service().as_str(), route.subject_uid().as_str(), @@ -284,6 +308,47 @@ pub async fn admit_and_register_with_route( evidence: TransportEvidence, now_tick: u64, ) -> Result +where + T: OwnedTransport + 'static, +{ + admit_and_register_with_route_identity(registrar, acceptor, engine, evidence, now_tick, None) + .await +} + +/// Authenticate, register, and retain the fixed service identity of a +/// daemon-owned listener. This is distinct from the wire package because a +/// provider may expose more than one service over the generic Provider +/// package. +pub(crate) async fn admit_and_register_with_service( + registrar: &mut ZoneRegistrar, + acceptor: SessionAcceptor, + engine: SessionEngine, + evidence: TransportEvidence, + now_tick: u64, + service_identity: &str, +) -> Result +where + T: OwnedTransport + 'static, +{ + admit_and_register_with_route_identity( + registrar, + acceptor, + engine, + evidence, + now_tick, + Some(service_identity), + ) + .await +} + +async fn admit_and_register_with_route_identity( + registrar: &mut ZoneRegistrar, + acceptor: SessionAcceptor, + engine: SessionEngine, + evidence: TransportEvidence, + now_tick: u64, + service_identity: Option<&str>, +) -> Result where T: OwnedTransport + 'static, { @@ -292,14 +357,32 @@ where .await .map_err(|_| InteractionAdmissionError::SessionAdmission)?; let route = session.route_binding(); + let service_identity = match service_identity { + Some(service_identity) + if COMPONENT_SESSION_SERVICES + .iter() + .any(|(candidate, package)| { + *candidate == service_identity && package.as_str() == route.service().as_str() + }) => + { + service_identity.to_owned() + } + Some(_) => return Err(InteractionAdmissionError::Registration), + None => route.service().as_str().to_owned(), + }; let ingress = registrar .register_component_session(session) .await .map_err(InteractionAdmissionError::from)?; - Ok(RegisteredInteractionSession { ingress, route }) + Ok(RegisteredInteractionSession { + ingress, + route, + service_identity, + }) } -/// The daemon-owned composition for one authenticated interaction session. +/// The daemon-owned composition for one authenticated Provider or +/// Process/Shell session. /// /// This object is intentionally the only place where a registered bus ingress, /// Provider runtime state, and supervisor effect owner meet. The Provider @@ -330,7 +413,8 @@ where notification_configuration: Option, } -/// Daemon-owned collection of independently Zone-bound compositions. +/// Daemon-owned collection of independently Zone-bound Provider and +/// Process/Shell compositions. pub struct InteractionRuntimeSet where S: ProcessLaunchEffectPort + Clone + Send + Sync + 'static, @@ -387,6 +471,22 @@ where self.runtimes.get_mut(zone.as_str()) } + /// Find the sole authenticated ComponentSession owned by one exact + /// execution target and service across the daemon's Zone compositions. + /// Absent, stale, or ambiguous sources fail closed. + pub fn component_session_driver_for_target( + &self, + service: &str, + target: &ResourceRef, + ) -> Option { + let mut drivers = self + .runtimes + .values() + .filter_map(|runtime| runtime.component_session_driver_for_target(service, target)); + let driver = drivers.next()?; + drivers.next().is_none().then_some(driver) + } + async fn remove_session(&mut self, zone: &ZoneId, session_key: &str) -> Result<(), String> { self.runtime_for_mut(zone) .ok_or_else(|| "interaction runtime unavailable".to_owned())? @@ -412,6 +512,23 @@ where } } +/// Resolve one exact service-owned ComponentSession without converting a +/// contended runtime lock into an absent source. +pub(crate) fn blocking_component_session_driver_for_service( + runtime: &Arc>>>, + service: &str, + target: &ResourceRef, +) -> Option +where + S: ProcessLaunchEffectPort + Clone + Send + Sync + 'static, + G: GuestFrontendEffectPort + 'static, +{ + let runtime = runtime.blocking_lock(); + runtime + .as_ref() + .and_then(|runtime| runtime.component_session_driver_for_target(service, target)) +} + impl Default for InteractionRuntimeSet where S: ProcessLaunchEffectPort + Clone + Send + Sync + 'static, @@ -983,15 +1100,62 @@ where where T: OwnedTransport + 'static, { - let session = admit_and_register_with_route( - &mut self.registrar, - acceptor, - engine, - evidence, - now_tick, - ) - .await?; - let service = session.route().service().as_str().to_owned(); + self.admit_and_register_inner(acceptor, engine, evidence, now_tick, None) + .await + } + + /// Admit a ComponentSession while retaining the fixed service identity of + /// the listener that accepted it. + pub async fn admit_and_register_for_service( + &mut self, + acceptor: SessionAcceptor, + engine: SessionEngine, + evidence: TransportEvidence, + now_tick: u64, + service_identity: &str, + ) -> Result<&RegisteredInteractionSession, InteractionAdmissionError> + where + T: OwnedTransport + 'static, + { + self.admit_and_register_inner(acceptor, engine, evidence, now_tick, Some(service_identity)) + .await + } + + async fn admit_and_register_inner( + &mut self, + acceptor: SessionAcceptor, + engine: SessionEngine, + evidence: TransportEvidence, + now_tick: u64, + service_identity: Option<&str>, + ) -> Result<&RegisteredInteractionSession, InteractionAdmissionError> + where + T: OwnedTransport + 'static, + { + let session = match service_identity { + Some(service_identity) => { + admit_and_register_with_service( + &mut self.registrar, + acceptor, + engine, + evidence, + now_tick, + service_identity, + ) + .await? + } + None => { + admit_and_register_with_route( + &mut self.registrar, + acceptor, + engine, + evidence, + now_tick, + ) + .await? + } + }; + let service = session.service().as_str().to_owned(); let session_key = session.session_key(); if self.sessions.contains_key(&session_key) { let RegisteredInteractionSession { ingress, .. } = session; @@ -1044,6 +1208,33 @@ where .collect() } + /// Find the sole authenticated ComponentSession for one exact execution + /// target and service. The route context is the only source of target + /// identity; no caller-supplied session handle is accepted. Absent, stale, + /// or ambiguous sources fail closed. + pub fn component_session_driver_for_target( + &self, + service: &str, + target: &ResourceRef, + ) -> Option { + let mut drivers = self.sessions.values().filter_map(|session| { + if session.service_identity() != service { + return None; + } + let context = session.route().context(); + let target_matches = context.execution_ref() == Some(target) + || (!matches!(service, PROCESS_ATTACH_SERVICE | SHELL_SESSION_SERVICE) + && context.execution_ref().is_none() + && context.subject_ref() == target); + let driver = target_matches + .then(|| session.component_session_driver()) + .flatten()?; + (driver.generation() == session.route().reconnect_generation().get()).then_some(driver) + }); + let driver = drivers.next()?; + drivers.next().is_none().then_some(driver) + } + fn route_for_session(&self, session_key: &str) -> Option<&AuthenticatedSessionRouteBinding> { self.sessions .get(session_key) @@ -3908,9 +4099,9 @@ fn unix_guest_subject_uid(uid: u32) -> ResourceUid { .expect("digest-derived test guest UID is valid") } -/// Bind and retain the daemon-owned ComponentSession listeners for all -/// interaction Provider service packages. Providers do not open these -/// sockets and no Provider-owned service unit is created. +/// Bind and retain the daemon-owned ComponentSession listeners for all fixed +/// service packages. Providers do not open these sockets and no Provider-owned +/// service unit is created. pub fn spawn_interaction_listeners( runtime: Arc>>>, state_dir: PathBuf, @@ -3998,11 +4189,11 @@ where } Ok(()) } - let mut paths = Vec::with_capacity(INTERACTION_SERVICES.len()); + let mut paths = Vec::with_capacity(COMPONENT_SESSION_SERVICES.len()); let handlers = Arc::new(Mutex::new(Vec::new())); let active_handlers = Arc::new(AtomicUsize::new(0)); - let mut threads = Vec::with_capacity(INTERACTION_SERVICES.len()); - for (service, _) in INTERACTION_SERVICES { + let mut threads = Vec::with_capacity(COMPONENT_SESSION_SERVICES.len()); + for (service, _) in COMPONENT_SESSION_SERVICES { let slug = service.replace('.', "-"); let path = state_dir.join(format!("interaction-{slug}.sock")); let listener = bind_interaction_listener(&path, expected_peer_uid) @@ -4445,19 +4636,26 @@ where .and_then(|set| set.runtime_for_mut(&zone)) .ok_or_else(|| "interaction runtime unavailable".to_owned())?; let registered = composition - .admit_and_register(acceptor, engine, evidence, 1) + .admit_and_register_for_service(acceptor, engine, evidence, 1, &service) .await .map_err(|error| error.to_string())?; let session_key = registered.session_key(); - (session_key, registered.request_receiver()) + let session_driver = registered + .component_session_driver() + .ok_or_else(|| "interaction session driver unavailable".to_owned())?; + (session_key, registered.request_receiver(), session_driver) }; - let (session_key, request_receiver) = request_receiver; + let (session_key, request_receiver, session_driver) = request_receiver; loop { let frame = tokio::select! { frame = request_receiver.recv() => match frame { Ok(frame) => frame, Err(_) => break, }, + control = session_driver.receive_control() => match control { + Ok(d2b_session::SessionEvent::Close(_)) | Err(_) => break, + Ok(_) => continue, + }, _ = tokio::time::sleep(Duration::from_millis(50)) => { if stop.load(Ordering::Acquire) { break; @@ -4862,6 +5060,18 @@ mod tests { policy.transport_binding.transport, TransportClass::UnixSeqpacket, ); + assert_eq!( + interaction_endpoint_policy(PROCESS_ATTACH_SERVICE, 7) + .expect("Process attach policy") + .service, + ServicePackage::ProviderV3, + ); + assert_eq!( + interaction_endpoint_policy(SHELL_SESSION_SERVICE, 7) + .expect("shell supervisor policy") + .service, + ServicePackage::ProviderV3, + ); } #[test] @@ -4889,6 +5099,247 @@ mod tests { assert!(!path.exists()); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn process_session_lookup_owns_named_stream_and_disconnects_fail_closed() { + let directory = tempfile::tempdir().unwrap(); + let zone = ZoneId::parse("work").unwrap(); + let uid = nix::unistd::getuid().as_raw(); + let runtime = Arc::new(AsyncMutex::new(Some(test_interaction_runtime(&zone, uid)))); + let display_path = directory.path().join("display.sock"); + let display_listener = bind_interaction_listener(&display_path, uid).unwrap(); + let (display_client, display_server) = establish_test_client( + &display_listener, + &runtime, + &zone, + d2b_provider_display_wayland::SERVICE_PACKAGE, + uid, + &display_path, + ) + .await; + let process_path = directory.path().join("process.sock"); + let process_listener = bind_interaction_listener(&process_path, uid).unwrap(); + let (process_client, process_server) = establish_test_client( + &process_listener, + &runtime, + &zone, + PROCESS_ATTACH_SERVICE, + uid, + &process_path, + ) + .await; + for _ in 0..50 { + if runtime + .lock() + .await + .as_ref() + .and_then(|set| set.runtime_for(&zone)) + .is_some_and(|composition| composition.session_count() == 2) + { + break; + } + tokio::time::sleep(Duration::from_millis(1)).await; + } + let target = ResourceRef::parse(&format!("Guest/uid-{uid}")).unwrap(); + let display_target = ResourceRef::parse("Host/host-system").unwrap(); + let missing = ResourceRef::parse("Guest/missing").unwrap(); + let guard = runtime.lock().await; + let composition = guard + .as_ref() + .and_then(|set| set.runtime_for(&zone)) + .expect("runtime"); + assert!( + composition + .component_session_driver_for_target(PROCESS_ATTACH_SERVICE, &target,) + .is_some(), + "the enrolled Process session must expose its real driver" + ); + assert!( + composition + .component_session_driver_for_target( + d2b_provider_display_wayland::SERVICE_PACKAGE, + &target, + ) + .is_none(), + "the display session must not own the Process target" + ); + assert!( + composition + .component_session_driver_for_target(PROCESS_ATTACH_SERVICE, &display_target,) + .is_none(), + "a display-only target must not be treated as a Process source" + ); + assert!( + composition + .component_session_driver_for_target(PROCESS_ATTACH_SERVICE, &missing,) + .is_none(), + "an absent target must remain fail-closed" + ); + drop(guard); + + let stream = d2b_session::StreamId::new(0x101).unwrap(); + process_client + .open_named_stream(stream, 64, 64) + .await + .unwrap(); + display_client + .open_named_stream(stream, 64, 64) + .await + .unwrap(); + let process_driver = { + let guard = runtime.lock().await; + guard + .as_ref() + .and_then(|set| set.runtime_for(&zone)) + .and_then(|composition| { + composition.component_session_driver_for_target(PROCESS_ATTACH_SERVICE, &target) + }) + .expect("Process session driver") + }; + process_driver + .open_named_stream(stream, 64, 64) + .await + .unwrap(); + process_driver + .send_named_stream(stream, b"process-owner".to_vec()) + .await + .unwrap(); + let event = tokio::time::timeout( + Duration::from_secs(1), + process_client.receive_named_stream(), + ) + .await + .expect("Process stream data timeout") + .unwrap(); + assert!(matches!( + event, + d2b_session::StreamEvent::Data { + stream: received, + bytes + } if received == stream && bytes == b"process-owner" + )); + assert!( + tokio::time::timeout( + Duration::from_millis(50), + display_client.receive_named_stream(), + ) + .await + .is_err(), + "display interaction session must not receive Process data" + ); + + process_driver.reset_named_stream(stream).await.unwrap(); + let event = tokio::time::timeout( + Duration::from_secs(1), + process_client.receive_named_stream(), + ) + .await + .expect("Process stream reset timeout") + .unwrap(); + assert!(matches!( + event, + d2b_session::StreamEvent::Reset { stream: received } if received == stream + )); + + drop(process_client); + for _ in 0..1_000 { + if runtime + .lock() + .await + .as_ref() + .and_then(|set| set.runtime_for(&zone)) + .is_some_and(|composition| composition.session_count() == 1) + { + break; + } + tokio::time::sleep(Duration::from_millis(1)).await; + } + let guard = runtime.lock().await; + let composition = guard + .as_ref() + .and_then(|set| set.runtime_for(&zone)) + .expect("runtime"); + assert!( + composition + .component_session_driver_for_target(PROCESS_ATTACH_SERVICE, &target) + .is_none(), + "a disconnected Process source must be removed rather than reused" + ); + drop(guard); + display_server.abort(); + process_server.abort(); + drop(display_listener); + drop(process_listener); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn process_session_lookup_waits_for_runtime_contention() { + let directory = tempfile::tempdir().unwrap(); + let zone = ZoneId::parse("work").unwrap(); + let uid = nix::unistd::getuid().as_raw(); + let runtime = Arc::new(AsyncMutex::new(Some(test_interaction_runtime(&zone, uid)))); + let path = directory.path().join("process.sock"); + let listener = bind_interaction_listener(&path, uid).unwrap(); + let (client, server) = establish_test_client( + &listener, + &runtime, + &zone, + PROCESS_ATTACH_SERVICE, + uid, + &path, + ) + .await; + for _ in 0..50 { + if runtime + .lock() + .await + .as_ref() + .and_then(|set| set.runtime_for(&zone)) + .is_some_and(|composition| composition.session_count() == 1) + { + break; + } + tokio::time::sleep(Duration::from_millis(1)).await; + } + let target = ResourceRef::parse(&format!("Guest/uid-{uid}")).unwrap(); + let guard = runtime.lock().await; + let lookup_runtime = Arc::clone(&runtime); + let mut lookup = tokio::spawn(async move { + tokio::task::spawn_blocking(move || { + blocking_component_session_driver_for_service( + &lookup_runtime, + PROCESS_ATTACH_SERVICE, + &target, + ) + }) + .await + .unwrap() + }); + assert!( + tokio::time::timeout(Duration::from_millis(20), &mut lookup) + .await + .is_err(), + "runtime contention must not be reported as an absent source" + ); + drop(guard); + assert!( + tokio::time::timeout(Duration::from_secs(1), &mut lookup) + .await + .expect("lookup release timeout") + .expect("lookup task failed") + .is_some(), + "the enrolled Process source must resolve after contention clears" + ); + client + .close( + d2b_contracts_zone_session::v3::component_session::CloseReason::Normal, + d2b_contracts_zone_session::v3::component_session::Remediation::None, + ) + .await + .unwrap(); + server.abort(); + drop(listener); + } + fn test_interaction_composition( zone: &ZoneId, uid: u32, @@ -4982,10 +5433,8 @@ mod tests { policy_revision: display_generation, api_catalog_revision: 1, active_configuration_revision: - d2b_contracts_resource::v3::ConfigurationGeneration::new( - display_generation, - ) - .unwrap(), + d2b_contracts_resource::v3::ConfigurationGeneration::new(display_generation) + .unwrap(), controller_generation: Some( d2b_contracts_resource::v3::ControllerGeneration::new( controller_generation, @@ -5038,10 +5487,8 @@ mod tests { policy_revision: display_generation, api_catalog_revision: 1, active_configuration_revision: - d2b_contracts_resource::v3::ConfigurationGeneration::new( - display_generation, - ) - .unwrap(), + d2b_contracts_resource::v3::ConfigurationGeneration::new(display_generation) + .unwrap(), controller_generation: Some( d2b_contracts_resource::v3::ControllerGeneration::new( controller_generation, diff --git a/packages/d2bd/src/resource_runtime.rs b/packages/d2bd/src/resource_runtime.rs index 411b864275..f31754c5af 100644 --- a/packages/d2bd/src/resource_runtime.rs +++ b/packages/d2bd/src/resource_runtime.rs @@ -1318,6 +1318,15 @@ impl ZoneResourceRuntime { Ok(Arc::new(adapter.client())) } + /// Borrow the daemon-owned Resource API client used by the target-local + /// process reconciler. The client is present only after the Zone's + /// authenticated system-core session has been enrolled. + pub(crate) fn process_resource_client( + &self, + ) -> Option>> { + self.process_status_client.clone() + } + /// Persist a provider reconcile phase through the authenticated Resource /// API so restart admission can rely on durable observed generation. pub(crate) async fn persist_public_reconcile_phase( From 845e7033840fd46b24671d7578b7f23ab43aa37c Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 09:11:01 -0700 Subject: [PATCH 11/56] fix(controller): scope test-only imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/d2b-core-controller/src/api_catalog.rs | 13 +++++-------- packages/d2b-core-controller/src/providers.rs | 11 +++++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/d2b-core-controller/src/api_catalog.rs b/packages/d2b-core-controller/src/api_catalog.rs index b843d3ed16..cef55b8c89 100644 --- a/packages/d2b-core-controller/src/api_catalog.rs +++ b/packages/d2b-core-controller/src/api_catalog.rs @@ -2,11 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use d2b_contracts_provider::v3::{ - ArtifactDigest, BinaryRef, ComponentExecution, ComponentTargetCapability, - ControllerInstanceScope, ControllerTargetKind, EffectPortClass, ProviderManifest, - ResourceApiBinding, TargetRuntimeArtifacts, -}; +use d2b_contracts_provider::v3::{ProviderManifest, ResourceApiBinding}; use d2b_contracts_resource::v3::{ResourceRef, ResourceTypeName, SchemaFingerprint}; /// Closed reason a catalog candidate was refused. @@ -251,9 +247,10 @@ impl ApiCatalogHandler { mod tests { use d2b_contracts_provider::v3::UpgradePolicy as ProviderUpgradePolicy; use d2b_contracts_provider::v3::{ - ArtifactDigest, ArtifactDigestSet, CompatibilityRange, ComponentDescriptor, ComponentType, - PolicyEvaluation, RevocationState, SignatureState, StandardCapabilityMatrix, TrustEvidence, - UpgradeDisposition, + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ComponentType, ControllerInstanceScope, + ControllerTargetKind, EffectPortClass, PolicyEvaluation, RevocationState, SignatureState, + StandardCapabilityMatrix, TargetRuntimeArtifacts, TrustEvidence, UpgradeDisposition, }; use d2b_contracts_resource::v3::{ ArtifactId, SchemaVersion, diff --git a/packages/d2b-core-controller/src/providers.rs b/packages/d2b-core-controller/src/providers.rs index caca608ab9..4d20a1ad83 100644 --- a/packages/d2b-core-controller/src/providers.rs +++ b/packages/d2b-core-controller/src/providers.rs @@ -1,9 +1,6 @@ //! Provider lifecycle validation and child-resource planning. -use d2b_contracts_provider::v3::{ - BinaryRef, ComponentExecution, ComponentTargetCapability, ComponentType, ControllerTargetKind, - EffectPortClass, ProviderManifest, TargetRuntimeArtifacts, -}; +use d2b_contracts_provider::v3::{ComponentType, ProviderManifest}; use d2b_contracts_resource::v3::{ResourceRef, SchemaFingerprint}; /// Provider lifecycle phase derived from exact child observations. @@ -212,8 +209,10 @@ impl ProviderHandler { mod tests { use d2b_contracts_provider::v3::UpgradePolicy as ProviderUpgradePolicy; use d2b_contracts_provider::v3::{ - ArtifactDigest, ArtifactDigestSet, CompatibilityRange, ComponentDescriptor, - PolicyEvaluation, RevocationState, SignatureState, TrustEvidence, UpgradeDisposition, + ArtifactDigest, ArtifactDigestSet, BinaryRef, CompatibilityRange, ComponentDescriptor, + ComponentExecution, ComponentTargetCapability, ControllerTargetKind, EffectPortClass, + PolicyEvaluation, RevocationState, SignatureState, TargetRuntimeArtifacts, TrustEvidence, + UpgradeDisposition, }; use d2b_contracts_resource::v3::{ ArtifactId, From f38e5c4754c991a1b541d11ca942d323d3ea8cca Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 09:29:23 -0700 Subject: [PATCH 12/56] policy: refresh U5 production closure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 2 +- .../broker-production/production/closure.json | 2 +- .../production/metadata.json | 2 +- .../main-product/policy/Cargo.lock | 4 +++ .../main-product/policy/closure.json | 26 ++++++++++++++++++- .../main-product/policy/metadata.json | 2 +- .../main-product/production/Cargo.lock | 4 +++ .../main-product/production/closure.json | 26 ++++++++++++++++++- .../main-product/production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 2 +- .../broker-production/production/closure.json | 2 +- .../production/metadata.json | 2 +- .../main-product/policy/Cargo.lock | 4 +++ .../main-product/policy/closure.json | 26 ++++++++++++++++++- .../main-product/policy/metadata.json | 2 +- .../main-product/production/Cargo.lock | 4 +++ .../main-product/production/closure.json | 26 ++++++++++++++++++- .../main-product/production/metadata.json | 2 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 2 +- .../production/closure.json | 2 +- .../production/metadata.json | 2 +- 52 files changed, 160 insertions(+), 48 deletions(-) diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json index f6d9cdadc0..fbed6c7579 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 832eed4fbf..9f9640262a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json index 6346ff91c3..6a13398377 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 832eed4fbf..9f9640262a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index b4bd13f7ed..57bb6055d4 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index 855ed6eb62..64a2b89aec 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index 2fa5a7f620..26744edf47 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index 855ed6eb62..64a2b89aec 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 10fb75a888..30612c3b7d 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index 03c8bcf216..7500f6f592 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index 83469e79aa..66ff57b9d1 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index 03c8bcf216..7500f6f592 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json index e2051b3fa1..a5e6a8e2ae 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json index 832eed4fbf..9f9640262a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json index 69bfa89266..4a4834adfa 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json index 832eed4fbf..9f9640262a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock index 2cebf75db9..2f04cd320a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock @@ -1647,11 +1647,13 @@ dependencies = [ name = "d2b-provider-system-systemd" version = "0.0.0-bootstrap" dependencies = [ + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-process-conformance", "d2b-provider-toolkit", "serde", + "serde_json", "tokio", ] @@ -1784,9 +1786,11 @@ name = "d2b-resource-client" version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core-controller", + "serde_json", "tokio", ] diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json index 96cf25d39c..2b0af1fafb 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json @@ -72,7 +72,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -6718,6 +6718,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -6748,6 +6754,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", @@ -7216,6 +7228,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -7240,6 +7258,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json index 7f2f5f00f0..eca8489709 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 420, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock index 2cebf75db9..2f04cd320a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock @@ -1647,11 +1647,13 @@ dependencies = [ name = "d2b-provider-system-systemd" version = "0.0.0-bootstrap" dependencies = [ + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-process-conformance", "d2b-provider-toolkit", "serde", + "serde_json", "tokio", ] @@ -1784,9 +1786,11 @@ name = "d2b-resource-client" version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core-controller", + "serde_json", "tokio", ] diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json index 5a829e9ec1..905bfe9181 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json @@ -72,7 +72,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -6364,6 +6364,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -6394,6 +6400,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", @@ -6808,6 +6820,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -6832,6 +6850,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json index 7f2f5f00f0..eca8489709 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 420, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index 770bb45995..814995c53d 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 4b93ff6d9e..6915d7311a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index abda343c6c..70d6d10eb3 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 4b93ff6d9e..6915d7311a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json index 0800c66e72..a5cb94ede5 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 49faecdbd3..fa3f908c62 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json index 1da9b8f0f1..05873db0c5 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 49faecdbd3..fa3f908c62 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index 7a2f5a2b3d..2cef90d186 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index 82ee3fbe0d..e940d11e77 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index 6e2d490248..b52d817e26 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index 82ee3fbe0d..e940d11e77 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 0654cc7bd6..1d31ac1151 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index 35c7429596..7a1f6a33ed 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index 8494ca69ad..af9c0b3234 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index 35c7429596..7a1f6a33ed 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json index f7aacb8b97..e8d0920679 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json index 49faecdbd3..fa3f908c62 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json index 53202f782e..b84e1f2a9c 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json index 49faecdbd3..fa3f908c62 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock index 2cebf75db9..2f04cd320a 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock @@ -1647,11 +1647,13 @@ dependencies = [ name = "d2b-provider-system-systemd" version = "0.0.0-bootstrap" dependencies = [ + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-process-conformance", "d2b-provider-toolkit", "serde", + "serde_json", "tokio", ] @@ -1784,9 +1786,11 @@ name = "d2b-resource-client" version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core-controller", + "serde_json", "tokio", ] diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json index b340cb24e6..e9cacd8b6b 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json @@ -72,7 +72,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -6770,6 +6770,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -6800,6 +6806,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", @@ -7268,6 +7280,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -7292,6 +7310,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json index f7ef9a0bf7..d4725881d2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 422, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock index 2cebf75db9..2f04cd320a 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock @@ -1647,11 +1647,13 @@ dependencies = [ name = "d2b-provider-system-systemd" version = "0.0.0-bootstrap" dependencies = [ + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-process-conformance", "d2b-provider-toolkit", "serde", + "serde_json", "tokio", ] @@ -1784,9 +1786,11 @@ name = "d2b-resource-client" version = "0.0.0-bootstrap" dependencies = [ "d2b-contracts", + "d2b-contracts-control", "d2b-contracts-resource", "d2b-contracts-zone-session", "d2b-core-controller", + "serde_json", "tokio", ] diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json index 31e8856aef..0b15d1f30e 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json @@ -72,7 +72,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -6416,6 +6416,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -6446,6 +6452,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-system-systemd@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", @@ -6860,6 +6872,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -6884,6 +6902,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-resource-client@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-resource-client@0.0.0-bootstrap#path", "to": "tokio@1.53.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json index f7ef9a0bf7..d4725881d2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 422, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index f40ca32d01..5b1fa4dd92 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 04fba978f8..594df22b47 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index c1d1ae7baa..6a8f6956fd 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 04fba978f8..594df22b47 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "1d63a78788753f4cfb78773614bb08175945eda90d3d396158a587feff9ac8e8", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", From bfcf0921353ac5dcc2fd72a3b235be96304de61e Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 09:55:24 -0700 Subject: [PATCH 13/56] bazel: preserve local test action PATH Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- changelog.d/fix-bazel-test-runner-path.md | 4 ++++ packages/xtask/tests/buildbuddy_config.rs | 4 ++++ tests/golden/bazel/cache-policy.json | 2 +- tests/tools/bazel-check | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fix-bazel-test-runner-path.md diff --git a/changelog.d/fix-bazel-test-runner-path.md b/changelog.d/fix-bazel-test-runner-path.md new file mode 100644 index 0000000000..fd516d22f4 --- /dev/null +++ b/changelog.d/fix-bazel-test-runner-path.md @@ -0,0 +1,4 @@ +### Fixed + +- Preserve the pinned development-shell `PATH` for Bazel test-runner actions + so local Layer-1 tests can resolve their declared shell tools on NixOS. diff --git a/packages/xtask/tests/buildbuddy_config.rs b/packages/xtask/tests/buildbuddy_config.rs index f3027e1212..4f17256779 100644 --- a/packages/xtask/tests/buildbuddy_config.rs +++ b/packages/xtask/tests/buildbuddy_config.rs @@ -426,6 +426,10 @@ fn committed_profiles_share_authentication_and_worker_policy() { wrapper.contains("--test_env=D2B_SHELLCHECK_BIN=\"${D2B_SHELLCHECK_BIN:-}\""), "source-hygiene tests must receive the declared shellcheck binary" ); + assert!( + wrapper.contains("--action_env=PATH=\"$test_path\""), + "Bazel test-runner actions must resolve pinned shell tools through the computed test PATH" + ); let flake = read_text("flake.nix"); assert!( flake.contains("export D2B_SHELLCHECK_BIN=\"${pkgs.shellcheck}/bin/shellcheck\""), diff --git a/tests/golden/bazel/cache-policy.json b/tests/golden/bazel/cache-policy.json index 640a230961..96d1f4b3d8 100644 --- a/tests/golden/bazel/cache-policy.json +++ b/tests/golden/bazel/cache-policy.json @@ -75,7 +75,7 @@ "tests/tools/bazel-check" ], "allowedSecurityDigests": [ - "sha256:128ade7a73c07c191edd5015d611f389f70500aa23bdc1049fae895fa6369e46" + "sha256:7874ff535e4c843934e2b9f0e9bc2c6ece0f3d361c869b82aa429fc39ebe6f8d" ], "untrustedCredential": "none", "trustedCredential": "credential-helper" diff --git a/tests/tools/bazel-check b/tests/tools/bazel-check index f14c103325..2acee0fb18 100755 --- a/tests/tools/bazel-check +++ b/tests/tools/bazel-check @@ -410,6 +410,7 @@ run_bazel() { --test_env=D2B_BAZEL_BIN="$bazel_bin" \ --test_env=D2B_PROJECT_SHELL=d2b \ --test_env=D2B_SHELLCHECK_BIN="${D2B_SHELLCHECK_BIN:-}" \ + --action_env=PATH="$test_path" \ --test_env=PATH="$test_path" \ --test_env=BAZEL_SH="$test_bazel_sh" \ "$@" \ From d741375b5034d79164daaa4d9c97e0534915d2b5 Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 10:42:23 -0700 Subject: [PATCH 14/56] bazel: keep host xtask tests local Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bazel/checks/rust/BUILD.bazel | 3 +++ changelog.d/fix-bazel-test-runner-path.md | 5 +++-- packages/xtask/BUILD.bazel | 5 +++-- packages/xtask/tests/buildbuddy_config.rs | 16 ++++++++++++---- tests/golden/bazel/cache-policy.json | 2 +- tests/tools/bazel-check | 1 - 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/bazel/checks/rust/BUILD.bazel b/bazel/checks/rust/BUILD.bazel index e0e9648f24..075b5b6528 100644 --- a/bazel/checks/rust/BUILD.bazel +++ b/bazel/checks/rust/BUILD.bazel @@ -54,6 +54,9 @@ test_suite( "//packages/d2b-bus:production_watch_rss", "//packages/xtask:buildbuddy_config", "//packages/xtask:heavy_gate_reexec", + "//packages/xtask:policy_changelog_gate", + "//packages/xtask:policy_production_closure", + "//packages/xtask:xtask_test", ], ) diff --git a/changelog.d/fix-bazel-test-runner-path.md b/changelog.d/fix-bazel-test-runner-path.md index fd516d22f4..63872c0dcc 100644 --- a/changelog.d/fix-bazel-test-runner-path.md +++ b/changelog.d/fix-bazel-test-runner-path.md @@ -1,4 +1,5 @@ ### Fixed -- Preserve the pinned development-shell `PATH` for Bazel test-runner actions - so local Layer-1 tests can resolve their declared shell tools on NixOS. +- Route host-coupled xtask tests to local test execution so they can resolve + repository and tool state without changing the worker-standard `PATH` for + remote actions. diff --git a/packages/xtask/BUILD.bazel b/packages/xtask/BUILD.bazel index 6874614691..a44d7a05c2 100644 --- a/packages/xtask/BUILD.bazel +++ b/packages/xtask/BUILD.bazel @@ -325,7 +325,7 @@ rust_test( "//bazel/checks/rust:changelog_sources", ], env_inherit = ["D2B_REPO_ROOT", "PATH"], - tags = ["no-remote-cache", "no-sandbox"], + tags = ["no-remote-cache", "no-sandbox", "no-remote-exec"], visibility = ["//visibility:public"], deps = all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -353,7 +353,7 @@ rust_test( "//:policy_inputs_workspace_sources", ], env_inherit = ["D2B_REPO_ROOT", "PATH"], - tags = ["no-remote-cache", "no-sandbox"], + tags = ["no-remote-cache", "no-sandbox", "no-remote-exec"], visibility = ["//visibility:public"], deps = all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -378,6 +378,7 @@ rust_test( tags = [ "no-remote-cache", "no-sandbox", + "no-remote-exec", ], deps = ["//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-contracts-provider:d2b_contracts_provider",] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), diff --git a/packages/xtask/tests/buildbuddy_config.rs b/packages/xtask/tests/buildbuddy_config.rs index 4f17256779..a80cb66b49 100644 --- a/packages/xtask/tests/buildbuddy_config.rs +++ b/packages/xtask/tests/buildbuddy_config.rs @@ -426,10 +426,18 @@ fn committed_profiles_share_authentication_and_worker_policy() { wrapper.contains("--test_env=D2B_SHELLCHECK_BIN=\"${D2B_SHELLCHECK_BIN:-}\""), "source-hygiene tests must receive the declared shellcheck binary" ); - assert!( - wrapper.contains("--action_env=PATH=\"$test_path\""), - "Bazel test-runner actions must resolve pinned shell tools through the computed test PATH" - ); + let xtask_build = read_text("packages/xtask/BUILD.bazel"); + for target in [ + "policy_changelog_gate", + "policy_production_closure", + "xtask_test", + ] { + let block = rule_block(&xtask_build, target, &["rust_test("]); + assert!( + rule_tags(block).contains("no-remote-exec"), + "host-coupled xtask target {target} must opt out of remote execution" + ); + } let flake = read_text("flake.nix"); assert!( flake.contains("export D2B_SHELLCHECK_BIN=\"${pkgs.shellcheck}/bin/shellcheck\""), diff --git a/tests/golden/bazel/cache-policy.json b/tests/golden/bazel/cache-policy.json index 96d1f4b3d8..640a230961 100644 --- a/tests/golden/bazel/cache-policy.json +++ b/tests/golden/bazel/cache-policy.json @@ -75,7 +75,7 @@ "tests/tools/bazel-check" ], "allowedSecurityDigests": [ - "sha256:7874ff535e4c843934e2b9f0e9bc2c6ece0f3d361c869b82aa429fc39ebe6f8d" + "sha256:128ade7a73c07c191edd5015d611f389f70500aa23bdc1049fae895fa6369e46" ], "untrustedCredential": "none", "trustedCredential": "credential-helper" diff --git a/tests/tools/bazel-check b/tests/tools/bazel-check index 2acee0fb18..f14c103325 100755 --- a/tests/tools/bazel-check +++ b/tests/tools/bazel-check @@ -410,7 +410,6 @@ run_bazel() { --test_env=D2B_BAZEL_BIN="$bazel_bin" \ --test_env=D2B_PROJECT_SHELL=d2b \ --test_env=D2B_SHELLCHECK_BIN="${D2B_SHELLCHECK_BIN:-}" \ - --action_env=PATH="$test_path" \ --test_env=PATH="$test_path" \ --test_env=BAZEL_SH="$test_bazel_sh" \ "$@" \ From 2542261c90a5b7a85d550c11637b78295ed97f6d Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 10:56:27 -0700 Subject: [PATCH 15/56] test(bazel): guard remote action PATH Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/xtask/tests/buildbuddy_config.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/xtask/tests/buildbuddy_config.rs b/packages/xtask/tests/buildbuddy_config.rs index a80cb66b49..ea479d5f56 100644 --- a/packages/xtask/tests/buildbuddy_config.rs +++ b/packages/xtask/tests/buildbuddy_config.rs @@ -422,6 +422,10 @@ fn committed_profiles_share_authentication_and_worker_policy() { ), "remote test runners must receive worker-standard PATH entries" ); + assert!( + !wrapper.contains(r#"--action_env=PATH="$test_path""#), + "remote actions must retain the worker-standard PATH" + ); assert!( wrapper.contains("--test_env=D2B_SHELLCHECK_BIN=\"${D2B_SHELLCHECK_BIN:-}\""), "source-hygiene tests must receive the declared shellcheck binary" From 928fdaf55127a13957834eec808ff6c7a132c64b Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 11:34:48 -0700 Subject: [PATCH 16/56] bazel: drop invalid xtask routing experiment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bazel/checks/rust/BUILD.bazel | 3 --- changelog.d/fix-bazel-test-runner-path.md | 5 ----- packages/xtask/BUILD.bazel | 5 ++--- packages/xtask/tests/buildbuddy_config.rs | 16 ---------------- 4 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 changelog.d/fix-bazel-test-runner-path.md diff --git a/bazel/checks/rust/BUILD.bazel b/bazel/checks/rust/BUILD.bazel index 075b5b6528..e0e9648f24 100644 --- a/bazel/checks/rust/BUILD.bazel +++ b/bazel/checks/rust/BUILD.bazel @@ -54,9 +54,6 @@ test_suite( "//packages/d2b-bus:production_watch_rss", "//packages/xtask:buildbuddy_config", "//packages/xtask:heavy_gate_reexec", - "//packages/xtask:policy_changelog_gate", - "//packages/xtask:policy_production_closure", - "//packages/xtask:xtask_test", ], ) diff --git a/changelog.d/fix-bazel-test-runner-path.md b/changelog.d/fix-bazel-test-runner-path.md deleted file mode 100644 index 63872c0dcc..0000000000 --- a/changelog.d/fix-bazel-test-runner-path.md +++ /dev/null @@ -1,5 +0,0 @@ -### Fixed - -- Route host-coupled xtask tests to local test execution so they can resolve - repository and tool state without changing the worker-standard `PATH` for - remote actions. diff --git a/packages/xtask/BUILD.bazel b/packages/xtask/BUILD.bazel index a44d7a05c2..6874614691 100644 --- a/packages/xtask/BUILD.bazel +++ b/packages/xtask/BUILD.bazel @@ -325,7 +325,7 @@ rust_test( "//bazel/checks/rust:changelog_sources", ], env_inherit = ["D2B_REPO_ROOT", "PATH"], - tags = ["no-remote-cache", "no-sandbox", "no-remote-exec"], + tags = ["no-remote-cache", "no-sandbox"], visibility = ["//visibility:public"], deps = all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -353,7 +353,7 @@ rust_test( "//:policy_inputs_workspace_sources", ], env_inherit = ["D2B_REPO_ROOT", "PATH"], - tags = ["no-remote-cache", "no-sandbox", "no-remote-exec"], + tags = ["no-remote-cache", "no-sandbox"], visibility = ["//visibility:public"], deps = all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) @@ -378,7 +378,6 @@ rust_test( tags = [ "no-remote-cache", "no-sandbox", - "no-remote-exec", ], deps = ["//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-contracts-provider:d2b_contracts_provider",] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), diff --git a/packages/xtask/tests/buildbuddy_config.rs b/packages/xtask/tests/buildbuddy_config.rs index ea479d5f56..f3027e1212 100644 --- a/packages/xtask/tests/buildbuddy_config.rs +++ b/packages/xtask/tests/buildbuddy_config.rs @@ -422,26 +422,10 @@ fn committed_profiles_share_authentication_and_worker_policy() { ), "remote test runners must receive worker-standard PATH entries" ); - assert!( - !wrapper.contains(r#"--action_env=PATH="$test_path""#), - "remote actions must retain the worker-standard PATH" - ); assert!( wrapper.contains("--test_env=D2B_SHELLCHECK_BIN=\"${D2B_SHELLCHECK_BIN:-}\""), "source-hygiene tests must receive the declared shellcheck binary" ); - let xtask_build = read_text("packages/xtask/BUILD.bazel"); - for target in [ - "policy_changelog_gate", - "policy_production_closure", - "xtask_test", - ] { - let block = rule_block(&xtask_build, target, &["rust_test("]); - assert!( - rule_tags(block).contains("no-remote-exec"), - "host-coupled xtask target {target} must opt out of remote execution" - ); - } let flake = read_text("flake.nix"); assert!( flake.contains("export D2B_SHELLCHECK_BIN=\"${pkgs.shellcheck}/bin/shellcheck\""), From 5fd4c55f4013222f3fa499d30d9afdde45dac5ee Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 12:07:10 -0700 Subject: [PATCH 17/56] runtime: gate retired detached create path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/d2bd-runtime/src/exec_detached.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/d2bd-runtime/src/exec_detached.rs b/packages/d2bd-runtime/src/exec_detached.rs index f0db3d2cba..9cdc9d68d7 100644 --- a/packages/d2bd-runtime/src/exec_detached.rs +++ b/packages/d2bd-runtime/src/exec_detached.rs @@ -49,6 +49,7 @@ pub trait DetachedExecContext { // waits up to 10 s for the runner to write its status file // (`CREATE_TIMEOUT_MS`); 20 s covers that window plus scheduling slack, // consistent with `exec_session_real::ESTABLISH_TIMEOUT`. +#[cfg(any(test, feature = "test-support"))] const DETACHED_CREATE_DEADLINE: Duration = Duration::from_secs(20); const DETACHED_CANCEL_DEADLINE: Duration = Duration::from_secs(30); #[cfg(test)] @@ -198,6 +199,7 @@ use std::sync::{Arc, Mutex, OnceLock}; #[derive(Debug)] enum DetachedRealRequest { + #[cfg(any(test, feature = "test-support"))] Create(ExecStartSpec), List, Status { @@ -216,6 +218,7 @@ enum DetachedRealRequest { #[derive(Debug)] enum DetachedRealResponse { + #[cfg(any(test, feature = "test-support"))] Create(ExecDetachedCreateResult), List(ExecDetachedListResult), Status(ExecDetachedStatusResult), @@ -357,6 +360,7 @@ pub fn create_idempotent_as( create_with_request_id(state, start, Some(request_id), caller_role) } +#[cfg(any(test, feature = "test-support"))] fn create_with_request_id( state: &impl DetachedExecContext, start: &public_wire::ExecStartArgs, @@ -692,6 +696,7 @@ fn run_detached_request( crate::runtime_util::block_on_future(async move { let client = DetachedClient::connect(params, broker_socket, caller_role).await?; match request { + #[cfg(any(test, feature = "test-support"))] DetachedRealRequest::Create(spec) => client .exec_create(&spec) .await @@ -731,6 +736,7 @@ fn internal_error(detail: impl Into) -> TypedError { #[async_trait] trait DetachedGuestControlRpc: Send + Sync { + #[cfg(any(test, feature = "test-support"))] async fn exec_create( &self, request: pb::ExecCreateRequest, @@ -764,6 +770,7 @@ trait DetachedGuestControlRpc: Send + Sync { #[async_trait] impl DetachedGuestControlRpc for TtrpcGuestControlClient { + #[cfg(any(test, feature = "test-support"))] async fn exec_create( &self, request: pb::ExecCreateRequest, @@ -850,6 +857,7 @@ impl DetachedClient where C: DetachedGuestControlRpc, { + #[cfg(any(test, feature = "test-support"))] async fn exec_create( &self, spec: &ExecStartSpec, From e89653d523548cdac7169a992460ba30a9c10caa Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 12:34:41 -0700 Subject: [PATCH 18/56] d2bd: gate retired exec bridge helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/d2bd/src/composition.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/d2bd/src/composition.rs b/packages/d2bd/src/composition.rs index 0ee45ee4ef..ed96e975da 100644 --- a/packages/d2bd/src/composition.rs +++ b/packages/d2bd/src/composition.rs @@ -202,15 +202,17 @@ pub use d2bd_runtime::daemon_config::{ storage_lifecycle_report_path, }; pub use d2bd_runtime::exec_detached; +#[cfg(test)] pub(crate) use d2bd_runtime::exec_owner_io; pub use d2bd_runtime::exec_session; use d2bd_runtime::exec_session::ExecGuestConnector; pub use d2bd_runtime::exec_session_real; #[cfg(test)] use d2bd_runtime::exec_support::map_exec_op_error; +pub(crate) use d2bd_runtime::exec_support::map_exec_reserve_error; +#[cfg(test)] pub(crate) use d2bd_runtime::exec_support::{ emit_exec_established_event, exec_error_kind_label, exec_metric_into, map_exec_establish_error, - map_exec_reserve_error, }; pub use d2bd_runtime::guest_control_bridge; pub use d2bd_runtime::runtime_capability::{ @@ -227,10 +229,14 @@ use d2bd_runtime::runtime_util::{ block_on_future, duplicate_received_fd, hex_bytes, projection_digest_bytes, }; use d2bd_runtime::shell_backend::{ - SHELL_MANAGEMENT_TIMEOUT, guest_advertises_capability, map_shell_attach_response, - map_shell_detach_response, map_shell_health_error, map_shell_kill_response, - map_shell_list_response, shell_capability_failed, shell_error_to_typed, shell_failed, - shell_poll_timeout, shell_protocol_failed, shell_transport_failed, + SHELL_MANAGEMENT_TIMEOUT, shell_capability_failed, shell_failed, shell_protocol_failed, + shell_transport_failed, +}; +#[cfg(test)] +use d2bd_runtime::shell_backend::{ + guest_advertises_capability, map_shell_attach_response, map_shell_detach_response, + map_shell_health_error, map_shell_kill_response, map_shell_list_response, shell_error_to_typed, + shell_poll_timeout, }; #[cfg(test)] use d2bd_runtime::shell_backend::{SHELL_POLL_CAP, SHELL_POLL_SLACK}; @@ -13839,6 +13845,7 @@ fn shell_close_attach_with_runtime( /// Increment the closed-label exec outcome counter. `outcome` and `error_kind` /// are the only labels besides the constant subsystem; all three are drawn /// from a hard allowlist (no vm/uid/handle/argv ever becomes a label). +#[cfg(test)] fn exec_metric(state: &ServerState, outcome: &'static str, error_kind: &'static str) { exec_metric_into(&state.metrics_registry, outcome, error_kind); } @@ -13860,6 +13867,7 @@ fn emit_detached_create_audit(state: &ServerState, peer_uid: u32, vm: &str, exec } } +#[cfg(test)] fn emit_detached_kill_audit( state: &ServerState, peer_uid: u32, From 184d33a349535c4081fa1d63971546ccd33f562b Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sat, 22 Aug 2026 13:11:59 -0700 Subject: [PATCH 19/56] bazel: preserve local action PATH Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../process-shell-component-session.md | 5 +++++ packages/xtask/tests/buildbuddy_config.rs | 21 ++++++++++++++++++- tests/golden/bazel/cache-policy.json | 2 +- tests/tools/bazel-check | 2 ++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/changelog.d/process-shell-component-session.md b/changelog.d/process-shell-component-session.md index 0a5c28dc65..03173cb4f1 100644 --- a/changelog.d/process-shell-component-session.md +++ b/changelog.d/process-shell-component-session.md @@ -4,3 +4,8 @@ - Demultiplex concurrent named-stream responses, cancel/reset on owner disconnect, and use one correlated JSON frame codec across CLI, ResourceClient, Providers, and daemon shell owners. - Reject the retired public `exec`/guest shell bridge in production composition and record target-local supervisor Process ownership for ShellSession resources. - Resolve enrolled target-local ComponentSessions for real Process and ShellSession routes, and persist shell supervisor Process ownership through the daemon Resource API with exact owner and finalizer fencing. + +### Fixed + +- Preserve the development-shell PATH for local Bazel test runners without + overriding the worker-standard action PATH used by remote profiles. diff --git a/packages/xtask/tests/buildbuddy_config.rs b/packages/xtask/tests/buildbuddy_config.rs index f3027e1212..2c7590278e 100644 --- a/packages/xtask/tests/buildbuddy_config.rs +++ b/packages/xtask/tests/buildbuddy_config.rs @@ -2417,6 +2417,11 @@ fn developer_profiles_publish_the_tested_checkout_metadata() { let relative_xtask = xtask .strip_prefix(&scratch) .expect("xtask stub must be below the test scratch directory"); + let test_path = format!( + "{}:{}", + bin.display(), + std::env::var("PATH").expect("test PATH must be present") + ); let run = |profile: &str, capture: &Path, trusted: bool| { let mut command = Command::new("bash"); @@ -2437,7 +2442,7 @@ fn developer_profiles_publish_the_tested_checkout_metadata() { .env("D2B_CAPTURE_ARGS", capture) .env( "PATH", - format!("{}:{}", bin.display(), std::env::var("PATH").unwrap()), + &test_path, ); if trusted { command @@ -2472,6 +2477,20 @@ fn developer_profiles_publish_the_tested_checkout_metadata() { !local.contains("--build_metadata="), "local profile must not publish developer metadata" ); + assert!( + local + .lines() + .any(|argument| argument == format!("--action_env=PATH={test_path}")), + "local profile must pass its test PATH to Bazel actions" + ); + for (profile, arguments) in [("remote", &remote), ("trusted-seed", &trusted)] { + assert!( + !arguments + .lines() + .any(|argument| argument.starts_with("--action_env=PATH=")), + "{profile} profile must retain the worker-standard action PATH" + ); + } let remote_metadata = remote .lines() .filter(|argument| argument.starts_with("--build_metadata=")) diff --git a/tests/golden/bazel/cache-policy.json b/tests/golden/bazel/cache-policy.json index 640a230961..c98b8a33a3 100644 --- a/tests/golden/bazel/cache-policy.json +++ b/tests/golden/bazel/cache-policy.json @@ -75,7 +75,7 @@ "tests/tools/bazel-check" ], "allowedSecurityDigests": [ - "sha256:128ade7a73c07c191edd5015d611f389f70500aa23bdc1049fae895fa6369e46" + "sha256:5c236d02496f39faac99518e596e6275b46d283d1aa373f20af2298a248ae151" ], "untrustedCredential": "none", "trustedCredential": "credential-helper" diff --git a/tests/tools/bazel-check b/tests/tools/bazel-check index f14c103325..ee326d36cf 100755 --- a/tests/tools/bazel-check +++ b/tests/tools/bazel-check @@ -399,6 +399,8 @@ run_bazel() { command_flags+=(--shell_executable=/bin/bash) test_bazel_sh=/bin/bash test_path="/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$test_path" + else + command_flags+=(--action_env=PATH="$test_path") fi D2B_BAZEL_PROFILE="$profile" "$bazel_bin" "${startup_flags[@]}" test "--config=$profile" \ "${command_flags[@]}" \ From b6c8fb4f2074d6d55948b39d1d0c3637a5fb130e Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sun, 23 Aug 2026 21:55:40 -0700 Subject: [PATCH 20/56] guest: migrate activation config and health Route Guest activation, bounded configuration, health, and signed target-local process execution through authenticated ComponentSession and Guest broker contracts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- BUILD.bazel | 1 + Cargo.lock | 21 + Cargo.toml | 1 + changelog.d/2026-08-21-v3-guest-u6.md | 10 + docs/reference/daemon-api.md | 464 +++--- docs/reference/schemas/v2/processes.json | 9 +- docs/reference/schemas/v2/wire-protocol.json | 217 +++ ...ore.d2bus.org_EphemeralProcess.schema.json | 82 + nixos-modules/minijail-profiles.nix | 24 + nixos-modules/processes-json.nix | 38 +- nixos-modules/resources-zones-processes.nix | 1 + packages/d2b-broker/src/live_handlers.rs | 29 +- packages/d2b-broker/src/ops/systemd.rs | 2 + packages/d2b-broker/src/runtime.rs | 516 +++++- packages/d2b-broker/src/sys.rs | 155 +- packages/d2b-broker/tests/guest_profile.rs | 2 + packages/d2b-bus/src/router.rs | 2 + .../d2b-contracts-broker/src/broker_wire.rs | 199 ++- .../d2b-contracts-control/src/public_wire.rs | 3 + .../src/v3/activation_nixos.rs | 70 +- .../d2b-contracts-resource/src/v3/process.rs | 39 +- .../src/v3/component_session.rs | 34 +- .../src/v3/services.rs | 5 +- .../src/v3/zone_session.rs | 12 +- .../tests/contracts.rs | 79 +- packages/d2b-core/src/bundle_resolver.rs | 21 +- packages/d2b-core/src/processes.rs | 10 +- packages/d2b-core/src/runtime.rs | 1 + .../bundle_resolver_runner_intent_parity.rs | 2 +- .../d2b-process-conformance/src/identity.rs | 5 + packages/d2b-process-conformance/src/lib.rs | 2 +- .../d2b-process-conformance/src/testing.rs | 31 +- .../d2b-process-conformance/src/ticket.rs | 199 +++ .../d2b-provider-activation-nixos/Cargo.toml | 1 + .../src/controller.rs | 153 +- .../d2b-provider-activation-nixos/src/lib.rs | 5 +- .../src/runner.rs | 5 +- .../tests/reconcile.rs | 68 +- .../tests/runner.rs | 1 + .../d2b-provider-config-nixos/BUILD.bazel | 70 + packages/d2b-provider-config-nixos/Cargo.toml | 23 + .../src/controller.rs | 399 +++++ packages/d2b-provider-config-nixos/src/lib.rs | 35 + .../d2b-provider-config-nixos/src/service.rs | 515 ++++++ .../d2b-provider-config-nixos/src/ttrpc.rs | 303 ++++ .../tests/config_lifecycle.rs | 271 ++++ .../tests/redaction.rs | 18 + .../tests/service_contract.rs | 100 ++ .../src/controller.rs | 40 +- .../src/health.rs | 198 +++ .../src/lib.rs | 5 +- .../d2b-provider-supervisor/src/broker.rs | 59 +- .../d2b-provider-supervisor/src/systemd.rs | 23 +- .../tests/production_adapter.rs | 7 +- .../tests/execution_parents.rs | 54 +- .../tests/execution_parents.rs | 54 +- packages/d2b-resource-client/src/target.rs | 10 +- packages/d2b-resource-store-redb/src/actor.rs | 36 +- packages/d2b-resource-store-redb/src/lib.rs | 33 + packages/d2b-resource-store-redb/src/tests.rs | 56 +- .../src/transaction.rs | 1 + packages/d2b-session-unix/src/vsock.rs | 25 +- packages/d2b-session/src/admission.rs | 19 + packages/d2b-session/src/client.rs | 272 ++++ packages/d2b-session/src/engine.rs | 2 +- packages/d2b-session/src/handshake.rs | 4 +- packages/d2b-session/src/lib.rs | 2 + packages/d2b-session/src/operation.rs | 9 + .../d2b-session/tests/component_session.rs | 29 + packages/d2b/BUILD.bazel | 1 + packages/d2b/Cargo.toml | 1 + packages/d2b/src/activation.rs | 485 +++++- packages/d2b/src/context.rs | 11 + packages/d2b/src/legacy.rs | 58 +- packages/d2b/src/status_read_model.rs | 1 + .../src/guest_component_session.rs | 400 +++++ packages/d2bd-runtime/src/guest_mode.rs | 78 +- .../src/guest_resource_runtime.rs | 1097 +++++++++++++ packages/d2bd-runtime/src/lib.rs | 2 + packages/d2bd-runtime/src/readiness.rs | 4 +- packages/d2bd-runtime/src/resource_api.rs | 6 + packages/d2bd-runtime/src/vm_start_support.rs | 4 + packages/d2bd/BUILD.bazel | 1 + packages/d2bd/Cargo.toml | 1 + .../d2bd/src/activation_resource_runtime.rs | 645 ++++++-- packages/d2bd/src/composition.rs | 1435 +++++++++++++++-- packages/d2bd/src/interaction_composition.rs | 12 + packages/d2bd/src/main.rs | 6 + packages/d2bd/src/process_provider_runtime.rs | 126 +- packages/d2bd/src/process_resource_runtime.rs | 155 +- packages/d2bd/src/resource_runtime.rs | 828 +++++++++- packages/d2bd/src/tpm_effect_port.rs | 2 + packages/d2bd/src/usbip_production.rs | 2 + .../tests/guest_mode_component_session.rs | 78 +- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 4 +- .../production/closure.json | 2 +- .../production/metadata.json | 4 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 4 +- .../production/closure.json | 2 +- .../production/metadata.json | 4 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 4 +- .../production/closure.json | 2 +- .../production/metadata.json | 4 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 4 +- .../broker-production/production/closure.json | 2 +- .../production/metadata.json | 4 +- .../main-product/policy/Cargo.lock | 21 + .../main-product/policy/closure.json | 95 +- .../main-product/policy/metadata.json | 10 +- .../main-product/production/Cargo.lock | 21 + .../main-product/production/closure.json | 95 +- .../main-product/production/metadata.json | 10 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 4 +- .../production/closure.json | 2 +- .../production/metadata.json | 4 +- .../broker-default-tests/policy/closure.json | 2 +- .../broker-default-tests/policy/metadata.json | 4 +- .../production/closure.json | 2 +- .../production/metadata.json | 4 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 4 +- .../production/closure.json | 2 +- .../production/metadata.json | 4 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 4 +- .../production/closure.json | 2 +- .../production/metadata.json | 4 +- .../broker-production/policy/closure.json | 2 +- .../broker-production/policy/metadata.json | 4 +- .../broker-production/production/closure.json | 2 +- .../production/metadata.json | 4 +- .../main-product/policy/Cargo.lock | 21 + .../main-product/policy/closure.json | 95 +- .../main-product/policy/metadata.json | 10 +- .../main-product/production/Cargo.lock | 21 + .../main-product/production/closure.json | 95 +- .../main-product/production/metadata.json | 10 +- .../policy/closure.json | 2 +- .../policy/metadata.json | 4 +- .../production/closure.json | 2 +- .../production/metadata.json | 4 +- 146 files changed, 10388 insertions(+), 866 deletions(-) create mode 100644 changelog.d/2026-08-21-v3-guest-u6.md create mode 100644 packages/d2b-provider-config-nixos/BUILD.bazel create mode 100644 packages/d2b-provider-config-nixos/Cargo.toml create mode 100644 packages/d2b-provider-config-nixos/src/controller.rs create mode 100644 packages/d2b-provider-config-nixos/src/lib.rs create mode 100644 packages/d2b-provider-config-nixos/src/service.rs create mode 100644 packages/d2b-provider-config-nixos/src/ttrpc.rs create mode 100644 packages/d2b-provider-config-nixos/tests/config_lifecycle.rs create mode 100644 packages/d2b-provider-config-nixos/tests/redaction.rs create mode 100644 packages/d2b-provider-config-nixos/tests/service_contract.rs create mode 100644 packages/d2b-session/src/client.rs create mode 100644 packages/d2bd-runtime/src/guest_component_session.rs create mode 100644 packages/d2bd-runtime/src/guest_resource_runtime.rs diff --git a/BUILD.bazel b/BUILD.bazel index 51b9283289..9bfa19635a 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -311,6 +311,7 @@ filegroup( "//packages/d2b-provider-activation-nixos:cargo_workspace_sources", "//packages/d2b-provider-audio-pipewire:cargo_workspace_sources", "//packages/d2b-provider-clipboard-wayland:cargo_workspace_sources", + "//packages/d2b-provider-config-nixos:cargo_workspace_sources", "//packages/d2b-provider-credential-entra:cargo_workspace_sources", "//packages/d2b-provider-credential-managed-identity:cargo_workspace_sources", "//packages/d2b-provider-credential-secret-service:cargo_workspace_sources", diff --git a/Cargo.lock b/Cargo.lock index cecdb991f2..56dc0ad5eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -987,6 +987,7 @@ dependencies = [ "d2b-contracts-zone-session", "d2b-core", "d2b-cutover", + "d2b-provider-config-nixos", "d2b-realm-core", "d2b-resource-client", "d2b-zone-routing", @@ -1364,6 +1365,7 @@ dependencies = [ "d2b-contracts-zone-session", "serde", "serde_json", + "sha2", ] [[package]] @@ -1407,6 +1409,24 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "d2b-provider-config-nixos" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "base64", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "libc", + "rustix 0.38.44", + "schemars", + "serde", + "serde_json", + "sha2", + "ttrpc", +] + [[package]] name = "d2b-provider-credential-entra" version = "0.0.0-bootstrap" @@ -1966,6 +1986,7 @@ dependencies = [ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-device-security-key", "d2b-provider-device-tpm", "d2b-provider-device-usbip", diff --git a/Cargo.toml b/Cargo.toml index 17f2079a42..9cf66c9ce2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "packages/d2b-provider", "packages/d2b-provider-toolkit", "packages/d2b-provider-activation-nixos", + "packages/d2b-provider-config-nixos", "packages/d2b-provider-audio-pipewire", "packages/d2b-provider-clipboard-wayland", "packages/d2b-provider-display-wayland", diff --git a/changelog.d/2026-08-21-v3-guest-u6.md b/changelog.d/2026-08-21-v3-guest-u6.md new file mode 100644 index 0000000000..b8775fe9e2 --- /dev/null +++ b/changelog.d/2026-08-21-v3-guest-u6.md @@ -0,0 +1,10 @@ +### Added + +- Route Guest Process and EphemeralProcess execution through authenticated + Guest ComponentSession and Guest-profile broker contracts with signed + target, session, boot, assignment, and generation binding. + +### Changed + +- Use typed Resource API services for Guest activation, configuration access, + and readiness instead of legacy Guest control callers. diff --git a/docs/reference/daemon-api.md b/docs/reference/daemon-api.md index c6f512d963..13279d1810 100644 --- a/docs/reference/daemon-api.md +++ b/docs/reference/daemon-api.md @@ -84,9 +84,9 @@ compatibility surface. | `HelloOk` | struct | [`HelloOk`](../../packages/d2b-contracts/src/lib.rs#L142) | struct { `server_version`: `Version`; `selected_version`: `Version`; `capabilities`: `Vec` } | | `HelloRejected` | struct | [`HelloRejected`](../../packages/d2b-contracts/src/lib.rs#L150) | struct { `reason`: `HelloRejectedReason` } | | `HelloRejectedReason` | enum | [`HelloRejectedReason`](../../packages/d2b-contracts/src/lib.rs#L156) | `VersionMismatch`; `CapabilityNegotiationFailed`; `InternalError` | -| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1637) | struct { `client_version`: `String`; `supported_features`: `Vec` } | +| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1723) | struct { `client_version`: `String`; `supported_features`: `Vec` } | | `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-control/src/guest_wire.rs#L581) | struct { `metadata`: `GuestRequestMetadata`; `host_nonce`: `GuestNonce`; `transcript_version`: `u32` } | -| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1776) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | +| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1862) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | | `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-control/src/guest_wire.rs#L589) | struct { `guest_nonce`: `GuestNonce`; `guest_boot_id`: `GuestBootId`; `protocol_version`: `u32` } | | `KnownFeatureFlag` | enum | [`KnownFeatureFlag`](../../packages/d2b-contracts/src/lib.rs#L104) | `TypedErrors`; `ManifestV04`; `StatusCheckBridges`; `ExportBrokerAudit`; `ConfiguredLaunchV1`; `UnsafeLocalProviderV1`; `CutoverRunnerV1` | | `SemverRange` | struct | [`SemverRange`](../../packages/d2b-contracts/src/error.rs#L1135) | empty struct | @@ -258,109 +258,109 @@ host reboot. | `AuditRequest` | struct | [`AuditRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L453) | struct { `filter`: `Option`; `format`: `AuditFormat`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | | `HostCheckRequest` | struct | [`HostCheckRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L470) | struct { `read_only`: `bool`; `strict`: `bool` } | | `VmLifecycleRequest` | struct | [`VmLifecycleRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L496) | struct { `vm`: `String`; `flags`: `MutationFlags`; `force`: `bool`; `no_wait_api`: `bool` } | -| `ActivationRequest` | struct | [`ActivationRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L516) | struct { `vm`: `String`; `flags`: `MutationFlags` } | -| `GcRequest` | struct | [`GcRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L524) | struct { `flags`: `MutationFlags`; `keep_generations`: `Option` } | -| `KeysShowRequest` | struct | [`KeysShowRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L533) | struct { `vm`: `String` } | -| `KeysRotateRequest` | struct | [`KeysRotateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L539) | struct { `vm`: `String`; `flags`: `MutationFlags` } | -| `TrustRequest` | struct | [`TrustRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L547) | struct { `vm`: `String`; `flags`: `MutationFlags` } | -| `RotateKnownHostRequest` | struct | [`RotateKnownHostRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L555) | struct { `vm`: `String`; `flags`: `MutationFlags` } | -| `UsbipBindCliRequest` | struct | [`UsbipBindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L563) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | -| `UsbipUnbindCliRequest` | struct | [`UsbipUnbindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L572) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | -| `StoreVerifyRequest` | struct | [`StoreVerifyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L581) | struct { `vm`: `String`; `repair`: `bool` } | -| `ReadGuestConfigRequest` | struct | [`ReadGuestConfigRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L594) | struct { `vm`: `String` } | -| `MigrateRequest` | struct | [`MigrateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2024) | struct { `flags`: `MutationFlags` } | -| `HostPrepareRequest` | struct | [`HostPrepareRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2031) | struct { `flags`: `MutationFlags` } | -| `HostDestroyRequest` | struct | [`HostDestroyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2038) | struct { `flags`: `MutationFlags` } | -| `HostInstallRequest` | struct | [`HostInstallRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2045) | struct { `flags`: `MutationFlags`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool` } | -| `HostReconcileRequest` | struct | [`HostReconcileRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2063) | struct { `flags`: `MutationFlags`; `network`: `bool` } | -| `HostCutoverRequest` | struct | [`HostCutoverRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2074) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `candidate_id`: `Option`; `revision_plan_id`: `Option`; `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option`; `preview_digest`: `Option`; `recovery_digest`: `Option`; `operator_id`: `Option`; `consent_digest`: `Option`; `consent_json`: `Option`; `destructive_consent_digest`: `Option`; `destructive_consent_json`: `Option`; `destroy_durable_volumes`: `Option`; `recovery_attestation_json`: `Option`; `host_digest`: `Option`; `fresh_consent_digest`: `Option`; `reason`: `Option`; `reset_scope`: `Option`; `target`: `Option`; `zone`: `Option` } | -| `UsbSecurityKeyStatusRequest` | struct | [`UsbSecurityKeyStatusRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L3892) | empty struct | -| `UsbSecurityKeySessionsRequest` | struct | [`UsbSecurityKeySessionsRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L3978) | empty struct | -| `UsbSecurityKeyCancelRequest` | struct | [`UsbSecurityKeyCancelRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4032) | struct { `session_id`: `Option`; `current`: `bool` } | -| `UsbSecurityKeyTestRequest` | struct | [`UsbSecurityKeyTestRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4057) | struct { `vm`: `String` } | +| `ActivationRequest` | struct | [`ActivationRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L516) | struct { `vm`: `String`; `to_generation`: `Option`; `flags`: `MutationFlags` } | +| `GcRequest` | struct | [`GcRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L527) | struct { `flags`: `MutationFlags`; `keep_generations`: `Option` } | +| `KeysShowRequest` | struct | [`KeysShowRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L536) | struct { `vm`: `String` } | +| `KeysRotateRequest` | struct | [`KeysRotateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L542) | struct { `vm`: `String`; `flags`: `MutationFlags` } | +| `TrustRequest` | struct | [`TrustRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L550) | struct { `vm`: `String`; `flags`: `MutationFlags` } | +| `RotateKnownHostRequest` | struct | [`RotateKnownHostRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L558) | struct { `vm`: `String`; `flags`: `MutationFlags` } | +| `UsbipBindCliRequest` | struct | [`UsbipBindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L566) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | +| `UsbipUnbindCliRequest` | struct | [`UsbipUnbindCliRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L575) | struct { `vm`: `String`; `bus_id`: `String`; `flags`: `MutationFlags` } | +| `StoreVerifyRequest` | struct | [`StoreVerifyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L584) | struct { `vm`: `String`; `repair`: `bool` } | +| `ReadGuestConfigRequest` | struct | [`ReadGuestConfigRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L597) | struct { `vm`: `String` } | +| `MigrateRequest` | struct | [`MigrateRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2027) | struct { `flags`: `MutationFlags` } | +| `HostPrepareRequest` | struct | [`HostPrepareRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2034) | struct { `flags`: `MutationFlags` } | +| `HostDestroyRequest` | struct | [`HostDestroyRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2041) | struct { `flags`: `MutationFlags` } | +| `HostInstallRequest` | struct | [`HostInstallRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2048) | struct { `flags`: `MutationFlags`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool` } | +| `HostReconcileRequest` | struct | [`HostReconcileRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2066) | struct { `flags`: `MutationFlags`; `network`: `bool` } | +| `HostCutoverRequest` | struct | [`HostCutoverRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L2077) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `candidate_id`: `Option`; `revision_plan_id`: `Option`; `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option`; `preview_digest`: `Option`; `recovery_digest`: `Option`; `operator_id`: `Option`; `consent_digest`: `Option`; `consent_json`: `Option`; `destructive_consent_digest`: `Option`; `destructive_consent_json`: `Option`; `destroy_durable_volumes`: `Option`; `recovery_attestation_json`: `Option`; `host_digest`: `Option`; `fresh_consent_digest`: `Option`; `reason`: `Option`; `reset_scope`: `Option`; `target`: `Option`; `zone`: `Option` } | +| `UsbSecurityKeyStatusRequest` | struct | [`UsbSecurityKeyStatusRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L3895) | empty struct | +| `UsbSecurityKeySessionsRequest` | struct | [`UsbSecurityKeySessionsRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L3981) | empty struct | +| `UsbSecurityKeyCancelRequest` | struct | [`UsbSecurityKeyCancelRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4035) | struct { `session_id`: `Option`; `current`: `bool` } | +| `UsbSecurityKeyTestRequest` | struct | [`UsbSecurityKeyTestRequest`](../../packages/d2b-contracts-control/src/public_wire.rs#L4060) | struct { `vm`: `String` } | ### Broker socket request types | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `BrokerRequest` | enum | [`BrokerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L35) | `ApplyHostGenerationHandoff` - (crate::host_generation::ApplyHostGenerationHandoff); `LaunchCutoverRunner` - (LaunchCutoverRunnerRequest); `CutoverAudit` - (CutoverAuditRequest); `CutoverEffect` - (CutoverEffectRequest); `ApplyNftables` - (ApplyNftablesRequest); `ApplyNftablesProjection` - (ApplyNftablesProjectionRequest); `ApplyNmUnmanaged` - (ApplyNmUnmanagedRequest); `ApplyRoute` - (ApplyRouteRequest); `ApplySysctl` - (ApplySysctlRequest); `BindUnixSocket` - (BindUnixSocketRequest); `CreateOrReconcileUsersGroups` - (CreateOrReconcileUsersGroupsRequest); `CreateBridge` - (CreateBridgeRequest); `DeleteBridge` - (DeleteBridgeRequest); `CreatePersistentTap` - (CreatePersistentTapRequest); `DeletePersistentTap` - (DeletePersistentTapRequest); `CreateTapFd` - (CreateTapFdRequest); `DelegateCgroupV2` - (DelegateCgroupV2Request); `ExportBrokerAudit` - (ExportBrokerAuditRequest); `Hello` - (HelloRequest); `GuestControlSign` - (GuestControlSignRequest); `InjectSecretById` - (SecretByIdRequest); `LaunchMinijailChild` - (LaunchMinijailChildRequest); `ModprobeIfAllowed` - (ModprobeIfAllowedRequest); `OpenCgroupDir` - (OpenCgroupDirRequest); `OpenDevice` - (OpenDeviceRequest); `OpenFuse` - (OpenFuseRequest); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyRequest); `OpenKvm` - (OpenKvmRequest); `QemuMediaEnroll` - (QemuMediaEnrollRequest); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryRequest); `QemuMediaBoot` - (QemuMediaBootRequest); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleRequest); `QemuMediaQueryStatus` - (QemuMediaQueryStatusRequest); `QemuMediaQuit` - (QemuMediaLifecycleRequest); `QemuMediaAttach` - (QemuMediaHotplugRequest); `QemuMediaDetach` - (QemuMediaHotplugRequest); `OpenPidfd` - (OpenPidfdRequest); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketRequest); `ObserveRunner` - (ObserveRunnerRequest); `PipeWireAudio` - (PipeWireAudioRequest); `StartSystemdUnit` - (StartSystemdUnitRequest); `CheckSystemdUserManager` - (CheckSystemdUserManagerRequest); `ObserveSystemdUnit` - (ObserveSystemdUnitRequest); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdRequest); `StopSystemdUnit` - (StopSystemdUnitRequest); `OpenZoneStore` - (OpenZoneStoreRequest); `OpenVhostNet` - (OpenVhostNetRequest); `PauseBroker`; `PollChildReaped`; `PrepareRuntimeDir` - (PrepareDirRequest); `PrepareStateDir` - (PrepareDirRequest); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateRequest); `ReconcileStorageScope` - (ReconcileStorageScopeRequest); `ValidateLockSpec` - (ValidateLockSpecRequest); `PrepareStoreView` - (PrepareStoreViewRequest); `StoreSync` - (StoreSyncRequest); `StoreVerify` - (StoreVerifyRequest); `ReadSecretById` - (SecretByIdRequest); `ResumeBroker`; `RotateSecretById` - (SecretByIdRequest); `RunHostInstall` - (RunHostInstallRequest); `RunMigrate` - (RunMigrateRequest); `RunActivation` - (RunActivationRequest); `RunGc` - (RunGcRequest); `RunKeysRotate` - (RunKeysRotateRequest); `RunHostKeyTrust` - (RunHostKeyTrustRequest); `RunRotateKnownHost` - (RunRotateKnownHostRequest); `SetBridgePortFlags` - (SetBridgePortFlagsRequest); `SetSocketAcl` - (SetSocketAclRequest); `SetupMountNamespace` - (SetupMountNamespaceRequest); `CgroupKill` - (CgroupKillRequest); `SignalRunner` - (SignalRunnerRequest); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdRequest); `SpawnRunner` - (SpawnRunnerRequest); `UpdateHostsFile` - (UpdateHostsFileRequest); `UsbipBind` - (UsbipBindRequest); `UsbipBindFirewallRule` - (UsbipBindFirewallRuleRequest); `UsbipProxyReconcile` - (UsbipProxyReconcileRequest); `UsbipUnbind` - (UsbipUnbindRequest); `UsbipExplicitBind` - (UsbipExplicitBindRequest); `UsbipExplicitFirewallRule` - (UsbipExplicitFirewallRuleRequest); `ResourceActivationAudit` - (ResourceActivationAuditRequest); `ValidateBundle`; `SeedDnsmasqLease` - (SeedDnsmasqLeaseRequest); `BindMountFromHardlinkFarm` - (BindMountFromHardlinkFarmRequest); `OwnershipMatrixCheck` - (OwnershipMatrixCheckRequest); `SshHostKeyPreflight` - (SshHostKeyPreflightRequest); `DiskInit` - (DiskInitRequest); `SecurityKeyOpenDevice` - (d2b_contracts::security_key::SecurityKeyOpenDeviceRequest); `SecurityKeyApplyUdevRules` - (d2b_contracts::security_key::SecurityKeyApplyUdevRulesRequest) | -| `LaunchCutoverRunnerRequest` | struct | [`LaunchCutoverRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L344) | struct { `operation_id`: `BundleOpId`; `bootstrap_fd_index`: `u32`; `capability_digest`: `CanonicalAuditDigest`; `expires_at_ms`: `u64` } | -| `CutoverAuditRequest` | struct | [`CutoverAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L409) | struct { `operation_id`: `BundleOpId`; `phase`: `u8`; `transition`: `CutoverAuditTransition`; `request_digest`: `CanonicalAuditDigest`; `reason_digest`: `Option` } | -| `CutoverVerificationRequest` | struct | [`CutoverVerificationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L506) | struct { `expected_zone_ids`: `Vec` } | -| `CutoverAdmissionRequest` | struct | [`CutoverAdmissionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L543) | struct { `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option` } | -| `CutoverEffectRequest` | struct | [`CutoverEffectRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L604) | struct { `operation_id`: `BundleOpId`; `authority`: `CutoverEffectAuthority`; `phase`: `u8`; `effect_id`: `BundleOpId`; `effect`: `CutoverEffectKind`; `replay_class`: `CutoverReplayClass`; `request_digest`: `CanonicalAuditDigest`; `capability_digest`: `CanonicalAuditDigest`; `identity`: `Option`; `handoff`: `Option`; `payload`: `Option` } | -| `RunHostInstallRequest` | struct | [`RunHostInstallRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1476) | struct { `bundle_installer_intent_ref`: `BundleOpId`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool`; `tracing_span_id`: `Option` } | -| `RunMigrateRequest` | struct | [`RunMigrateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1500) | struct { `bundle_migrate_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `RunActivationRequest` | struct | [`RunActivationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1533) | struct { `bundle_activation_intent_ref`: `BundleOpId`; `mode`: `ActivationMode`; `system_artifact_id`: `Option`; `phase`: `ActivationPhase`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunGcRequest` | struct | [`RunGcRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1560) | struct { `bundle_gc_intent_ref`: `BundleOpId`; `keep_generations`: `Option`; `tracing_span_id`: `Option` } | -| `RunKeysRotateRequest` | struct | [`RunKeysRotateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1579) | struct { `bundle_keys_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunHostKeyTrustRequest` | struct | [`RunHostKeyTrustRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1596) | struct { `bundle_trust_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `RunRotateKnownHostRequest` | struct | [`RunRotateKnownHostRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1614) | struct { `bundle_rotate_known_host_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | -| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1637) | struct { `client_version`: `String`; `supported_features`: `Vec` } | -| `ApplyNftablesRequest` | struct | [`ApplyNftablesRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1788) | struct { `bundle_nft_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `desired_hash`: `Option`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplyNftablesProjectionRequest` | struct | [`ApplyNftablesProjectionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1819) | struct { `bundle_nft_projection_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `action`: `NftablesProjectionAction`; `expected_generation_id`: `ResourceBundleGenerationId`; `desired_hash`: `Option`; `tracing_span_id`: `Option` } | -| `ApplyNmUnmanagedRequest` | struct | [`ApplyNmUnmanagedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1833) | struct { `bundle_nm_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplyRouteRequest` | struct | [`ApplyRouteRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1844) | struct { `bundle_route_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `ApplySysctlRequest` | struct | [`ApplySysctlRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1855) | struct { `bundle_sysctl_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `BindUnixSocketRequest` | struct | [`BindUnixSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1866) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `CreateOrReconcileUsersGroupsRequest` | struct | [`CreateOrReconcileUsersGroupsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1876) | struct { `subject_ids`: `Vec`; `tracing_span_id`: `Option` } | -| `CreatePersistentTapRequest` | struct | [`CreatePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1890) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `attachment_id`: `Option`; `network_generation`: `Option`; `attachment_generation`: `Option`; `tracing_span_id`: `Option` } | -| `DeletePersistentTapRequest` | struct | [`DeletePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1910) | struct { `attachment_id`: `ResourceUid`; `expected_network_generation`: `ResourceGeneration`; `expected_attachment_generation`: `ResourceGeneration`; `tracing_span_id`: `Option` } | -| `CreateBridgeRequest` | struct | [`CreateBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1924) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `DeleteBridgeRequest` | struct | [`DeleteBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1935) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `CreateTapFdRequest` | struct | [`CreateTapFdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1946) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `DelegateCgroupV2Request` | struct | [`DelegateCgroupV2Request`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1958) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `ExportBrokerAuditRequest` | struct | [`ExportBrokerAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1966) | struct { `filter`: `Option`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | -| `GuestControlSignRequest` | struct | [`GuestControlSignRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2076) | struct { `vm_id`: `VmId`; `role`: `GuestControlProofRole`; `protocol_version`: `u32`; `direction`: `GuestControlDirection`; `purpose`: `GuestControlAuthPurpose`; `guest_control_port`: `u32`; `peer_cid`: `Option`; `host_nonce`: `Vec`; `guest_nonce`: `Vec`; `guest_boot_id`: `GuestBootIdWire`; `capabilities_hash`: `Option`; `tracing_span_id`: `Option` } | -| `SecretByIdRequest` | struct | [`SecretByIdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2131) | struct { `opaque_id`: `String`; `tracing_span_id`: `Option` } | -| `LaunchMinijailChildRequest` | struct | [`LaunchMinijailChildRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2143) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `ModprobeIfAllowedRequest` | struct | [`ModprobeIfAllowedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2157) | struct { `module_name`: `String`; `tracing_span_id`: `Option` } | -| `OpenCgroupDirRequest` | struct | [`OpenCgroupDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2167) | struct { `scope_id`: `ScopeId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | -| `OpenDeviceRequest` | struct | [`OpenDeviceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2176) | struct { `role_id`: `RoleId`; `device_class`: `String`; `tracing_span_id`: `Option` } | -| `OpenKvmRequest` | struct | [`OpenKvmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2185) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `QemuMediaEnrollRequest` | struct | [`QemuMediaEnrollRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2199) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `bus_id`: `String`; `tracing_span_id`: `Option` } | -| `QemuMediaRefreshRegistryRequest` | struct | [`QemuMediaRefreshRegistryRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2220) | struct { `tracing_span_id`: `Option` } | -| `QemuMediaBootRequest` | struct | [`QemuMediaBootRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2242) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `QemuMediaLifecycleRequest` | struct | [`QemuMediaLifecycleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2250) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `QemuMediaQueryStatusRequest` | struct | [`QemuMediaQueryStatusRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2258) | struct { `vm_id`: `VmId`; `shutdown_context`: `bool`; `tracing_span_id`: `Option` } | -| `QemuMediaHotplugRequest` | struct | [`QemuMediaHotplugRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2323) | struct { `vm_id`: `VmId`; `bus_id`: `String`; `tracing_span_id`: `Option` } | -| `OpenPidfdRequest` | struct | [`OpenPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2381) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `expected_start_time_ticks`: `u64`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `OpenPeerPidfdFromAcceptedSocketRequest` | struct | [`OpenPeerPidfdFromAcceptedSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2424) | empty struct | -| `ObserveRunnerRequest` | struct | [`ObserveRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2439) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `PipeWireAudioRequest` | struct | [`PipeWireAudioRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2489) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `bundle_runner_intent_ref`: `BundleOpId`; `channel`: `PipeWireAudioChannel`; `action`: `PipeWireAudioAction`; `tracing_span_id`: `Option` } | -| `SystemdUnitRequest` | struct | [`SystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2556) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `bundle_content_identity`: `String`; `provider_identity`: `[u8; 32]`; `template_identity`: `[u8; 32]`; `generation`: `u64`; `domain`: `SystemdUnitDomain`; `execution_ref`: `Option`; `user_ref`: `Option`; `sandbox_plan`: `Option`; `tracing_span_id`: `Option` } | -| `OpenSystemdUnitPidfdRequest` | struct | [`OpenSystemdUnitPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2609) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity` } | -| `StopSystemdUnitRequest` | struct | [`StopSystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2620) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity`; `class`: `SystemdStopClass` } | -| `OpenZoneStoreRequest` | struct | [`OpenZoneStoreRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2696) | struct { `zone_store_id`: `ZoneStoreId` } | -| `OpenVhostNetRequest` | struct | [`OpenVhostNetRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2730) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `OpenFuseRequest` | struct | [`OpenFuseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2738) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `OpenHidrawSecurityKeyRequest` | struct | [`OpenHidrawSecurityKeyRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2750) | struct { `vm_id`: `VmId`; `selector_id`: `String`; `device_ref`: `ResourceRef`; `authority_key`: `String`; `tracing_span_id`: `Option` } | -| `PrepareDirRequest` | struct | [`PrepareDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2793) | struct { `vm_id`: `VmId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | -| `MigrateLegacySwtpmStateRequest` | struct | [`MigrateLegacySwtpmStateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2803) | struct { `bundle_legacy_swtpm_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `probe_only`: `bool`; `tracing_span_id`: `Option` } | -| `PrepareStoreViewRequest` | struct | [`PrepareStoreViewRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2856) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `StoreSyncRequest` | struct | [`StoreSyncRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2876) | struct { `vm_id`: `VmId`; `bundle_closure_ref`: `BundleClosureRef`; `generation_token`: `u32`; `tracing_span_id`: `Option` } | -| `SetBridgePortFlagsRequest` | struct | [`SetBridgePortFlagsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2920) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `SetSocketAclRequest` | struct | [`SetSocketAclRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2929) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `SetupMountNamespaceRequest` | struct | [`SetupMountNamespaceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2939) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `UpdateHostsFileRequest` | struct | [`UpdateHostsFileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2950) | struct { `bundle_hosts_intent_ref`: `BundleOpId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | -| `UsbipBindRequest` | struct | [`UsbipBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2963) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `UsbipBindFirewallRuleRequest` | struct | [`UsbipBindFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2980) | struct { `bundle_usbip_firewall_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `UsbipProxyReconcileRequest` | struct | [`UsbipProxyReconcileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2988) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `UsbipUnbindRequest` | struct | [`UsbipUnbindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2996) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `preserve_durable_claim`: `bool`; `tracing_span_id`: `Option` } | -| `UsbipExplicitBindRequest` | struct | [`UsbipExplicitBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3018) | struct { `bus_id`: `String`; `vm`: `String`; `env`: `String`; `tracing_span_id`: `Option` } | -| `UsbipExplicitFirewallRuleRequest` | struct | [`UsbipExplicitFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3036) | struct { `bus_id`: `String`; `env`: `String`; `host_uplink_ip`: `String`; `net_uplink_ip`: `String`; `tracing_span_id`: `Option` } | -| `ResourceActivationAuditRequest` | struct | [`ResourceActivationAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3128) | struct { `audit_join`: `AuditJoinContext` } | -| `SignalRunnerRequest` | struct | [`SignalRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3159) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `signal`: `RunnerSignal`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `CgroupKillRequest` | struct | [`CgroupKillRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3186) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | -| `DeregisterRunnerPidfdRequest` | struct | [`DeregisterRunnerPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3195) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `tracing_span_id`: `Option` } | -| `SpawnRunnerRequest` | struct | [`SpawnRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3322) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `bundle_content_identity`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `sandbox_plan`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `runtime_allocations`: `Vec`; `tracing_span_id`: `Option`; `workload_identity`: `Option` } | -| `SeedDnsmasqLeaseRequest` | struct | [`SeedDnsmasqLeaseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3597) | struct { `vm_id`: `VmId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | -| `BindMountFromHardlinkFarmRequest` | struct | [`BindMountFromHardlinkFarmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3609) | struct { `vm_id`: `VmId`; `bundle_store_view_intent_ref`: `Option`; `tracing_span_id`: `Option` } | -| `OwnershipMatrixCheckRequest` | struct | [`OwnershipMatrixCheckRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3624) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `SshHostKeyPreflightRequest` | struct | [`SshHostKeyPreflightRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3635) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | -| `ReconcileStorageScopeRequest` | struct | [`ReconcileStorageScopeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3649) | struct { `storage_ref`: `BundleOpId`; `apply`: `bool`; `tracing_span_id`: `Option` } | -| `ValidateLockSpecRequest` | struct | [`ValidateLockSpecRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3686) | struct { `lock_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | -| `DiskInitRequest` | struct | [`DiskInitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3720) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `BrokerRequest` | enum | [`BrokerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L36) | `ApplyHostGenerationHandoff` - (crate::host_generation::ApplyHostGenerationHandoff); `LaunchCutoverRunner` - (LaunchCutoverRunnerRequest); `CutoverAudit` - (CutoverAuditRequest); `CutoverEffect` - (CutoverEffectRequest); `ApplyNftables` - (ApplyNftablesRequest); `ApplyNftablesProjection` - (ApplyNftablesProjectionRequest); `ApplyNmUnmanaged` - (ApplyNmUnmanagedRequest); `ApplyRoute` - (ApplyRouteRequest); `ApplySysctl` - (ApplySysctlRequest); `BindUnixSocket` - (BindUnixSocketRequest); `CreateOrReconcileUsersGroups` - (CreateOrReconcileUsersGroupsRequest); `CreateBridge` - (CreateBridgeRequest); `DeleteBridge` - (DeleteBridgeRequest); `CreatePersistentTap` - (CreatePersistentTapRequest); `DeletePersistentTap` - (DeletePersistentTapRequest); `CreateTapFd` - (CreateTapFdRequest); `DelegateCgroupV2` - (DelegateCgroupV2Request); `ExportBrokerAudit` - (ExportBrokerAuditRequest); `Hello` - (HelloRequest); `GuestControlSign` - (GuestControlSignRequest); `InjectSecretById` - (SecretByIdRequest); `LaunchMinijailChild` - (LaunchMinijailChildRequest); `ModprobeIfAllowed` - (ModprobeIfAllowedRequest); `OpenCgroupDir` - (OpenCgroupDirRequest); `OpenDevice` - (OpenDeviceRequest); `OpenFuse` - (OpenFuseRequest); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyRequest); `OpenKvm` - (OpenKvmRequest); `QemuMediaEnroll` - (QemuMediaEnrollRequest); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryRequest); `QemuMediaBoot` - (QemuMediaBootRequest); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleRequest); `QemuMediaQueryStatus` - (QemuMediaQueryStatusRequest); `QemuMediaQuit` - (QemuMediaLifecycleRequest); `QemuMediaAttach` - (QemuMediaHotplugRequest); `QemuMediaDetach` - (QemuMediaHotplugRequest); `OpenPidfd` - (OpenPidfdRequest); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketRequest); `ObserveRunner` - (ObserveRunnerRequest); `PipeWireAudio` - (PipeWireAudioRequest); `StartSystemdUnit` - (StartSystemdUnitRequest); `CheckSystemdUserManager` - (CheckSystemdUserManagerRequest); `ObserveSystemdUnit` - (ObserveSystemdUnitRequest); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdRequest); `StopSystemdUnit` - (StopSystemdUnitRequest); `OpenZoneStore` - (OpenZoneStoreRequest); `OpenVhostNet` - (OpenVhostNetRequest); `PauseBroker`; `PollChildReaped`; `PrepareRuntimeDir` - (PrepareDirRequest); `PrepareStateDir` - (PrepareDirRequest); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateRequest); `ReconcileStorageScope` - (ReconcileStorageScopeRequest); `ValidateLockSpec` - (ValidateLockSpecRequest); `PrepareStoreView` - (PrepareStoreViewRequest); `StoreSync` - (StoreSyncRequest); `StoreVerify` - (StoreVerifyRequest); `ReadSecretById` - (SecretByIdRequest); `ResumeBroker`; `RotateSecretById` - (SecretByIdRequest); `RunHostInstall` - (RunHostInstallRequest); `RunMigrate` - (RunMigrateRequest); `RunActivation` - (RunActivationRequest); `RunGc` - (RunGcRequest); `RunKeysRotate` - (RunKeysRotateRequest); `RunHostKeyTrust` - (RunHostKeyTrustRequest); `RunRotateKnownHost` - (RunRotateKnownHostRequest); `SetBridgePortFlags` - (SetBridgePortFlagsRequest); `SetSocketAcl` - (SetSocketAclRequest); `SetupMountNamespace` - (SetupMountNamespaceRequest); `CgroupKill` - (CgroupKillRequest); `SignalRunner` - (SignalRunnerRequest); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdRequest); `SpawnRunner` - (SpawnRunnerRequest); `UpdateHostsFile` - (UpdateHostsFileRequest); `UsbipBind` - (UsbipBindRequest); `UsbipBindFirewallRule` - (UsbipBindFirewallRuleRequest); `UsbipProxyReconcile` - (UsbipProxyReconcileRequest); `UsbipUnbind` - (UsbipUnbindRequest); `UsbipExplicitBind` - (UsbipExplicitBindRequest); `UsbipExplicitFirewallRule` - (UsbipExplicitFirewallRuleRequest); `ResourceActivationAudit` - (ResourceActivationAuditRequest); `ValidateBundle`; `SeedDnsmasqLease` - (SeedDnsmasqLeaseRequest); `BindMountFromHardlinkFarm` - (BindMountFromHardlinkFarmRequest); `OwnershipMatrixCheck` - (OwnershipMatrixCheckRequest); `SshHostKeyPreflight` - (SshHostKeyPreflightRequest); `DiskInit` - (DiskInitRequest); `SecurityKeyOpenDevice` - (d2b_contracts::security_key::SecurityKeyOpenDeviceRequest); `SecurityKeyApplyUdevRules` - (d2b_contracts::security_key::SecurityKeyApplyUdevRulesRequest) | +| `LaunchCutoverRunnerRequest` | struct | [`LaunchCutoverRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L345) | struct { `operation_id`: `BundleOpId`; `bootstrap_fd_index`: `u32`; `capability_digest`: `CanonicalAuditDigest`; `expires_at_ms`: `u64` } | +| `CutoverAuditRequest` | struct | [`CutoverAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L410) | struct { `operation_id`: `BundleOpId`; `phase`: `u8`; `transition`: `CutoverAuditTransition`; `request_digest`: `CanonicalAuditDigest`; `reason_digest`: `Option` } | +| `CutoverVerificationRequest` | struct | [`CutoverVerificationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L507) | struct { `expected_zone_ids`: `Vec` } | +| `CutoverAdmissionRequest` | struct | [`CutoverAdmissionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L544) | struct { `system_artifact_id`: `Option`; `source_system_artifact_id`: `Option` } | +| `CutoverEffectRequest` | struct | [`CutoverEffectRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L605) | struct { `operation_id`: `BundleOpId`; `authority`: `CutoverEffectAuthority`; `phase`: `u8`; `effect_id`: `BundleOpId`; `effect`: `CutoverEffectKind`; `replay_class`: `CutoverReplayClass`; `request_digest`: `CanonicalAuditDigest`; `capability_digest`: `CanonicalAuditDigest`; `identity`: `Option`; `handoff`: `Option`; `payload`: `Option` } | +| `RunHostInstallRequest` | struct | [`RunHostInstallRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1562) | struct { `bundle_installer_intent_ref`: `BundleOpId`; `enable`: `bool`; `start`: `bool`; `no_start`: `bool`; `tracing_span_id`: `Option` } | +| `RunMigrateRequest` | struct | [`RunMigrateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1586) | struct { `bundle_migrate_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `RunActivationRequest` | struct | [`RunActivationRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1619) | struct { `bundle_activation_intent_ref`: `BundleOpId`; `mode`: `ActivationMode`; `system_artifact_id`: `Option`; `phase`: `ActivationPhase`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunGcRequest` | struct | [`RunGcRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1646) | struct { `bundle_gc_intent_ref`: `BundleOpId`; `keep_generations`: `Option`; `tracing_span_id`: `Option` } | +| `RunKeysRotateRequest` | struct | [`RunKeysRotateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1665) | struct { `bundle_keys_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunHostKeyTrustRequest` | struct | [`RunHostKeyTrustRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1682) | struct { `bundle_trust_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `RunRotateKnownHostRequest` | struct | [`RunRotateKnownHostRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1700) | struct { `bundle_rotate_known_host_intent_ref`: `BundleOpId`; `vm`: `String`; `tracing_span_id`: `Option` } | +| `HelloRequest` | struct | [`HelloRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1723) | struct { `client_version`: `String`; `supported_features`: `Vec` } | +| `ApplyNftablesRequest` | struct | [`ApplyNftablesRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1874) | struct { `bundle_nft_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `desired_hash`: `Option`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplyNftablesProjectionRequest` | struct | [`ApplyNftablesProjectionRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1905) | struct { `bundle_nft_projection_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `action`: `NftablesProjectionAction`; `expected_generation_id`: `ResourceBundleGenerationId`; `desired_hash`: `Option`; `tracing_span_id`: `Option` } | +| `ApplyNmUnmanagedRequest` | struct | [`ApplyNmUnmanagedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1919) | struct { `bundle_nm_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplyRouteRequest` | struct | [`ApplyRouteRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1930) | struct { `bundle_route_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `ApplySysctlRequest` | struct | [`ApplySysctlRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1941) | struct { `bundle_sysctl_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `BindUnixSocketRequest` | struct | [`BindUnixSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1952) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `CreateOrReconcileUsersGroupsRequest` | struct | [`CreateOrReconcileUsersGroupsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1962) | struct { `subject_ids`: `Vec`; `tracing_span_id`: `Option` } | +| `CreatePersistentTapRequest` | struct | [`CreatePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1976) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `attachment_id`: `Option`; `network_generation`: `Option`; `attachment_generation`: `Option`; `tracing_span_id`: `Option` } | +| `DeletePersistentTapRequest` | struct | [`DeletePersistentTapRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1996) | struct { `attachment_id`: `ResourceUid`; `expected_network_generation`: `ResourceGeneration`; `expected_attachment_generation`: `ResourceGeneration`; `tracing_span_id`: `Option` } | +| `CreateBridgeRequest` | struct | [`CreateBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2010) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `DeleteBridgeRequest` | struct | [`DeleteBridgeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2021) | struct { `bundle_bridge_intent_ref`: `BundleOpId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `CreateTapFdRequest` | struct | [`CreateTapFdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2032) | struct { `role_id`: `RoleId`; `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `DelegateCgroupV2Request` | struct | [`DelegateCgroupV2Request`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2044) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `ExportBrokerAuditRequest` | struct | [`ExportBrokerAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2052) | struct { `filter`: `Option`; `since`: `Option`; `cursor`: `Option`; `limit`: `u32` } | +| `GuestControlSignRequest` | struct | [`GuestControlSignRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2162) | struct { `vm_id`: `VmId`; `role`: `GuestControlProofRole`; `protocol_version`: `u32`; `direction`: `GuestControlDirection`; `purpose`: `GuestControlAuthPurpose`; `guest_control_port`: `u32`; `peer_cid`: `Option`; `host_nonce`: `Vec`; `guest_nonce`: `Vec`; `guest_boot_id`: `GuestBootIdWire`; `capabilities_hash`: `Option`; `tracing_span_id`: `Option` } | +| `SecretByIdRequest` | struct | [`SecretByIdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2217) | struct { `opaque_id`: `String`; `tracing_span_id`: `Option` } | +| `LaunchMinijailChildRequest` | struct | [`LaunchMinijailChildRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2229) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `ModprobeIfAllowedRequest` | struct | [`ModprobeIfAllowedRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2243) | struct { `module_name`: `String`; `tracing_span_id`: `Option` } | +| `OpenCgroupDirRequest` | struct | [`OpenCgroupDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2253) | struct { `scope_id`: `ScopeId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | +| `OpenDeviceRequest` | struct | [`OpenDeviceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2262) | struct { `role_id`: `RoleId`; `device_class`: `String`; `tracing_span_id`: `Option` } | +| `OpenKvmRequest` | struct | [`OpenKvmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2271) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `QemuMediaEnrollRequest` | struct | [`QemuMediaEnrollRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2285) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `bus_id`: `String`; `tracing_span_id`: `Option` } | +| `QemuMediaRefreshRegistryRequest` | struct | [`QemuMediaRefreshRegistryRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2306) | struct { `tracing_span_id`: `Option` } | +| `QemuMediaBootRequest` | struct | [`QemuMediaBootRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2328) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `QemuMediaLifecycleRequest` | struct | [`QemuMediaLifecycleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2336) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `QemuMediaQueryStatusRequest` | struct | [`QemuMediaQueryStatusRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2344) | struct { `vm_id`: `VmId`; `shutdown_context`: `bool`; `tracing_span_id`: `Option` } | +| `QemuMediaHotplugRequest` | struct | [`QemuMediaHotplugRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2409) | struct { `vm_id`: `VmId`; `bus_id`: `String`; `tracing_span_id`: `Option` } | +| `OpenPidfdRequest` | struct | [`OpenPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2496) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `expected_start_time_ticks`: `u64`; `resource_ref`: `Option`; `resource_uid`: `Option`; `guest_execution`: `Option`; `tracing_span_id`: `Option` } | +| `OpenPeerPidfdFromAcceptedSocketRequest` | struct | [`OpenPeerPidfdFromAcceptedSocketRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2542) | empty struct | +| `ObserveRunnerRequest` | struct | [`ObserveRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2557) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `guest_execution`: `Option`; `tracing_span_id`: `Option` } | +| `PipeWireAudioRequest` | struct | [`PipeWireAudioRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2610) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `bundle_runner_intent_ref`: `BundleOpId`; `channel`: `PipeWireAudioChannel`; `action`: `PipeWireAudioAction`; `tracing_span_id`: `Option` } | +| `SystemdUnitRequest` | struct | [`SystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2680) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `bundle_content_identity`: `String`; `provider_identity`: `[u8; 32]`; `template_identity`: `[u8; 32]`; `generation`: `u64`; `domain`: `SystemdUnitDomain`; `execution_ref`: `Option`; `user_ref`: `Option`; `guest_execution`: `Option`; `sandbox_plan`: `Option`; `tracing_span_id`: `Option` } | +| `OpenSystemdUnitPidfdRequest` | struct | [`OpenSystemdUnitPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2736) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity` } | +| `StopSystemdUnitRequest` | struct | [`StopSystemdUnitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2747) | struct { `unit`: `SystemdUnitRequest`; `expected`: `SystemdUnitIdentity`; `class`: `SystemdStopClass` } | +| `OpenZoneStoreRequest` | struct | [`OpenZoneStoreRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2823) | struct { `zone_store_id`: `ZoneStoreId` } | +| `OpenVhostNetRequest` | struct | [`OpenVhostNetRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2857) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `OpenFuseRequest` | struct | [`OpenFuseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2865) | struct { `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `OpenHidrawSecurityKeyRequest` | struct | [`OpenHidrawSecurityKeyRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2877) | struct { `vm_id`: `VmId`; `selector_id`: `String`; `device_ref`: `ResourceRef`; `authority_key`: `String`; `tracing_span_id`: `Option` } | +| `PrepareDirRequest` | struct | [`PrepareDirRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2920) | struct { `vm_id`: `VmId`; `path_class`: `PathClass`; `tracing_span_id`: `Option` } | +| `MigrateLegacySwtpmStateRequest` | struct | [`MigrateLegacySwtpmStateRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2930) | struct { `bundle_legacy_swtpm_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `probe_only`: `bool`; `tracing_span_id`: `Option` } | +| `PrepareStoreViewRequest` | struct | [`PrepareStoreViewRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2983) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `StoreSyncRequest` | struct | [`StoreSyncRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3003) | struct { `vm_id`: `VmId`; `bundle_closure_ref`: `BundleClosureRef`; `generation_token`: `u32`; `tracing_span_id`: `Option` } | +| `SetBridgePortFlagsRequest` | struct | [`SetBridgePortFlagsRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3047) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `SetSocketAclRequest` | struct | [`SetSocketAclRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3056) | struct { `bundle_socket_intent_ref`: `BundleOpId`; `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `SetupMountNamespaceRequest` | struct | [`SetupMountNamespaceRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3066) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `UpdateHostsFileRequest` | struct | [`UpdateHostsFileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3077) | struct { `bundle_hosts_intent_ref`: `BundleOpId`; `destroy`: `bool`; `tracing_span_id`: `Option` } | +| `UsbipBindRequest` | struct | [`UsbipBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3090) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `UsbipBindFirewallRuleRequest` | struct | [`UsbipBindFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3107) | struct { `bundle_usbip_firewall_intent_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `UsbipProxyReconcileRequest` | struct | [`UsbipProxyReconcileRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3115) | struct { `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `UsbipUnbindRequest` | struct | [`UsbipUnbindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3123) | struct { `bundle_usbip_bind_intent_ref`: `BundleOpId`; `preserve_durable_claim`: `bool`; `tracing_span_id`: `Option` } | +| `UsbipExplicitBindRequest` | struct | [`UsbipExplicitBindRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3145) | struct { `bus_id`: `String`; `vm`: `String`; `env`: `String`; `tracing_span_id`: `Option` } | +| `UsbipExplicitFirewallRuleRequest` | struct | [`UsbipExplicitFirewallRuleRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3163) | struct { `bus_id`: `String`; `env`: `String`; `host_uplink_ip`: `String`; `net_uplink_ip`: `String`; `tracing_span_id`: `Option` } | +| `ResourceActivationAuditRequest` | struct | [`ResourceActivationAuditRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3255) | struct { `audit_join`: `AuditJoinContext` } | +| `SignalRunnerRequest` | struct | [`SignalRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3286) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `signal`: `RunnerSignal`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `guest_execution`: `Option`; `tracing_span_id`: `Option` } | +| `CgroupKillRequest` | struct | [`CgroupKillRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3316) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `tracing_span_id`: `Option` } | +| `DeregisterRunnerPidfdRequest` | struct | [`DeregisterRunnerPidfdRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3325) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `Option`; `expected_start_time_ticks`: `Option`; `resource_ref`: `Option`; `resource_uid`: `Option`; `guest_execution`: `Option`; `tracing_span_id`: `Option` } | +| `SpawnRunnerRequest` | struct | [`SpawnRunnerRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3459) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `resource_ref`: `Option`; `resource_uid`: `Option`; `bundle_content_identity`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `activation_input`: `Option`; `sandbox_plan`: `Option`; `role`: `RunnerRole`; `bundle_runner_intent_ref`: `BundleOpId`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `guest_execution`: `Option`; `runtime_allocations`: `Vec`; `tracing_span_id`: `Option`; `workload_identity`: `Option` } | +| `SeedDnsmasqLeaseRequest` | struct | [`SeedDnsmasqLeaseRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3742) | struct { `vm_id`: `VmId`; `scope_id`: `ScopeId`; `tracing_span_id`: `Option` } | +| `BindMountFromHardlinkFarmRequest` | struct | [`BindMountFromHardlinkFarmRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3754) | struct { `vm_id`: `VmId`; `bundle_store_view_intent_ref`: `Option`; `tracing_span_id`: `Option` } | +| `OwnershipMatrixCheckRequest` | struct | [`OwnershipMatrixCheckRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3769) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `SshHostKeyPreflightRequest` | struct | [`SshHostKeyPreflightRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3780) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | +| `ReconcileStorageScopeRequest` | struct | [`ReconcileStorageScopeRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3794) | struct { `storage_ref`: `BundleOpId`; `apply`: `bool`; `tracing_span_id`: `Option` } | +| `ValidateLockSpecRequest` | struct | [`ValidateLockSpecRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3831) | struct { `lock_ref`: `BundleOpId`; `tracing_span_id`: `Option` } | +| `DiskInitRequest` | struct | [`DiskInitRequest`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3865) | struct { `vm_id`: `VmId`; `tracing_span_id`: `Option` } | ### Console and audio wire types @@ -400,77 +400,77 @@ see the auto-generated tables above for the committed Rust variants. | `PublicResponse` | enum | [`PublicResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L149) | `Capabilities` - (CapabilitiesResponse); `AuthStatus` - (AuthStatusResponse); `List` - (ListResponse); `Status` - (StatusResponse); `Audit` - (AuditResponse); `HostCheck` - (HostCheckResponse); `HostCutover` - (HostCutoverResponse); `KeysList` - (KeysListResponse); `KeysShow` - (KeysShowResponse); `UsbipProbe` - (UsbipProbeResponse); `StoreVerify` - (StoreVerifyResponse); `MutatingVerb` - (MutatingVerbResponse); `ReadGuestConfig` - (ReadGuestConfigResponse); `Exec` - (ExecOpResponse); `Console` - (ConsoleOpResponse); `Audio` - (AudioOpResponse); `GatewayDisplay` - (GatewayDisplayOpResponse); `Workload` - (WorkloadOpResponse); `UsbSecurityKeyStatus` - (d2b_contracts::security_key::SecurityKeyStatusResponse); `UsbSecurityKeySessions` - (d2b_contracts::security_key::SecurityKeySessionsResponse); `UsbSecurityKeyCancel` - (d2b_contracts::security_key::SecurityKeyCancelResponse); `Error` - (Error) | | `WorkloadOpResponse` | enum | [`WorkloadOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L215) | `List` - (WorkloadListResult); `Status` - (Box); `LauncherExec` - (LauncherExecResult) | | `GatewayDisplayOpResponse` | enum | [`GatewayDisplayOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L370) | `Start` - (GatewayDisplayStartResult); `Stop` - (GatewayDisplayStopResult); `Open` - (GatewayDisplayOpenResult); `Close` - (GatewayDisplayCloseResult); `List` - (GatewayDisplayListResult); `ListDetailed` - (GatewayDisplayListDetailedResult) | -| `ReadGuestConfigResponse` | struct | [`ReadGuestConfigResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L606) | struct { `content_base64`: `String` } | -| `ExecOpResponse` | enum | [`ExecOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1246) | `Start` - (ExecStartResult); `DetachedCreate` - (ExecDetachedCreateResult); `WriteStdin` - (ExecWriteStdinResult); `ReadOutput` - (ExecReadOutputResult); `Signal` - (ExecControlResult); `Resize` - (ExecControlResult); `Wait` - (ExecWaitResult); `Close` - (ExecCloseResult); `List` - (ExecDetachedListResult); `Logs` - (ExecDetachedLogsResult); `Status` - (ExecDetachedStatusResult); `Kill` - (ExecDetachedKillResult) | -| `ConsoleOpResponse` | enum | [`ConsoleOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1808) | `Attach` - (ConsoleAttachResult); `WriteStdin` - (ConsoleControlResult); `ReadOutput` - (ConsoleReadOutputResult); `Resize` - (ConsoleControlResult); `Wait` - (ConsoleWaitResult); `Close` - (ConsoleCloseResult) | -| `AudioOpResponse` | enum | [`AudioOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2014) | `Status` - (AudioStatusResult); `SetVolume` - (AudioSetResult); `Mute` - (AudioSetResult) | -| `HostCutoverResponse` | struct | [`HostCutoverResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2180) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `state`: `String`; `phase`: `u8`; `preview_digest`: `Option`; `summary`: `String`; `mutation_accepted`: `bool`; `inventory`: `Option` } | -| `MutatingVerbResponse` | struct | [`MutatingVerbResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2241) | struct { `verb`: `String`; `outcome`: `MutatingVerbOutcome`; `target_wave`: `Option`; `summary`: `Option`; `remediation`: `Option`; `api_ready`: `Option` } | -| `CapabilitiesResponse` | struct | [`CapabilitiesResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2267) | struct { `broker_socket`: `String`; `capabilities`: `Vec`; `public_socket`: `String`; `server_version`: `Version`; `selected_version`: `Version` } | -| `AuthStatusResponse` | struct | [`AuthStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2277) | struct { `allowed_subcommands`: `Vec`; `denied_subcommands`: `Vec`; `role`: `AuthRole`; `sockets`: `Vec` } | -| `ListResponse` | struct | [`ListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2286) | struct { `vms`: `Vec`; `read_model`: `Option` } | -| `StatusResponse` | struct | [`StatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2294) | struct { `entries`: `Vec`; `read_model`: `Option` } | -| `AuditResponse` | struct | [`AuditResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2314) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | -| `HostCheckResponse` | struct | [`HostCheckResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2364) | struct { `exit_code`: `u8`; `findings`: `Vec` } | -| `KeysListResponse` | struct | [`KeysListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2382) | struct { `entries`: `Vec` } | -| `KeysShowResponse` | struct | [`KeysShowResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2388) | struct { `vm`: `String`; `env`: `Option`; `managed_key_path`: `String`; `public_key`: `String`; `fingerprint`: `String`; `known_hosts_entry`: `Option` } | -| `UsbipProbeResponse` | struct | [`UsbipProbeResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2629) | struct { `entries`: `Vec` } | -| `UsbSecurityKeyStatusResponse` | struct | [`UsbSecurityKeyStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L3963) | struct { `host_proxy_enabled`: `bool`; `physical_keys`: `Vec`; `vm_devices`: `Vec`; `lease`: `UsbSkLeaseStatus` } | -| `UsbSecurityKeySessionsResponse` | struct | [`UsbSecurityKeySessionsResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4025) | struct { `sessions`: `Vec` } | -| `UsbSecurityKeyCancelResponse` | struct | [`UsbSecurityKeyCancelResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4043) | struct { `cancelled_session_id`: `Option`; `already_idle`: `bool` } | -| `UsbSecurityKeyTestResponse` | struct | [`UsbSecurityKeyTestResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4080) | struct { `vm`: `String`; `ok`: `bool`; `checks`: `Vec` } | +| `ReadGuestConfigResponse` | struct | [`ReadGuestConfigResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L609) | struct { `content_base64`: `String` } | +| `ExecOpResponse` | enum | [`ExecOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1249) | `Start` - (ExecStartResult); `DetachedCreate` - (ExecDetachedCreateResult); `WriteStdin` - (ExecWriteStdinResult); `ReadOutput` - (ExecReadOutputResult); `Signal` - (ExecControlResult); `Resize` - (ExecControlResult); `Wait` - (ExecWaitResult); `Close` - (ExecCloseResult); `List` - (ExecDetachedListResult); `Logs` - (ExecDetachedLogsResult); `Status` - (ExecDetachedStatusResult); `Kill` - (ExecDetachedKillResult) | +| `ConsoleOpResponse` | enum | [`ConsoleOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L1811) | `Attach` - (ConsoleAttachResult); `WriteStdin` - (ConsoleControlResult); `ReadOutput` - (ConsoleReadOutputResult); `Resize` - (ConsoleControlResult); `Wait` - (ConsoleWaitResult); `Close` - (ConsoleCloseResult) | +| `AudioOpResponse` | enum | [`AudioOpResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2017) | `Status` - (AudioStatusResult); `SetVolume` - (AudioSetResult); `Mute` - (AudioSetResult) | +| `HostCutoverResponse` | struct | [`HostCutoverResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2183) | struct { `operation`: `HostCutoverOperation`; `operation_id`: `Option`; `state`: `String`; `phase`: `u8`; `preview_digest`: `Option`; `summary`: `String`; `mutation_accepted`: `bool`; `inventory`: `Option` } | +| `MutatingVerbResponse` | struct | [`MutatingVerbResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2244) | struct { `verb`: `String`; `outcome`: `MutatingVerbOutcome`; `target_wave`: `Option`; `summary`: `Option`; `remediation`: `Option`; `api_ready`: `Option` } | +| `CapabilitiesResponse` | struct | [`CapabilitiesResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2270) | struct { `broker_socket`: `String`; `capabilities`: `Vec`; `public_socket`: `String`; `server_version`: `Version`; `selected_version`: `Version` } | +| `AuthStatusResponse` | struct | [`AuthStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2280) | struct { `allowed_subcommands`: `Vec`; `denied_subcommands`: `Vec`; `role`: `AuthRole`; `sockets`: `Vec` } | +| `ListResponse` | struct | [`ListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2289) | struct { `vms`: `Vec`; `read_model`: `Option` } | +| `StatusResponse` | struct | [`StatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2297) | struct { `entries`: `Vec`; `read_model`: `Option` } | +| `AuditResponse` | struct | [`AuditResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2317) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | +| `HostCheckResponse` | struct | [`HostCheckResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2367) | struct { `exit_code`: `u8`; `findings`: `Vec` } | +| `KeysListResponse` | struct | [`KeysListResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2385) | struct { `entries`: `Vec` } | +| `KeysShowResponse` | struct | [`KeysShowResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2391) | struct { `vm`: `String`; `env`: `Option`; `managed_key_path`: `String`; `public_key`: `String`; `fingerprint`: `String`; `known_hosts_entry`: `Option` } | +| `UsbipProbeResponse` | struct | [`UsbipProbeResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L2632) | struct { `entries`: `Vec` } | +| `UsbSecurityKeyStatusResponse` | struct | [`UsbSecurityKeyStatusResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L3966) | struct { `host_proxy_enabled`: `bool`; `physical_keys`: `Vec`; `vm_devices`: `Vec`; `lease`: `UsbSkLeaseStatus` } | +| `UsbSecurityKeySessionsResponse` | struct | [`UsbSecurityKeySessionsResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4028) | struct { `sessions`: `Vec` } | +| `UsbSecurityKeyCancelResponse` | struct | [`UsbSecurityKeyCancelResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4046) | struct { `cancelled_session_id`: `Option`; `already_idle`: `bool` } | +| `UsbSecurityKeyTestResponse` | struct | [`UsbSecurityKeyTestResponse`](../../packages/d2b-contracts-control/src/public_wire.rs#L4083) | struct { `vm`: `String`; `ok`: `bool`; `checks`: `Vec` } | ### Broker socket response types | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `ApplyHostGenerationHandoffResponse` | struct | [`ApplyHostGenerationHandoffResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L332) | struct { `target`: `d2b_contracts_resource::v3::ResourceRef`; `state`: `crate::host_generation::HandoffState`; `source_generation`: `u64`; `target_generation`: `u64`; `source_remains_usable`: `bool`; `summary`: `String` } | -| `LaunchCutoverRunnerResponse` | struct | [`LaunchCutoverRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L359) | struct { `operation_id`: `BundleOpId`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `Option` } | -| `CutoverAuditResponse` | struct | [`CutoverAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L426) | struct { `record_id`: `CanonicalAuditDigest` } | -| `CutoverVerificationResponse` | struct | [`CutoverVerificationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L521) | struct { `zones`: `Vec`; `sources_preserved`: `bool`; `identity_digests_match`: `bool`; `candidate_current`: `bool` } | -| `CutoverAdmissionResponse` | struct | [`CutoverAdmissionResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L533) | struct { `candidate_current`: `bool`; `markers_valid`: `bool`; `ownership_valid`: `bool`; `predicates_hold`: `bool` } | -| `CutoverEffectResponse` | struct | [`CutoverEffectResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L634) | struct { `outcome`: `CutoverEffectOutcome`; `identity`: `Option`; `verification`: `Option`; `admission`: `Option`; `audit_record_id`: `CanonicalAuditDigest` } | -| `RunHostInstallResponse` | struct | [`RunHostInstallResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1490) | struct { `installed`: `bool`; `enabled`: `bool`; `started`: `bool`; `artifacts_written`: `Vec` } | -| `RunMigrateResponse` | struct | [`RunMigrateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1508) | struct { `migrated_vm_count`: `u32`; `notes`: `Vec` } | -| `RunActivationResponse` | struct | [`RunActivationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1548) | struct { `mode`: `ActivationMode`; `vm`: `String`; `generation_number`: `Option`; `guest_switch_script_path`: `Option`; `summary`: `String` } | -| `RunGcResponse` | struct | [`RunGcResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1570) | struct { `keep_generations`: `Option`; `retained_store_path_count`: `u32`; `summary`: `String` } | -| `RunKeysRotateResponse` | struct | [`RunKeysRotateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1588) | struct { `vm`: `String`; `key_path`: `String`; `public_key_fingerprint`: `String` } | -| `RunHostKeyTrustResponse` | struct | [`RunHostKeyTrustResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1605) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `updated`: `bool` } | -| `RunRotateKnownHostResponse` | struct | [`RunRotateKnownHostResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1623) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `removed`: `bool` } | -| `BrokerResponse` | enum | [`BrokerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1645) | `ApplyHostGenerationHandoff` - (ApplyHostGenerationHandoffResponse); `LaunchCutoverRunner` - (LaunchCutoverRunnerResponse); `CutoverAudit` - (CutoverAuditResponse); `CutoverEffect` - (CutoverEffectResponse); `Ack` - (AckResponse); `CreatePersistentTap` - (TapReadyResponse); `CreateTapFd` - (TapReadyResponse); `Error` - (BrokerErrorResponse); `ExportBrokerAudit` - (ExportBrokerAuditResponse); `RunHostInstall` - (RunHostInstallResponse); `RunMigrate` - (RunMigrateResponse); `RunActivation` - (RunActivationResponse); `RunGc` - (RunGcResponse); `RunKeysRotate` - (RunKeysRotateResponse); `RunHostKeyTrust` - (RunHostKeyTrustResponse); `RunRotateKnownHost` - (RunRotateKnownHostResponse); `Hello` - (HelloResponse); `GuestControlSign` - (GuestControlSignResponse); `QemuMediaEnroll` - (QemuMediaEnrollResponse); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryResponse); `QemuMediaBoot` - (QemuMediaHotplugResponse); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleResponse); `QemuMediaQueryStatus` - (QemuMediaQueryStatusResponse); `QemuMediaQuit` - (QemuMediaLifecycleResponse); `QemuMediaAttach` - (QemuMediaHotplugResponse); `QemuMediaDetach` - (QemuMediaHotplugResponse); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyResponse); `OpenPidfd` - (OpenPidfdResponse); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketResponse); `ObserveRunner` - (ObserveRunnerResponse); `PipeWireAudio` - (PipeWireAudioResponse); `StartSystemdUnit` - (StartSystemdUnitResponse); `CheckSystemdUserManager` - (CheckSystemdUserManagerResponse); `ObserveSystemdUnit` - (ObserveSystemdUnitResponse); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdResponse); `StopSystemdUnit` - (StopSystemdUnitResponse); `OpenZoneStore` - (OpenZoneStoreResponse); `ResourceActivationAudit` - (ResourceActivationAuditResponse); `PollChildReaped` - (PollChildReapedResponse); `ReconcileStorageScope` - (ReconcileStorageScopeResponse); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateResponse); `SetBridgePortFlags` - (BridgePortFlagsResponse); `SignalRunner` - (SignalRunnerResponse); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdResponse); `SpawnRunner` - (SpawnRunnerResponse); `StoreSync` - (StoreSyncResponse); `StoreVerify` - (StoreVerifyResponse); `ValidateLockSpec` - (ValidateLockSpecResponse); `ValidateBundle` - (ValidateBundleResponse) | -| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1749) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | -| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1776) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | -| `GuestControlSignResponse` | struct | [`GuestControlSignResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2124) | struct { `tag`: `Vec` } | -| `QemuMediaEnrollResponse` | struct | [`QemuMediaEnrollResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2209) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `read_only`: `bool`; `enrolled`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | -| `QemuMediaRefreshRegistryResponse` | struct | [`QemuMediaRefreshRegistryResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2227) | struct { `record_count`: `u32`; `redacted_index_written`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | -| `QemuMediaLifecycleResponse` | struct | [`QemuMediaLifecycleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2303) | struct { `vm_id`: `VmId`; `command`: `QemuMediaLifecycleAction` } | -| `QemuMediaQueryStatusResponse` | struct | [`QemuMediaQueryStatusResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2310) | struct { `vm_id`: `VmId`; `status`: `QemuMediaVmStatus` } | -| `QemuMediaHotplugResponse` | struct | [`QemuMediaHotplugResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2353) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `slot`: `String`; `read_only`: `bool`; `qmp_commands`: `Vec`; `events`: `Vec` } | -| `OpenPidfdResponse` | struct | [`OpenPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2408) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `verified_start_time_ticks`: `u64`; `pidfd_index`: `u32` } | -| `OpenPeerPidfdFromAcceptedSocketResponse` | struct | [`OpenPeerPidfdFromAcceptedSocketResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2429) | struct { `pidfd_index`: `u32` } | -| `ObserveRunnerResponse` | struct | [`ObserveRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2456) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `pid`: `i32`; `start_time_ticks`: `u64`; `cgroup_verified`: `bool`; `executable_verified`: `bool` } | -| `PipeWireAudioResponse` | struct | [`PipeWireAudioResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2508) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `applied`: `bool`; `host_ready`: `bool`; `node_present`: `bool` } | -| `StartTransientUnitResponse` | struct | [`StartTransientUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2643) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | -| `CheckSystemdUserManagerResponse` | struct | [`CheckSystemdUserManagerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2655) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `available`: `bool` } | -| `ObserveSystemdUnitResponse` | struct | [`ObserveSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2664) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `identity`: `Option` } | -| `OpenSystemdUnitPidfdResponse` | struct | [`OpenSystemdUnitPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2675) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | -| `StopSystemdUnitResponse` | struct | [`StopSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2685) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `stopped`: `bool` } | -| `OpenZoneStoreResponse` | struct | [`OpenZoneStoreResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2721) | struct { `zone_store_id`: `ZoneStoreId`; `store_identity`: `String`; `disposition`: `ZoneStoreDisposition`; `fd_index`: `u32` } | -| `OpenHidrawSecurityKeyResponse` | struct | [`OpenHidrawSecurityKeyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2780) | struct { `selector_resolved`: `String`; `device_class`: `String` } | -| `MigrateLegacySwtpmStateResponse` | struct | [`MigrateLegacySwtpmStateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2850) | struct { `outcome`: `LegacySwtpmMigrationOutcome` } | -| `StoreSyncResponse` | struct | [`StoreSyncResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2897) | struct { `vm`: `String`; `generation_id`: `String`; `generation_token`: `u32`; `hardlink_farm_path`: `String`; `closure_count`: `u32`; `retained_generations`: `Vec`; `swept_count`: `u32`; `cleanup_deferred`: `bool` } | -| `AckResponse` | struct | [`AckResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3051) | struct { `accepted`: `bool`; `operation`: `String` } | -| `TapReadyResponse` | struct | [`TapReadyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3058) | struct { `bridge`: `Option`; `tap`: `IfName` } | -| `ExportBrokerAuditResponse` | struct | [`ExportBrokerAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3065) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | -| `BridgePortFlagsResponse` | struct | [`BridgePortFlagsResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3110) | struct { `bridge`: `IfName`; `isolated`: `bool`; `neigh_suppress`: `bool`; `port`: `IfName` } | -| `ValidateBundleResponse` | struct | [`ValidateBundleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3119) | struct { `valid`: `bool` } | -| `ResourceActivationAuditResponse` | struct | [`ResourceActivationAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3141) | struct { `recorded`: `bool` } | -| `SignalRunnerResponse` | struct | [`SignalRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3178) | struct { `signaled`: `bool`; `vm_id`: `VmId`; `role_id`: `RoleId` } | -| `DeregisterRunnerPidfdResponse` | struct | [`DeregisterRunnerPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3214) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `removed`: `bool` } | -| `SpawnRunnerResponse` | struct | [`SpawnRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3424) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `bundle_content_identity`: `Option`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `u32`; `console_fd_index`: `Option` } | -| `ReconcileStorageScopeResponse` | struct | [`ReconcileStorageScopeResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3670) | struct { `storage_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `status`: `StorageReconcileStatus`; `applied`: `bool`; `path_hash`: `String` } | -| `ValidateLockSpecResponse` | struct | [`ValidateLockSpecResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3694) | struct { `lock_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `cloexec_required`: `bool`; `fd_passing_mechanism`: `String`; `order_key`: `String` } | -| `PollChildReapedResponse` | struct | [`PollChildReapedResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3781) | struct { `notifications`: `Vec` } | +| `ApplyHostGenerationHandoffResponse` | struct | [`ApplyHostGenerationHandoffResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L333) | struct { `target`: `d2b_contracts_resource::v3::ResourceRef`; `state`: `crate::host_generation::HandoffState`; `source_generation`: `u64`; `target_generation`: `u64`; `source_remains_usable`: `bool`; `summary`: `String` } | +| `LaunchCutoverRunnerResponse` | struct | [`LaunchCutoverRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L360) | struct { `operation_id`: `BundleOpId`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `Option` } | +| `CutoverAuditResponse` | struct | [`CutoverAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L427) | struct { `record_id`: `CanonicalAuditDigest` } | +| `CutoverVerificationResponse` | struct | [`CutoverVerificationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L522) | struct { `zones`: `Vec`; `sources_preserved`: `bool`; `identity_digests_match`: `bool`; `candidate_current`: `bool` } | +| `CutoverAdmissionResponse` | struct | [`CutoverAdmissionResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L534) | struct { `candidate_current`: `bool`; `markers_valid`: `bool`; `ownership_valid`: `bool`; `predicates_hold`: `bool` } | +| `CutoverEffectResponse` | struct | [`CutoverEffectResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L635) | struct { `outcome`: `CutoverEffectOutcome`; `identity`: `Option`; `verification`: `Option`; `admission`: `Option`; `audit_record_id`: `CanonicalAuditDigest` } | +| `RunHostInstallResponse` | struct | [`RunHostInstallResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1576) | struct { `installed`: `bool`; `enabled`: `bool`; `started`: `bool`; `artifacts_written`: `Vec` } | +| `RunMigrateResponse` | struct | [`RunMigrateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1594) | struct { `migrated_vm_count`: `u32`; `notes`: `Vec` } | +| `RunActivationResponse` | struct | [`RunActivationResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1634) | struct { `mode`: `ActivationMode`; `vm`: `String`; `generation_number`: `Option`; `guest_switch_script_path`: `Option`; `summary`: `String` } | +| `RunGcResponse` | struct | [`RunGcResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1656) | struct { `keep_generations`: `Option`; `retained_store_path_count`: `u32`; `summary`: `String` } | +| `RunKeysRotateResponse` | struct | [`RunKeysRotateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1674) | struct { `vm`: `String`; `key_path`: `String`; `public_key_fingerprint`: `String` } | +| `RunHostKeyTrustResponse` | struct | [`RunHostKeyTrustResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1691) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `updated`: `bool` } | +| `RunRotateKnownHostResponse` | struct | [`RunRotateKnownHostResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1709) | struct { `vm`: `String`; `static_ip`: `String`; `known_hosts_path`: `String`; `removed`: `bool` } | +| `BrokerResponse` | enum | [`BrokerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1731) | `ApplyHostGenerationHandoff` - (ApplyHostGenerationHandoffResponse); `LaunchCutoverRunner` - (LaunchCutoverRunnerResponse); `CutoverAudit` - (CutoverAuditResponse); `CutoverEffect` - (CutoverEffectResponse); `Ack` - (AckResponse); `CreatePersistentTap` - (TapReadyResponse); `CreateTapFd` - (TapReadyResponse); `Error` - (BrokerErrorResponse); `ExportBrokerAudit` - (ExportBrokerAuditResponse); `RunHostInstall` - (RunHostInstallResponse); `RunMigrate` - (RunMigrateResponse); `RunActivation` - (RunActivationResponse); `RunGc` - (RunGcResponse); `RunKeysRotate` - (RunKeysRotateResponse); `RunHostKeyTrust` - (RunHostKeyTrustResponse); `RunRotateKnownHost` - (RunRotateKnownHostResponse); `Hello` - (HelloResponse); `GuestControlSign` - (GuestControlSignResponse); `QemuMediaEnroll` - (QemuMediaEnrollResponse); `QemuMediaRefreshRegistry` - (QemuMediaRefreshRegistryResponse); `QemuMediaBoot` - (QemuMediaHotplugResponse); `QemuMediaSystemPowerdown` - (QemuMediaLifecycleResponse); `QemuMediaQueryStatus` - (QemuMediaQueryStatusResponse); `QemuMediaQuit` - (QemuMediaLifecycleResponse); `QemuMediaAttach` - (QemuMediaHotplugResponse); `QemuMediaDetach` - (QemuMediaHotplugResponse); `OpenHidrawSecurityKey` - (OpenHidrawSecurityKeyResponse); `OpenPidfd` - (OpenPidfdResponse); `OpenPeerPidfdFromAcceptedSocket` - (OpenPeerPidfdFromAcceptedSocketResponse); `ObserveRunner` - (ObserveRunnerResponse); `PipeWireAudio` - (PipeWireAudioResponse); `StartSystemdUnit` - (StartSystemdUnitResponse); `CheckSystemdUserManager` - (CheckSystemdUserManagerResponse); `ObserveSystemdUnit` - (ObserveSystemdUnitResponse); `OpenSystemdUnitPidfd` - (OpenSystemdUnitPidfdResponse); `StopSystemdUnit` - (StopSystemdUnitResponse); `OpenZoneStore` - (OpenZoneStoreResponse); `ResourceActivationAudit` - (ResourceActivationAuditResponse); `PollChildReaped` - (PollChildReapedResponse); `ReconcileStorageScope` - (ReconcileStorageScopeResponse); `MigrateLegacySwtpmState` - (MigrateLegacySwtpmStateResponse); `SetBridgePortFlags` - (BridgePortFlagsResponse); `SignalRunner` - (SignalRunnerResponse); `DeregisterRunnerPidfd` - (DeregisterRunnerPidfdResponse); `SpawnRunner` - (SpawnRunnerResponse); `StoreSync` - (StoreSyncResponse); `StoreVerify` - (StoreVerifyResponse); `ValidateLockSpec` - (ValidateLockSpecResponse); `ValidateBundle` - (ValidateBundleResponse) | +| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1835) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | +| `HelloResponse` | struct | [`HelloResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1862) | struct { `server_version`: `String`; `selected_version`: `String`; `capabilities`: `Vec` } | +| `GuestControlSignResponse` | struct | [`GuestControlSignResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2210) | struct { `tag`: `Vec` } | +| `QemuMediaEnrollResponse` | struct | [`QemuMediaEnrollResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2295) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `read_only`: `bool`; `enrolled`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | +| `QemuMediaRefreshRegistryResponse` | struct | [`QemuMediaRefreshRegistryResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2313) | struct { `record_count`: `u32`; `redacted_index_written`: `bool`; `udev_rule_written`: `bool`; `udev_reloaded`: `bool` } | +| `QemuMediaLifecycleResponse` | struct | [`QemuMediaLifecycleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2389) | struct { `vm_id`: `VmId`; `command`: `QemuMediaLifecycleAction` } | +| `QemuMediaQueryStatusResponse` | struct | [`QemuMediaQueryStatusResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2396) | struct { `vm_id`: `VmId`; `status`: `QemuMediaVmStatus` } | +| `QemuMediaHotplugResponse` | struct | [`QemuMediaHotplugResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2439) | struct { `vm_id`: `VmId`; `media_ref`: `MediaRef`; `slot`: `String`; `read_only`: `bool`; `qmp_commands`: `Vec`; `events`: `Vec` } | +| `OpenPidfdResponse` | struct | [`OpenPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2526) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `pid`: `i32`; `verified_start_time_ticks`: `u64`; `pidfd_index`: `u32` } | +| `OpenPeerPidfdFromAcceptedSocketResponse` | struct | [`OpenPeerPidfdFromAcceptedSocketResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2547) | struct { `pidfd_index`: `u32` } | +| `ObserveRunnerResponse` | struct | [`ObserveRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2577) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `pid`: `i32`; `start_time_ticks`: `u64`; `cgroup_verified`: `bool`; `executable_verified`: `bool` } | +| `PipeWireAudioResponse` | struct | [`PipeWireAudioResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2629) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `applied`: `bool`; `host_ready`: `bool`; `node_present`: `bool` } | +| `StartTransientUnitResponse` | struct | [`StartTransientUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2770) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | +| `CheckSystemdUserManagerResponse` | struct | [`CheckSystemdUserManagerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2782) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `available`: `bool` } | +| `ObserveSystemdUnitResponse` | struct | [`ObserveSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2791) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `present`: `bool`; `identity`: `Option` } | +| `OpenSystemdUnitPidfdResponse` | struct | [`OpenSystemdUnitPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2802) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `identity`: `SystemdUnitIdentity`; `pidfd_index`: `u32` } | +| `StopSystemdUnitResponse` | struct | [`StopSystemdUnitResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2812) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `stopped`: `bool` } | +| `OpenZoneStoreResponse` | struct | [`OpenZoneStoreResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2848) | struct { `zone_store_id`: `ZoneStoreId`; `store_identity`: `String`; `disposition`: `ZoneStoreDisposition`; `fd_index`: `u32` } | +| `OpenHidrawSecurityKeyResponse` | struct | [`OpenHidrawSecurityKeyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2907) | struct { `selector_resolved`: `String`; `device_class`: `String` } | +| `MigrateLegacySwtpmStateResponse` | struct | [`MigrateLegacySwtpmStateResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2977) | struct { `outcome`: `LegacySwtpmMigrationOutcome` } | +| `StoreSyncResponse` | struct | [`StoreSyncResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3024) | struct { `vm`: `String`; `generation_id`: `String`; `generation_token`: `u32`; `hardlink_farm_path`: `String`; `closure_count`: `u32`; `retained_generations`: `Vec`; `swept_count`: `u32`; `cleanup_deferred`: `bool` } | +| `AckResponse` | struct | [`AckResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3178) | struct { `accepted`: `bool`; `operation`: `String` } | +| `TapReadyResponse` | struct | [`TapReadyResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3185) | struct { `bridge`: `Option`; `tap`: `IfName` } | +| `ExportBrokerAuditResponse` | struct | [`ExportBrokerAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3192) | struct { `entries`: `Vec`; `next_cursor`: `Option`; `complete`: `bool` } | +| `BridgePortFlagsResponse` | struct | [`BridgePortFlagsResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3237) | struct { `bridge`: `IfName`; `isolated`: `bool`; `neigh_suppress`: `bool`; `port`: `IfName` } | +| `ValidateBundleResponse` | struct | [`ValidateBundleResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3246) | struct { `valid`: `bool` } | +| `ResourceActivationAuditResponse` | struct | [`ResourceActivationAuditResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3268) | struct { `recorded`: `bool` } | +| `SignalRunnerResponse` | struct | [`SignalRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3308) | struct { `signaled`: `bool`; `vm_id`: `VmId`; `role_id`: `RoleId` } | +| `DeregisterRunnerPidfdResponse` | struct | [`DeregisterRunnerPidfdResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3347) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `removed`: `bool` } | +| `SpawnRunnerResponse` | struct | [`SpawnRunnerResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3567) | struct { `vm_id`: `VmId`; `role_id`: `RoleId`; `role`: `RunnerRole`; `execution_ref`: `Option`; `execution_domain`: `Option`; `user_ref`: `Option`; `guest_execution`: `Option`; `provider_identity`: `Option<[u8; 32]>`; `template_identity`: `Option<[u8; 32]>`; `generation`: `Option`; `bundle_content_identity`: `Option`; `pid`: `i32`; `start_time_ticks`: `u64`; `pidfd_index`: `u32`; `console_fd_index`: `Option` } | +| `ReconcileStorageScopeResponse` | struct | [`ReconcileStorageScopeResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3815) | struct { `storage_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `status`: `StorageReconcileStatus`; `applied`: `bool`; `path_hash`: `String` } | +| `ValidateLockSpecResponse` | struct | [`ValidateLockSpecResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3839) | struct { `lock_ref`: `BundleOpId`; `scope`: `String`; `kind`: `String`; `cloexec_required`: `bool`; `fd_passing_mechanism`: `String`; `order_key`: `String` } | +| `PollChildReapedResponse` | struct | [`PollChildReapedResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3926) | struct { `notifications`: `Vec` } | ## Per-VM lifecycle state @@ -538,42 +538,42 @@ running live guest activation. | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `VmLifecycleState` | enum | [`VmLifecycleState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2789) | `Stopped`; `Starting`; `Booted`; `Running`; `Stopping`; `Restarting`; `Failed`; `Unknown` | +| `VmLifecycleState` | enum | [`VmLifecycleState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2792) | `Stopped`; `Starting`; `Booted`; `Running`; `Stopping`; `Restarting`; `Failed`; `Unknown` | ### Other documented enums | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `CutoverAuditTransition` | enum | [`CutoverAuditTransition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L374) | `HoldRequested`; `HoldCleared`; `PhaseStarted`; `PhaseCompleted`; `EffectStarted`; `EffectCompleted`; `Terminal` | -| `CutoverEffectAuthority` | enum | [`CutoverEffectAuthority`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L434) | `Cutover`; `ResetZone`; `ResetProvider`; `ResetGuest` | -| `CutoverEffectKind` | enum | [`CutoverEffectKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L448) | `HostDrain`; `CutoverDisposition`; `ResourceStoreCreate`; `ProviderInstall`; `ZoneActivation`; `GuestActivation`; `Verification`; `CutoverFinalization`; `ScopedZoneReset`; `ScopedProviderReset`; `ScopedGuestReset`; `DestroyDurableVolume`; `PreserveSource`; `QuarantineDestination`; `CutoverBroker`; `ClosureActivation`; `ApplyAdmission` | -| `CutoverEffectPayload` | enum | [`CutoverEffectPayload`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L474) | `None`; `ApplyAdmission` - (CutoverAdmissionRequest); `Storage` - (ReconcileStorageScopeRequest); `ZoneStore` - (OpenZoneStoreRequest); `StoreSync` - (StoreSyncRequest); `StoreVerify` - (StoreVerifyRequest); `Verification` - (CutoverVerificationRequest); `Activation` - (RunActivationRequest); `Systemd` - (Box); `Quarantine` - struct { `staged_id`: `BundleOpId`; `source_id`: `BundleOpId`; `marker_digest`: `CanonicalAuditDigest` }; `Finalization` - struct { `artifacts`: `Vec`; `disposition_digest`: `CanonicalAuditDigest`; `consent_digest`: `CanonicalAuditDigest` }; `DestroyDurableVolume` - struct { `storage_ref`: `BundleOpId`; `marker_digest`: `CanonicalAuditDigest`; `consent_digest`: `CanonicalAuditDigest` } | -| `CutoverReplayClass` | enum | [`CutoverReplayClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L553) | `Repeatable`; `ReopenByJournaledIdentity`; `QuarantineOnly` | -| `CutoverEffectOutcome` | enum | [`CutoverEffectOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L625) | `Succeeded`; `Failed`; `Ambiguous` | -| `BrokerProfile` | enum | [`BrokerProfile`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1287) | `Host`; `Guest` | -| `ActivationMode` | enum | [`ActivationMode`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1515) | `Switch`; `Boot`; `Test`; `Rollback` | -| `ActivationPhase` | enum | [`ActivationPhase`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1524) | `Prepare`; `Commit`; `MetadataOnly` | -| `NftablesProjectionAction` | enum | [`NftablesProjectionAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1807) | `Apply`; `Remove` | -| `BrokerAuditSeverity` | enum | [`BrokerAuditSeverity`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2010) | `Info`; `Warning`; `Error`; `Denied` | -| `GuestControlProofRole` | enum | [`GuestControlProofRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2023) | `HostProof`; `GuestProof` | -| `GuestControlDirection` | enum | [`GuestControlDirection`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2030) | `HostToGuest` | -| `GuestControlAuthPurpose` | enum | [`GuestControlAuthPurpose`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2036) | `GuestControlAuthV1` | -| `QemuMediaLifecycleAction` | enum | [`QemuMediaLifecycleAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2272) | `SystemPowerdown`; `Quit` | -| `QemuMediaVmStatus` | enum | [`QemuMediaVmStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2279) | `Running`; `Paused`; `Shutdown`; `Suspended`; `Watchdog`; `Debug`; `Inmigrate`; `InternalError`; `IoError`; `Postmigrate`; `Prelaunch`; `FinishMigrate`; `RestoreVm`; `SaveVm`; `GuestPanicked`; `Colo`; `Preconfig`; `Unknown`; `ConnectionLostDuringShutdown` | -| `QemuMediaHotplugStatus` | enum | [`QemuMediaHotplugStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2332) | `IdentityResolved`; `QmpConnected`; `QmpCapabilities`; `FdAdded`; `BlockdevAdded`; `DeviceAdded`; `DeviceDeleted`; `BlockdevDeleted`; `FdRemoved`; `VmContinued` | -| `PipeWireAudioChannel` | enum | [`PipeWireAudioChannel`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2469) | `Speaker`; `Microphone` | -| `PipeWireAudioAction` | enum | [`PipeWireAudioAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2479) | `SetGrant` - struct { `on`: `bool` }; `SetLevel` - struct { `percent`: `u8` } | -| `SystemdUnitDomain` | enum | [`SystemdUnitDomain`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2523) | `System`; `User` | -| `SystemdStopClass` | enum | [`SystemdStopClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2633) | `Drain`; `Terminate` | -| `ZoneStoreDisposition` | enum | [`ZoneStoreDisposition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2703) | `Provisioned`; `Opened` | -| `LegacySwtpmMigrationOutcome` | enum | [`LegacySwtpmMigrationOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2818) | `Migrated`; `AlreadyMigrated`; `NotApplicable`; `Pending`; `Failed`; `Ambiguous`; `AdoptionRequired`; `NeverProvisioned` | -| `RunnerSignal` | enum | [`RunnerSignal`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3151) | `Term`; `Kill`; `Quit` | -| `RunnerRole` | enum | [`RunnerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3233) | `CloudHypervisor`; `QemuMedia`; `Virtiofsd`; `Swtpm`; `SwtpmFlush`; `Gpu`; `Audio`; `Video`; `VsockRelay`; `Usbip`; `OtelHostBridge`; `WaylandProxy` | -| `RunnerAllocationKind` | enum | [`RunnerAllocationKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3402) | `VsockCid`; `TapFdSlot`; `ApiSocketPath` | -| `BrokerCallerRole` | enum | [`BrokerCallerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3544) | `AdminUid` - struct { `uid`: `u32` }; `LauncherUid` - struct { `uid`: `u32` }; `RootUid` - struct { `uid`: `u32` }; `CutoverRunner` - struct { `operation_id`: `BundleOpId`; `capability_digest`: `CanonicalAuditDigest` }; `NotAuthorized` | -| `StorageReconcileStatus` | enum | [`StorageReconcileStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3659) | `Clean`; `Created`; `Reused`; `CheckedOnly`; `TemplateUnexpanded`; `Refused` | -| `ChildExitKind` | enum | [`ChildExitKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3733) | `Exited`; `Signaled`; `Killed` | -| `BrokerNotification` | enum | [`BrokerNotification`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3771) | `ChildReaped` - (ChildReapedNotification); `Unknown` | +| `CutoverAuditTransition` | enum | [`CutoverAuditTransition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L375) | `HoldRequested`; `HoldCleared`; `PhaseStarted`; `PhaseCompleted`; `EffectStarted`; `EffectCompleted`; `Terminal` | +| `CutoverEffectAuthority` | enum | [`CutoverEffectAuthority`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L435) | `Cutover`; `ResetZone`; `ResetProvider`; `ResetGuest` | +| `CutoverEffectKind` | enum | [`CutoverEffectKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L449) | `HostDrain`; `CutoverDisposition`; `ResourceStoreCreate`; `ProviderInstall`; `ZoneActivation`; `GuestActivation`; `Verification`; `CutoverFinalization`; `ScopedZoneReset`; `ScopedProviderReset`; `ScopedGuestReset`; `DestroyDurableVolume`; `PreserveSource`; `QuarantineDestination`; `CutoverBroker`; `ClosureActivation`; `ApplyAdmission` | +| `CutoverEffectPayload` | enum | [`CutoverEffectPayload`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L475) | `None`; `ApplyAdmission` - (CutoverAdmissionRequest); `Storage` - (ReconcileStorageScopeRequest); `ZoneStore` - (OpenZoneStoreRequest); `StoreSync` - (StoreSyncRequest); `StoreVerify` - (StoreVerifyRequest); `Verification` - (CutoverVerificationRequest); `Activation` - (RunActivationRequest); `Systemd` - (Box); `Quarantine` - struct { `staged_id`: `BundleOpId`; `source_id`: `BundleOpId`; `marker_digest`: `CanonicalAuditDigest` }; `Finalization` - struct { `artifacts`: `Vec`; `disposition_digest`: `CanonicalAuditDigest`; `consent_digest`: `CanonicalAuditDigest` }; `DestroyDurableVolume` - struct { `storage_ref`: `BundleOpId`; `marker_digest`: `CanonicalAuditDigest`; `consent_digest`: `CanonicalAuditDigest` } | +| `CutoverReplayClass` | enum | [`CutoverReplayClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L554) | `Repeatable`; `ReopenByJournaledIdentity`; `QuarantineOnly` | +| `CutoverEffectOutcome` | enum | [`CutoverEffectOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L626) | `Succeeded`; `Failed`; `Ambiguous` | +| `BrokerProfile` | enum | [`BrokerProfile`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1288) | `Host`; `Guest` | +| `ActivationMode` | enum | [`ActivationMode`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1601) | `Switch`; `Boot`; `Test`; `Rollback` | +| `ActivationPhase` | enum | [`ActivationPhase`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1610) | `Prepare`; `Commit`; `MetadataOnly` | +| `NftablesProjectionAction` | enum | [`NftablesProjectionAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1893) | `Apply`; `Remove` | +| `BrokerAuditSeverity` | enum | [`BrokerAuditSeverity`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2096) | `Info`; `Warning`; `Error`; `Denied` | +| `GuestControlProofRole` | enum | [`GuestControlProofRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2109) | `HostProof`; `GuestProof` | +| `GuestControlDirection` | enum | [`GuestControlDirection`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2116) | `HostToGuest` | +| `GuestControlAuthPurpose` | enum | [`GuestControlAuthPurpose`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2122) | `GuestControlAuthV1` | +| `QemuMediaLifecycleAction` | enum | [`QemuMediaLifecycleAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2358) | `SystemPowerdown`; `Quit` | +| `QemuMediaVmStatus` | enum | [`QemuMediaVmStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2365) | `Running`; `Paused`; `Shutdown`; `Suspended`; `Watchdog`; `Debug`; `Inmigrate`; `InternalError`; `IoError`; `Postmigrate`; `Prelaunch`; `FinishMigrate`; `RestoreVm`; `SaveVm`; `GuestPanicked`; `Colo`; `Preconfig`; `Unknown`; `ConnectionLostDuringShutdown` | +| `QemuMediaHotplugStatus` | enum | [`QemuMediaHotplugStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2418) | `IdentityResolved`; `QmpConnected`; `QmpCapabilities`; `FdAdded`; `BlockdevAdded`; `DeviceAdded`; `DeviceDeleted`; `BlockdevDeleted`; `FdRemoved`; `VmContinued` | +| `PipeWireAudioChannel` | enum | [`PipeWireAudioChannel`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2590) | `Speaker`; `Microphone` | +| `PipeWireAudioAction` | enum | [`PipeWireAudioAction`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2600) | `SetGrant` - struct { `on`: `bool` }; `SetLevel` - struct { `percent`: `u8` } | +| `SystemdUnitDomain` | enum | [`SystemdUnitDomain`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2644) | `System`; `User` | +| `SystemdStopClass` | enum | [`SystemdStopClass`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2760) | `Drain`; `Terminate` | +| `ZoneStoreDisposition` | enum | [`ZoneStoreDisposition`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2830) | `Provisioned`; `Opened` | +| `LegacySwtpmMigrationOutcome` | enum | [`LegacySwtpmMigrationOutcome`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L2945) | `Migrated`; `AlreadyMigrated`; `NotApplicable`; `Pending`; `Failed`; `Ambiguous`; `AdoptionRequired`; `NeverProvisioned` | +| `RunnerSignal` | enum | [`RunnerSignal`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3278) | `Term`; `Kill`; `Quit` | +| `RunnerRole` | enum | [`RunnerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3366) | `CloudHypervisor`; `QemuMedia`; `ActivationNixos`; `Virtiofsd`; `Swtpm`; `SwtpmFlush`; `Gpu`; `Audio`; `Video`; `VsockRelay`; `Usbip`; `OtelHostBridge`; `WaylandProxy` | +| `RunnerAllocationKind` | enum | [`RunnerAllocationKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3545) | `VsockCid`; `TapFdSlot`; `ApiSocketPath` | +| `BrokerCallerRole` | enum | [`BrokerCallerRole`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3689) | `AdminUid` - struct { `uid`: `u32` }; `LauncherUid` - struct { `uid`: `u32` }; `RootUid` - struct { `uid`: `u32` }; `CutoverRunner` - struct { `operation_id`: `BundleOpId`; `capability_digest`: `CanonicalAuditDigest` }; `NotAuthorized` | +| `StorageReconcileStatus` | enum | [`StorageReconcileStatus`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3804) | `Clean`; `Created`; `Reused`; `CheckedOnly`; `TemplateUnexpanded`; `Refused` | +| `ChildExitKind` | enum | [`ChildExitKind`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3878) | `Exited`; `Signaled`; `Killed` | +| `BrokerNotification` | enum | [`BrokerNotification`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L3916) | `ChildReaped` - (ChildReapedNotification); `Unknown` | | `HandoffCallerRole` | enum | [`HandoffCallerRole`](../../packages/d2b-contracts-broker/src/host_generation.rs#L131) | `Lifecycle`; `Admin` | | `HandoffState` | enum | [`HandoffState`](../../packages/d2b-contracts-broker/src/host_generation.rs#L175) | `Recorded`; `Validated`; `Mutating`; `Transferred`; `Completed`; `RolledBack`; `Refused` | | `AuthDirection` | enum | [`AuthDirection`](../../packages/d2b-contracts-control/src/guest_auth.rs#L13) | `HostToGuest` | @@ -613,37 +613,37 @@ running live guest activation. | `GraphicalLaunchPosture` | enum | [`GraphicalLaunchPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L261) | `Proxied`; `NotApplicable`; `GraphicalSessionInactive`; `WaylandUnavailable`; `ProxyUnavailable` | | `LauncherExecDisposition` | enum | [`LauncherExecDisposition`](../../packages/d2b-contracts-control/src/public_wire.rs#L302) | `Committed`; `AlreadyCommitted` | | `GatewayDisplayOp` | enum | [`GatewayDisplayOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L318) | `Start` - (GatewayDisplayStartArgs); `Stop` - (GatewayDisplayStopArgs); `Open` - (GatewayDisplayOpenArgs); `Close` - (GatewayDisplayCloseArgs); `List` - (GatewayDisplayListArgs); `ListDetailed` - (GatewayDisplayListArgs) | -| `ExecStream` | enum | [`ExecStream`](../../packages/d2b-contracts-control/src/public_wire.rs#L620) | `Stdout`; `Stderr` | -| `ExecOp` | enum | [`ExecOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L911) | `Start` - (ExecStartArgs); `WriteStdin` - (ExecWriteStdinArgs); `ReadOutput` - (ExecReadOutputArgs); `Signal` - (ExecSignalArgs); `Resize` - (ExecResizeArgs); `Wait` - (ExecWaitArgs); `Close` - (ExecCloseArgs); `List` - (ExecDetachedListArgs); `Logs` - (ExecDetachedLogsArgs); `Status` - (ExecDetachedStatusArgs); `Kill` - (ExecDetachedKillArgs) | -| `ExecTerminalStatus` | enum | [`ExecTerminalStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L1027) | `Exited` - struct { `code`: `i32` }; `Signaled` - struct { `signal`: `u32` }; `Error` - struct { `slug`: `String` } | -| `ExecDetachedKillOutcome` | enum | [`ExecDetachedKillOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L1218) | `Cancelling`; `AlreadyTerminal` | -| `ShellSessionState` | enum | [`ShellSessionState`](../../packages/d2b-contracts-control/src/public_wire.rs#L1412) | `Attached`; `Detached`; `Killed`; `PoolUnavailable`; `FeatureDisabled`; `OutputGap` | -| `ShellCloseCause` | enum | [`ShellCloseCause`](../../packages/d2b-contracts-control/src/public_wire.rs#L1423) | `ClientDetach`; `EvictedByForce`; `EvictedByAdminDetach`; `KilledByAdmin`; `PoolUnavailable`; `OutputGap` | -| `ConsoleProviderKind` | enum | [`ConsoleProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1533) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | -| `ConsoleOp` | enum | [`ConsoleOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1676) | `Attach` - (ConsoleAttachArgs); `WriteStdin` - (ConsoleWriteStdinArgs); `ReadOutput` - (ConsoleReadOutputArgs); `Resize` - (ConsoleResizeArgs); `Wait` - (ConsoleWaitArgs); `Close` - (ConsoleCloseArgs) | -| `AudioChannel` | enum | [`AudioChannel`](../../packages/d2b-contracts-control/src/public_wire.rs#L1822) | `Speaker`; `Microphone` | -| `AudioEnforcementPosture` | enum | [`AudioEnforcementPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L1836) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | -| `AudioProviderKind` | enum | [`AudioProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1878) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | -| `AudioOp` | enum | [`AudioOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1923) | `Status` - (AudioStatusArgs); `SetVolume` - (AudioSetVolumeArgs); `Mute` - (AudioMuteArgs) | -| `AudioSetApplied` | enum | [`AudioSetApplied`](../../packages/d2b-contracts-control/src/public_wire.rs#L1986) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | -| `HostCutoverOperation` | enum | [`HostCutoverOperation`](../../packages/d2b-contracts-control/src/public_wire.rs#L2142) | `Preview`; `Status`; `Hold`; `Resume`; `Apply`; `Rollback`; `Verify`; `Doctor`; `Finalize`; `Reset` | -| `HostCutoverResetScope` | enum | [`HostCutoverResetScope`](../../packages/d2b-contracts-control/src/public_wire.rs#L2168) | `Zone`; `Provider`; `Guest` | -| `MutatingVerbOutcome` | enum | [`MutatingVerbOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L2256) | `DryRunPlanned`; `Applied`; `ApiReadyTimeout`; `NotYetImplemented`; `BrokerError`; `InvalidRequest` | -| `UsbipProbeStatus` | enum | [`UsbipProbeStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L2400) | `Bound`; `Unbound`; `Degraded`; `Enrollable`; `Enrolled`; `Stale`; `DirectConfig`; `Unknown` | -| `UsbipDurableClaimState` | enum | [`UsbipDurableClaimState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2418) | `Missing`; `HeldByDesiredOwner`; `HeldByOtherOwner`; `StaleOwner`; `Corrupt`; `NotApplicable`; `Unknown` | -| `UsbipHostBindState` | enum | [`UsbipHostBindState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2443) | `Unbound`; `BoundToUsbipHost`; `BoundToUnexpectedDriver`; `DeviceMissing`; `NotApplicable`; `Unknown` | -| `UsbipHostCarrierState` | enum | [`UsbipHostCarrierState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2456) | `Absent`; `Unavailable`; `WithheldForOwner`; `Ready`; `DepartedDuringProbe`; `NotApplicable`; `Unknown` | -| `UsbipProxyState` | enum | [`UsbipProxyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2470) | `NotDeclared`; `Stopped`; `Starting`; `Listening`; `Stale`; `Failed`; `NotApplicable`; `Unknown` | -| `UsbipGuestImportState` | enum | [`UsbipGuestImportState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2493) | `Detached`; `Imported`; `Unavailable`; `NotApplicable`; `Unknown` | -| `UsbipTopologyState` | enum | [`UsbipTopologyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2511) | `Match`; `Mismatch`; `Incomplete`; `NotObserved`; `NotApplicable`; `Unknown` | -| `UsbipPolicyState` | enum | [`UsbipPolicyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2524) | `Allowed`; `Denied`; `Missing`; `NotApplicable`; `Unknown` | -| `UsbipProbeDegradedReasonCode` | enum | [`UsbipProbeDegradedReasonCode`](../../packages/d2b-contracts-control/src/public_wire.rs#L2543) | `PolicyFailed`; `DeviceDepartedBeforeClaim`; `DeviceDepartedAfterLock`; `DeviceDepartedDuringMutation`; `DeviceReappearedWithDifferentTopology`; `LockHeldByOtherOwner`; `InvalidPersistedLockClaim`; `CarrierUnavailable`; `HostBindUnavailable`; `ProxyUnavailable`; `GuestImportUnavailable`; `StaleHostState`; `StaleGuestState`; `ProbeIncomplete`; `Unknown` | -| `UsbProbeEntryKind` | enum | [`UsbProbeEntryKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2579) | `Usbip`; `QemuMediaSlot` | -| `AuditFormat` | enum | [`AuditFormat`](../../packages/d2b-contracts-control/src/public_wire.rs#L2649) | `Human`; `Json` | -| `AuthRole` | enum | [`AuthRole`](../../packages/d2b-contracts-control/src/public_wire.rs#L2657) | `None`; `Launcher`; `Admin` | -| `HostFindingSeverity` | enum | [`HostFindingSeverity`](../../packages/d2b-contracts-control/src/public_wire.rs#L2878) | `Pass`; `Warn`; `Fail` | -| `UsbSkLeaseState` | enum | [`UsbSkLeaseState`](../../packages/d2b-contracts-control/src/public_wire.rs#L3930) | `Idle`; `Active`; `Queued`; `Unknown` | -| `UsbSkSessionOutcome` | enum | [`UsbSkSessionOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L3985) | `Success`; `Timeout`; `Cancelled`; `DeviceUnavailable`; `Active`; `Unknown` | +| `ExecStream` | enum | [`ExecStream`](../../packages/d2b-contracts-control/src/public_wire.rs#L623) | `Stdout`; `Stderr` | +| `ExecOp` | enum | [`ExecOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L914) | `Start` - (ExecStartArgs); `WriteStdin` - (ExecWriteStdinArgs); `ReadOutput` - (ExecReadOutputArgs); `Signal` - (ExecSignalArgs); `Resize` - (ExecResizeArgs); `Wait` - (ExecWaitArgs); `Close` - (ExecCloseArgs); `List` - (ExecDetachedListArgs); `Logs` - (ExecDetachedLogsArgs); `Status` - (ExecDetachedStatusArgs); `Kill` - (ExecDetachedKillArgs) | +| `ExecTerminalStatus` | enum | [`ExecTerminalStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L1030) | `Exited` - struct { `code`: `i32` }; `Signaled` - struct { `signal`: `u32` }; `Error` - struct { `slug`: `String` } | +| `ExecDetachedKillOutcome` | enum | [`ExecDetachedKillOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L1221) | `Cancelling`; `AlreadyTerminal` | +| `ShellSessionState` | enum | [`ShellSessionState`](../../packages/d2b-contracts-control/src/public_wire.rs#L1415) | `Attached`; `Detached`; `Killed`; `PoolUnavailable`; `FeatureDisabled`; `OutputGap` | +| `ShellCloseCause` | enum | [`ShellCloseCause`](../../packages/d2b-contracts-control/src/public_wire.rs#L1426) | `ClientDetach`; `EvictedByForce`; `EvictedByAdminDetach`; `KilledByAdmin`; `PoolUnavailable`; `OutputGap` | +| `ConsoleProviderKind` | enum | [`ConsoleProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1536) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | +| `ConsoleOp` | enum | [`ConsoleOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1679) | `Attach` - (ConsoleAttachArgs); `WriteStdin` - (ConsoleWriteStdinArgs); `ReadOutput` - (ConsoleReadOutputArgs); `Resize` - (ConsoleResizeArgs); `Wait` - (ConsoleWaitArgs); `Close` - (ConsoleCloseArgs) | +| `AudioChannel` | enum | [`AudioChannel`](../../packages/d2b-contracts-control/src/public_wire.rs#L1825) | `Speaker`; `Microphone` | +| `AudioEnforcementPosture` | enum | [`AudioEnforcementPosture`](../../packages/d2b-contracts-control/src/public_wire.rs#L1839) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | +| `AudioProviderKind` | enum | [`AudioProviderKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1881) | `LocalHypervisor`; `QemuMedia`; `AcaSandbox` | +| `AudioOp` | enum | [`AudioOp`](../../packages/d2b-contracts-control/src/public_wire.rs#L1926) | `Status` - (AudioStatusArgs); `SetVolume` - (AudioSetVolumeArgs); `Mute` - (AudioMuteArgs) | +| `AudioSetApplied` | enum | [`AudioSetApplied`](../../packages/d2b-contracts-control/src/public_wire.rs#L1989) | `HostAndGuest`; `HostOnly`; `GuestOnly`; `Unsupported` | +| `HostCutoverOperation` | enum | [`HostCutoverOperation`](../../packages/d2b-contracts-control/src/public_wire.rs#L2145) | `Preview`; `Status`; `Hold`; `Resume`; `Apply`; `Rollback`; `Verify`; `Doctor`; `Finalize`; `Reset` | +| `HostCutoverResetScope` | enum | [`HostCutoverResetScope`](../../packages/d2b-contracts-control/src/public_wire.rs#L2171) | `Zone`; `Provider`; `Guest` | +| `MutatingVerbOutcome` | enum | [`MutatingVerbOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L2259) | `DryRunPlanned`; `Applied`; `ApiReadyTimeout`; `NotYetImplemented`; `BrokerError`; `InvalidRequest` | +| `UsbipProbeStatus` | enum | [`UsbipProbeStatus`](../../packages/d2b-contracts-control/src/public_wire.rs#L2403) | `Bound`; `Unbound`; `Degraded`; `Enrollable`; `Enrolled`; `Stale`; `DirectConfig`; `Unknown` | +| `UsbipDurableClaimState` | enum | [`UsbipDurableClaimState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2421) | `Missing`; `HeldByDesiredOwner`; `HeldByOtherOwner`; `StaleOwner`; `Corrupt`; `NotApplicable`; `Unknown` | +| `UsbipHostBindState` | enum | [`UsbipHostBindState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2446) | `Unbound`; `BoundToUsbipHost`; `BoundToUnexpectedDriver`; `DeviceMissing`; `NotApplicable`; `Unknown` | +| `UsbipHostCarrierState` | enum | [`UsbipHostCarrierState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2459) | `Absent`; `Unavailable`; `WithheldForOwner`; `Ready`; `DepartedDuringProbe`; `NotApplicable`; `Unknown` | +| `UsbipProxyState` | enum | [`UsbipProxyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2473) | `NotDeclared`; `Stopped`; `Starting`; `Listening`; `Stale`; `Failed`; `NotApplicable`; `Unknown` | +| `UsbipGuestImportState` | enum | [`UsbipGuestImportState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2496) | `Detached`; `Imported`; `Unavailable`; `NotApplicable`; `Unknown` | +| `UsbipTopologyState` | enum | [`UsbipTopologyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2514) | `Match`; `Mismatch`; `Incomplete`; `NotObserved`; `NotApplicable`; `Unknown` | +| `UsbipPolicyState` | enum | [`UsbipPolicyState`](../../packages/d2b-contracts-control/src/public_wire.rs#L2527) | `Allowed`; `Denied`; `Missing`; `NotApplicable`; `Unknown` | +| `UsbipProbeDegradedReasonCode` | enum | [`UsbipProbeDegradedReasonCode`](../../packages/d2b-contracts-control/src/public_wire.rs#L2546) | `PolicyFailed`; `DeviceDepartedBeforeClaim`; `DeviceDepartedAfterLock`; `DeviceDepartedDuringMutation`; `DeviceReappearedWithDifferentTopology`; `LockHeldByOtherOwner`; `InvalidPersistedLockClaim`; `CarrierUnavailable`; `HostBindUnavailable`; `ProxyUnavailable`; `GuestImportUnavailable`; `StaleHostState`; `StaleGuestState`; `ProbeIncomplete`; `Unknown` | +| `UsbProbeEntryKind` | enum | [`UsbProbeEntryKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L2582) | `Usbip`; `QemuMediaSlot` | +| `AuditFormat` | enum | [`AuditFormat`](../../packages/d2b-contracts-control/src/public_wire.rs#L2652) | `Human`; `Json` | +| `AuthRole` | enum | [`AuthRole`](../../packages/d2b-contracts-control/src/public_wire.rs#L2660) | `None`; `Launcher`; `Admin` | +| `HostFindingSeverity` | enum | [`HostFindingSeverity`](../../packages/d2b-contracts-control/src/public_wire.rs#L2881) | `Pass`; `Warn`; `Fail` | +| `UsbSkLeaseState` | enum | [`UsbSkLeaseState`](../../packages/d2b-contracts-control/src/public_wire.rs#L3933) | `Idle`; `Active`; `Queued`; `Unknown` | +| `UsbSkSessionOutcome` | enum | [`UsbSkSessionOutcome`](../../packages/d2b-contracts-control/src/public_wire.rs#L3988) | `Success`; `Timeout`; `Cancelled`; `DeviceUnavailable`; `Active`; `Unknown` | | `TerminalStream` | enum | [`TerminalStream`](../../packages/d2b-contracts-control/src/terminal_wire.rs#L19) | `Stdout`; `Stderr` | | `TerminalStatus` | enum | [`TerminalStatus`](../../packages/d2b-contracts-control/src/terminal_wire.rs#L197) | `Exited` - struct { `code`: `i32` }; `Signaled` - struct { `signal`: `u32` }; `Error` - struct { `slug`: `String` } | | `HelperScopeKind` | enum | [`HelperScopeKind`](../../packages/d2b-contracts-control/src/unsafe_local_wire.rs#L125) | `LauncherApp`; `WaylandProxy`; `PersistentShell` | @@ -741,13 +741,13 @@ the failure class, for example `host check`, `audit`, `status`, or | Type | Kind | Rust definition | Shape | | --- | --- | --- | --- | -| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1749) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | +| `BrokerErrorResponse` | struct | [`BrokerErrorResponse`](../../packages/d2b-contracts-broker/src/broker_wire.rs#L1835) | struct { `kind`: `String`; `operation`: `String`; `target_wave`: `Option`; `message`: `String`; `action`: `String` } | | `HandoffError` | enum | [`HandoffError`](../../packages/d2b-contracts-broker/src/host_generation.rs#L194) | `CompatibilityFloorInvalid`; `GenerationTooOld`; `TargetFingerprintMismatch`; `TargetGenerationMismatch`; `GenerationAncestryInvalid`; `InvalidTransition` | | `GuestControlError` | struct | [`GuestControlError`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1489) | struct { `kind`: `GuestControlErrorKind`; `remediation`: `HealthRemediation`; `retry_after_ms`: `Option` } | | `GuestControlErrorKind` | enum | [`GuestControlErrorKind`](../../packages/d2b-contracts-control/src/guest_wire.rs#L1497) | `ProtocolError`; `MaxChunkExceeded`; `StdinBackpressure`; `StdinClosed`; `StdinNotOpen`; `StdinClosedByProcess`; `StdinOffsetMismatch`; `StdinByteBudgetExhausted`; `OffsetExpired`; `OffsetInFuture`; `OffsetExhausted`; `OutputLost`; `TtyStderrUnavailable`; `TtyRequired`; `ExecCapacityExceeded`; `ExecAttachCapacityExceeded`; `ExecNotFound`; `ExecAlreadyExited`; `GuestExecDisabled`; `GuestExecRootDenied`; `GuestExecUserDenied`; `CwdInvalid`; `CwdDenied`; `RetainedLogPathUnsafe`; `RetainedLogQuotaExceeded`; `ReadWaitCapacityExceeded`; `WaitCapacityExceeded`; `SupersededReadWait`; `RateLimited`; `RequestIdConflict`; `ControlSeqMismatch`; `SlowConsumerCancelled`; `StaleSession`; `GuestControlUnavailableOldGeneration`; `AuthFailed`; `TransportUnreachable`; `ExecExpired`; `FileNotFound`; `FileTooLarge`; `PathUnsafe`; `ReadDenied`; `InvalidProgram`; `UsbipUnavailable`; `UsbipCommandFailed`; `UsbipInvalidBusId`; `UsbipInvalidHost`; `GuestShellDisabled`; `ShellInvalidName`; `ShellCapacityExceeded`; `ShellAttachCapacityExceeded`; `ShellNotFound`; `ShellAlreadyAttached`; `ShellPoolUnavailable`; `ShellDaemonEpochMismatch`; `ShellOutputGap`; `UsbipCommandTimeout`; `UsbipInvalidOutput`; `ActivationInvalidId`; `ActivationInvalidPath`; `ActivationInvalidMode`; `ActivationNotFound`; `ActivationStatusUnavailable`; `ActivationTimedOut`; `ActivationSpawnFailed`; `AudioPipeWireUnavailable`; `AudioChannelUnknown`; `AudioLevelOutOfRange`; `AudioEnforcementFailed` | -| `ShellNameError` | struct | [`ShellNameError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1281) | empty struct | -| `AudioErrorKind` | enum | [`AudioErrorKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1859) | `ProviderMisconfigured`; `VmNotFound`; `EnforcementUnavailable`; `AudioNotEnabled`; `InternalError` | -| `AudioVmError` | struct | [`AudioVmError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1960) | struct { `vm`: `String`; `kind`: `AudioErrorKind`; `remediation`: `Option` } | +| `ShellNameError` | struct | [`ShellNameError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1284) | empty struct | +| `AudioErrorKind` | enum | [`AudioErrorKind`](../../packages/d2b-contracts-control/src/public_wire.rs#L1862) | `ProviderMisconfigured`; `VmNotFound`; `EnforcementUnavailable`; `AudioNotEnabled`; `InternalError` | +| `AudioVmError` | struct | [`AudioVmError`](../../packages/d2b-contracts-control/src/public_wire.rs#L1963) | struct { `vm`: `String`; `kind`: `AudioErrorKind`; `remediation`: `Option` } | | `LevelPercentError` | enum | [`LevelPercentError`](../../packages/d2b-contracts/src/audio.rs#L28) | `OutOfRange` - (u8) | | `AudioPolicyError` | enum | [`AudioPolicyError`](../../packages/d2b-contracts/src/audio.rs#L217) | `InvalidJson` - (String); `InvalidField` - (String); `UnknownSchemaVersion` - (String); `Serialize` - (String) | | `AuditExportErrorCode` | enum | [`AuditExportErrorCode`](../../packages/d2b-contracts/src/audit_wire.rs#L20) | `HashBreak`; `RecordInvalid`; `ReadFailed` | diff --git a/docs/reference/schemas/v2/processes.json b/docs/reference/schemas/v2/processes.json index 41578d32b7..b490fb4cf3 100644 --- a/docs/reference/schemas/v2/processes.json +++ b/docs/reference/schemas/v2/processes.json @@ -357,7 +357,7 @@ ] }, "executionRef": { - "description": "Canonical Host or Guest execution target for this trusted runner.\n\nOmitted by legacy bundles, which resolve to `Guest/`.", + "description": "Canonical Host or Guest execution target for this trusted runner.\n\nOmitted by legacy bundles, which resolve to the singleton Host target except for the Guest-local activation runner.", "type": [ "string", "null" @@ -501,6 +501,13 @@ "qemu-media-runner" ] }, + { + "description": "Target-local one-shot NixOS activation runner.\n\nThis role is emitted only for the Guest execution target. The activation Provider creates an EphemeralProcess resource and the target-local process Provider resolves this role from the trusted bundle.", + "type": "string", + "enum": [ + "activation-nixos-runner" + ] + }, { "description": "vsock relay sidecar.", "type": "string", diff --git a/docs/reference/schemas/v2/wire-protocol.json b/docs/reference/schemas/v2/wire-protocol.json index 97cfbd6114..724297c394 100644 --- a/docs/reference/schemas/v2/wire-protocol.json +++ b/docs/reference/schemas/v2/wire-protocol.json @@ -145,12 +145,54 @@ "default": false, "type": "boolean" }, + "toGeneration": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, "vm": { "type": "string" } }, "additionalProperties": false }, + "ActivationRunnerInput": { + "description": "Closed input delivered to an activation-runner `EphemeralProcess`.\n\nThe broker serializes this value into the bounded stdin envelope consumed by `d2b-activation-helper`. It deliberately carries no executable, store path, target path, or caller-controlled bytes.", + "type": "object", + "required": [ + "activationMode", + "systemArtifactId", + "targetGeneration" + ], + "properties": { + "activationMode": { + "description": "Closed activation mode.", + "allOf": [ + { + "$ref": "#/definitions/ActivationMode" + } + ] + }, + "systemArtifactId": { + "description": "Private-catalog artifact identifier.", + "allOf": [ + { + "$ref": "#/definitions/ArtifactId" + } + ] + }, + "targetGeneration": { + "description": "Target generation ordinal bound to the owning `NixosGeneration`.", + "type": "integer", + "format": "uint64", + "minimum": 1.0 + } + }, + "additionalProperties": false + }, "ApplyHostGenerationHandoff": { "description": "Typed broker request. The broker derives its target from this authenticated request and never accepts a path, command, or authority token.", "type": "object", @@ -5540,6 +5582,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process cleanup.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "pid": { "description": "Optional exact process identity. Deregistration must not remove a replacement runner that reused the VM/role tuple.", "type": [ @@ -7820,6 +7873,54 @@ }, "additionalProperties": false }, + "GuestExecutionBinding": { + "description": "Exact authenticated binding for a Guest-local Process lifecycle.\n\nThe values are commitments, not raw boot identifiers or transport handles. A Guest broker requires this tuple for every target-local Process operation and revalidates the boot commitment against its own kernel before executing the request.", + "type": "object", + "required": [ + "assignmentEpoch", + "bootIdentityDigest", + "controllerGeneration", + "providerGeneration", + "sessionGeneration", + "targetUid" + ], + "properties": { + "assignmentEpoch": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "bootIdentityDigest": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32 + }, + "controllerGeneration": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "providerGeneration": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "sessionGeneration": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "targetUid": { + "$ref": "#/definitions/ResourceUid" + } + }, + "additionalProperties": false + }, "HandoffCallerRole": { "description": "Caller-derived broker role for the handoff operation.", "oneOf": [ @@ -9394,6 +9495,17 @@ "bundleRunnerIntentRef": { "type": "string" }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process adoption.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "resourceRef": { "description": "Optional generic Process identity binding.", "anyOf": [ @@ -9685,6 +9797,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process adoption.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "pid": { "description": "PID the snapshot recorded.", "type": "integer", @@ -9823,6 +9946,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process operations.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "providerIdentity": { "description": "Process Provider identity digest.", "type": "array", @@ -12467,6 +12601,13 @@ "qemu-media" ] }, + { + "description": "Target-local one-shot NixOS activation runner. Guest mode may spawn this role only from the bundle-authoritative process intent.", + "type": "string", + "enum": [ + "activation-nixos" + ] + }, { "description": "virtiofsd sidecar; one per `microvm.shares` row. The daemon/bundle provides argv from `nixos-modules/processes-json.nix`.", "type": "string", @@ -13387,6 +13528,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process control.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "pid": { "type": [ "integer", @@ -13533,6 +13685,17 @@ "vmId" ], "properties": { + "activationInput": { + "description": "Typed stdin input admitted only for the activation-nixos runner role.", + "anyOf": [ + { + "$ref": "#/definitions/ActivationRunnerInput" + }, + { + "type": "null" + } + ] + }, "bundleContentIdentity": { "description": "Content identity of the daemon's trusted bundle snapshot.", "type": [ @@ -13576,6 +13739,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process launches.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "providerIdentity": { "description": "Provider/template identity expected by the daemon.", "type": [ @@ -13747,6 +13921,16 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "pid": { "description": "Child PID. The daemon validates this against the pidfd it received and against `/proc//stat` field 22 (`start_time`).", "type": "integer", @@ -13967,6 +14151,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process operations.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "providerIdentity": { "description": "Process Provider identity digest.", "type": "array", @@ -14366,6 +14561,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding, when this is Guest-local.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "invocationId": { "description": "systemd's 16-byte InvocationID.", "type": "array", @@ -14463,6 +14669,17 @@ "format": "uint64", "minimum": 0.0 }, + "guestExecution": { + "description": "Exact Guest target/session binding for target-local Process operations.", + "anyOf": [ + { + "$ref": "#/definitions/GuestExecutionBinding" + }, + { + "type": "null" + } + ] + }, "providerIdentity": { "description": "Process Provider identity digest.", "type": "array", diff --git a/docs/reference/schemas/v3/core.d2bus.org_EphemeralProcess.schema.json b/docs/reference/schemas/v3/core.d2bus.org_EphemeralProcess.schema.json index 69d9bc9230..d5cb67c5f2 100644 --- a/docs/reference/schemas/v3/core.d2bus.org_EphemeralProcess.schema.json +++ b/docs/reference/schemas/v3/core.d2bus.org_EphemeralProcess.schema.json @@ -3,6 +3,78 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "definitions": { + "ActivationMode": { + "description": "Target-local activation mode.", + "oneOf": [ + { + "description": "Activate immediately for the running target.", + "enum": [ + "switch" + ], + "type": "string" + }, + { + "description": "Select the generation for the next boot.", + "enum": [ + "boot" + ], + "type": "string" + }, + { + "description": "Activate temporarily until the next boot.", + "enum": [ + "test" + ], + "type": "string" + }, + { + "description": "Record an already active generation without running a helper.", + "enum": [ + "adopt" + ], + "type": "string" + } + ] + }, + "ActivationRunnerInput": { + "additionalProperties": false, + "description": "Closed input delivered to an activation-runner `EphemeralProcess`.\n\nThe broker serializes this value into the bounded stdin envelope consumed by `d2b-activation-helper`. It deliberately carries no executable, store path, target path, or caller-controlled bytes.", + "properties": { + "activationMode": { + "allOf": [ + { + "$ref": "#/definitions/ActivationMode" + } + ], + "description": "Closed activation mode." + }, + "systemArtifactId": { + "allOf": [ + { + "$ref": "#/definitions/ArtifactId" + } + ], + "description": "Private-catalog artifact identifier." + }, + "targetGeneration": { + "description": "Target generation ordinal bound to the owning `NixosGeneration`.", + "format": "uint64", + "minimum": 1.0, + "type": "integer" + } + }, + "required": [ + "activationMode", + "systemArtifactId", + "targetGeneration" + ], + "type": "object" + }, + "ArtifactId": { + "maxLength": 63, + "minLength": 1, + "type": "string" + }, "BoundedToken": { "maxLength": 63, "minLength": 1, @@ -544,6 +616,16 @@ "additionalProperties": false, "description": "The EphemeralProcess ResourceType base spec.\n\nEphemeralProcess is the one-shot process itself, never a job that references a Process, and it carries no restart, readiness, health check, or adoption field.", "properties": { + "activationInput": { + "anyOf": [ + { + "$ref": "#/definitions/ActivationRunnerInput" + }, + { + "type": "null" + } + ] + }, "budget": { "$ref": "#/definitions/BudgetSpec" }, diff --git a/nixos-modules/minijail-profiles.nix b/nixos-modules/minijail-profiles.nix index 9f7a2554a0..f15d38a0b4 100644 --- a/nixos-modules/minijail-profiles.nix +++ b/nixos-modules/minijail-profiles.nix @@ -347,6 +347,30 @@ let seccompPolicyRef = "w1-guest-control-health"; cgroupSubtree = "d2b.slice/${name}/guest-control-health"; }; + + "${profileIdFor name "activation-nixos-runner"}" = mkProfile { + profileId = profileIdFor name "activation-nixos-runner"; + role = "activation-nixos-runner"; + principal = "d2b-${name}-activation"; + uid = 0; + gid = 0; + seccompPolicyRef = "w1-activation-nixos-runner"; + namespaces = defaultNamespaces // { + pid = true; + }; + adr_carve_out = "Target-local activation helper requires its declared root identity inside the Guest sandbox."; + readOnlyPaths = [ + "/etc/d2b" + "/nix/store" + "/nix/var/nix/profiles" + "/run/current-system" + ]; + writablePaths = [ + (mkWritablePath (stateDirOf name) "Write only target-local activation state.") + ]; + cgroupSubtree = "d2b.slice/${name}/activation-nixos-runner"; + controllers = serviceControllers; + }; } // lib.optionalAttrs vm.tpm.enable { # Swtpm + SwtpmFlush minijail profiles. diff --git a/nixos-modules/processes-json.nix b/nixos-modules/processes-json.nix index 5cdbec90f0..962fbb1127 100644 --- a/nixos-modules/processes-json.nix +++ b/nixos-modules/processes-json.nix @@ -59,13 +59,6 @@ let unixSocketListening = mkReadiness "unix-socket-listening"; tcpPort = host: port: mkReadiness "tcp-port" { inherit host port; }; commandReady = mkReadiness "command"; - # Authenticated guest-control Health readiness. Unlike a raw TCP-22 - # probe this predicate fails CLOSED: the daemon completes a full - # Hello + token challenge-response + Health over the guest-control vsock - # before the node is ready. The daemon resolves the per-VM vsock socket, - # peer credentials, and broker-backed signer from its own trusted state. - guestControlHealthReady = vmName: { kind = "guest-control-health"; value = { vm = vmName; }; }; - extractOptValues = optFlag: extraArgs: let flags = if builtins.isList optFlag then optFlag else [ optFlag ]; @@ -893,6 +886,10 @@ use devices::virtio::vhost_user_backend::run_video_device;' assert (binaryPath == null) == (argv == [ ]); { inherit id role readiness; + executionRef = + if role == "activation-nixos-runner" + then "Guest/${name}" + else "Host/host-system"; profile = profileFor name id; } // lib.optionalAttrs emitUnit { inherit unit; } @@ -937,11 +934,17 @@ use devices::virtio::vhost_user_backend::run_video_device;' manifest = cfg.manifest.${name}; microvm = d2bLib.vmRunner config name; hypervisorService = d2bLib.runtimeHypervisorService "nixos"; - # The guest-control authenticated Health probe is the framework - # readiness gate on guest-control-capable VMs. Per-VM sshd/host-keys are - # retained for the SSH-compat window but are no longer the framework - # readiness signal, so a TCP-22 readiness node is no longer emitted. + # Guest sessions are authenticated independently from VM boot. Their + # evidence is projected through Guest/Endpoint status rather than a + # readiness-only health RPC. guestControlEnabled = vm.guest.control.enable; + activationRunner = name: { + binaryPath = "${d2bHostTools.activationHelper}/bin/d2b-activation-helper"; + argv = [ + "d2b-activation-helper" + "apply-generation" + ]; + }; virtiofsShares = lib.filter (share: (share.proto or "virtiofs") == "virtiofs") microvm.shares; @@ -1124,8 +1127,13 @@ use devices::virtio::vhost_user_backend::run_video_device;' ++ lib.optional guestControlEnabled (node name { id = "guest-control-health"; role = "guest-control-health"; - readiness = [ (guestControlHealthReady name) ]; + readiness = [ ]; }) + ++ lib.optional guestControlEnabled (mkRunnerNode name { + id = "activation-nixos-runner"; + role = "activation-nixos-runner"; + readiness = [ ]; + } (activationRunner name)) ++ lib.optional vm.usb.securityKey.enable (node name { # The sk-frontend DAG node tracks the readiness of the host-side # vsock socket endpoint that the guest frontend connects to. The @@ -1172,10 +1180,10 @@ use devices::virtio::vhost_user_backend::run_video_device;' edgesFromNodes optionalSidecarBaseNodeIds "audio" "The audio sidecar starts only after every prerequisite sidecar is ready." ) ++ edgesFromNodes preVmmNodeIds "cloud-hypervisor" "Cloud Hypervisor starts only after every prerequisite sidecar is ready." - ++ lib.optional guestControlEnabled - (edge "cloud-hypervisor" "guest-control-health" "Authenticated guest-control Health readiness is probed only after Cloud Hypervisor is running.") ++ lib.optional vm.usb.securityKey.enable - (edge "cloud-hypervisor" "sk-frontend" "The sk-frontend vsock endpoint tracking starts only after Cloud Hypervisor creates the base vsock socket."); + (edge "cloud-hypervisor" "sk-frontend" "The sk-frontend vsock endpoint tracking starts only after Cloud Hypervisor creates the base vsock socket.") + ++ lib.optional guestControlEnabled + (edge "cloud-hypervisor" "guest-control-health" "Guest ComponentSession health starts only after Cloud Hypervisor creates the Guest vsock endpoint."); invariants = { perVmAuditPipeline = true; swtpmPreStartFlush = true; diff --git a/nixos-modules/resources-zones-processes.nix b/nixos-modules/resources-zones-processes.nix index a9fa786dc1..f6c0152ff5 100644 --- a/nixos-modules/resources-zones-processes.nix +++ b/nixos-modules/resources-zones-processes.nix @@ -286,6 +286,7 @@ let Audio = "Provider/audio-pipewire"; CloudHypervisorRunner = "Provider/runtime-cloud-hypervisor"; QemuMediaRunner = "Provider/runtime-qemu-media"; + ActivationNixosRunner = "Provider/activation-nixos"; VsockRelay = "Provider/transport-vsock"; OtelHostBridge = "Provider/observability-otel"; GuestControlHealth = "Provider/system-core"; diff --git a/packages/d2b-broker/src/live_handlers.rs b/packages/d2b-broker/src/live_handlers.rs index 75dd867e16..37e5e80443 100644 --- a/packages/d2b-broker/src/live_handlers.rs +++ b/packages/d2b-broker/src/live_handlers.rs @@ -36,6 +36,9 @@ use crate::ops::spawn_runner::{ use crate::ops::store_sync::generation_id_for_intent; use crate::ops::store_view_posture::plant_live_marker_with_matrix_posture; use d2b_contracts_broker::broker_wire::{ActivationMode, ActivationPhase}; +use d2b_contracts_resource::v3::{ + ActivationRunnerInput, MAX_ACTIVATION_RUNNER_INPUT_BYTES, +}; use d2b_core::bundle_resolver::{HostRuntime, ResolvedActivationIntent, ResolvedStoreViewIntent}; use d2b_core::minijail_profile::CgroupPlacement; use d2b_host::hardlink_farm; @@ -1994,7 +1997,10 @@ pub(crate) fn policy_ref_device_classes( // terminal/file operations); guest-control-health is the daemon-side // authenticated Health probe, which speaks ttRPC over the guest-control // vsock and uses connect(2)/socket ioctls. - "w1-host-reconcile" | "w1-store-virtiofs-preflight" | "w1-guest-control-health" => { + "w1-host-reconcile" + | "w1-store-virtiofs-preflight" + | "w1-guest-control-health" + | "w1-activation-nixos-runner" => { Some(&[]) } // swtpm is a software TPM emulator; no hardware device ioctls, @@ -3491,6 +3497,7 @@ fn refresh_guest_control_fs_acl(plan: &SpawnRunnerPlan) -> Result<(), LiveHandle pub fn live_spawn_runner( plan_input: &SpawnRunnerPlanInput, mut pre_opened_device_fds: Vec, + activation_input: Option<&ActivationRunnerInput>, ) -> Result { let plan = preflight(plan_input).map_err(LiveHandlerError::SpawnPreflight)?; @@ -3568,6 +3575,20 @@ pub fn live_spawn_runner( let memlock_limit_bytes = memlock_guest_bytes.map(|guest_bytes| { guest_bytes.saturating_add(qemu_media_memlock_headroom_bytes(guest_bytes)) }); + let activation_stdin = activation_input + .map(serde_json::to_vec) + .transpose() + .map_err(|error| LiveHandlerError::SpawnFailed { + detail: format!("activation input serialization failed: {error}"), + })?; + if activation_stdin + .as_ref() + .is_some_and(|bytes| bytes.len() > MAX_ACTIVATION_RUNNER_INPUT_BYTES) + { + return Err(LiveHandlerError::SpawnFailed { + detail: "activation input exceeds bounded stdin envelope".to_owned(), + }); + } if let Some(guest_bytes) = memlock_guest_bytes { qemu_media_preflight_memlock_budget(qemu_media_memlock_preflight_required_bytes( guest_bytes, @@ -3606,6 +3627,7 @@ pub fn live_spawn_runner( // execve. pre_opened_device_fds, memlock_limit_bytes, + activation_stdin, }; let outcome = crate::sys::pidfd_sys::clone3_spawn_runner( @@ -5042,7 +5064,7 @@ mod tests { user_namespace: None, umask: None, }; - let err = live_spawn_runner(&plan, Vec::new()).unwrap_err(); + let err = live_spawn_runner(&plan, Vec::new(), None).unwrap_err(); assert!(matches!(err, LiveHandlerError::SpawnPreflight(_))); } @@ -5701,7 +5723,8 @@ mod tests { umask: None, }; - let outcome = live_spawn_runner(&plan, Vec::new()).expect("spawn privileged test child"); + let outcome = + live_spawn_runner(&plan, Vec::new(), None).expect("spawn privileged test child"); let wait_status = nix::sys::wait::waitpid(nix::unistd::Pid::from_raw(outcome.pid), None) .expect("wait for test child"); assert!(matches!( diff --git a/packages/d2b-broker/src/ops/systemd.rs b/packages/d2b-broker/src/ops/systemd.rs index 0ea823d746..121ceaa0ff 100644 --- a/packages/d2b-broker/src/ops/systemd.rs +++ b/packages/d2b-broker/src/ops/systemd.rs @@ -392,6 +392,7 @@ fn read_identity( template_identity: request.template_identity, generation: request.generation, bundle_content_identity: request.bundle_content_identity.clone(), + guest_execution: request.guest_execution.clone(), })) } @@ -610,6 +611,7 @@ mod tests { template_identity: [2; 32], generation: 3, domain: SystemdUnitDomain::System, + guest_execution: None, sandbox_plan: None, tracing_span_id: None, } diff --git a/packages/d2b-broker/src/runtime.rs b/packages/d2b-broker/src/runtime.rs index 55e2902a7e..ff96e1390c 100644 --- a/packages/d2b-broker/src/runtime.rs +++ b/packages/d2b-broker/src/runtime.rs @@ -30,8 +30,7 @@ use nix::unistd::{dup, geteuid}; #[cfg(not(feature = "layer1-bootstrap"))] use serde::{Deserialize, Serialize}; use serde_json::Value; -#[cfg(not(feature = "layer1-bootstrap"))] -use sha2::Sha256; +use sha2::{Digest, Sha256}; #[cfg(not(feature = "layer1-bootstrap"))] use tracing::info; use tracing::warn; @@ -61,7 +60,7 @@ use d2b_contracts_broker::broker_wire::BrokerProfile; #[cfg(not(feature = "layer1-bootstrap"))] use d2b_contracts_broker::broker_wire::{ AuditJoinContext, BrokerCallerRole as CallerRole, BrokerProfile, BrokerRequest, - BrokerRequestEnvelope as RequestEnvelope, BrokerResponse, CanonicalAuditDigest, + BrokerRequestEnvelope as RequestEnvelope, BrokerResponse, CanonicalAuditDigest, RunnerRole, }; #[cfg(feature = "layer1-bootstrap")] type AuditJoinContext = (); @@ -1779,6 +1778,13 @@ fn handle_connection( return Ok(()); } #[cfg(not(feature = "layer1-bootstrap"))] + if config.profile == BrokerProfile::Guest { + if let Err(error) = validate_guest_process_binding(&request) { + send_json_frame(fd.as_raw_fd(), &error.into_response())?; + return Ok(()); + } + } + #[cfg(not(feature = "layer1-bootstrap"))] let cutover_window = match cutover_window_active(config) { Ok(active) => active, Err(error) => { @@ -4318,6 +4324,7 @@ fn dispatch_request_with_backend_and_request_fds( req.resource_uid.as_ref(), req.pid, req.expected_start_time_ticks, + req.guest_execution.as_ref(), ) { return Err(BrokerError::NoPidfd { runner_id }); @@ -4395,6 +4402,7 @@ fn dispatch_request_with_backend_and_request_fds( req.resource_uid.as_ref(), req.pid, req.expected_start_time_ticks, + req.guest_execution.as_ref(), ) }) }); @@ -4472,6 +4480,56 @@ fn dispatch_request_with_backend_and_request_fds( resolved: "nonzero".to_owned(), }); } + match (&req.activation_input, req.role) { + (Some(input), RunnerRole::ActivationNixos) => { + if input.target_generation == 0 { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input.target_generation", + requested: "0".to_owned(), + resolved: "nonzero".to_owned(), + }); + } + let Some(_generation) = req.generation else { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "generation", + requested: "missing".to_owned(), + resolved: "required-for-activation-input".to_owned(), + }); + }; + let encoded = serde_json::to_vec(input).map_err(|_| { + BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input", + requested: "unserializable".to_owned(), + resolved: "bounded-json".to_owned(), + } + })?; + if encoded.len() + > d2b_contracts_resource::v3::MAX_ACTIVATION_RUNNER_INPUT_BYTES + { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input", + requested: encoded.len().to_string(), + resolved: d2b_contracts_resource::v3::MAX_ACTIVATION_RUNNER_INPUT_BYTES + .to_string(), + }); + } + } + (Some(_), _) => { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input", + requested: "present".to_owned(), + resolved: "activation-nixos-role-only".to_owned(), + }); + } + (None, RunnerRole::ActivationNixos) => { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "activation_input", + requested: "missing".to_owned(), + resolved: "required-for-activation-nixos".to_owned(), + }); + } + (None, _) => {} + } if req.resource_ref.is_some() && (req.execution_ref.is_none() || req.execution_domain.is_none() @@ -4722,6 +4780,7 @@ fn dispatch_request_with_backend_and_request_fds( execution_ref: req.execution_ref.clone(), execution_domain: req.execution_domain, user_ref: req.user_ref.clone(), + guest_execution: req.guest_execution.clone(), provider_identity: req.provider_identity, template_identity: req.template_identity, generation: req.generation, @@ -7739,6 +7798,7 @@ struct RunnerRegistration { start_time_ticks: u64, binary_path: PathBuf, cgroup_subtree: String, + guest_execution: Option, } #[cfg(not(feature = "layer1-bootstrap"))] @@ -7771,6 +7831,7 @@ fn register_runner_metadata( start_time_ticks, binary_path: intent.binary_path.clone(), cgroup_subtree: intent.cgroup_placement.subtree.clone(), + guest_execution: request.guest_execution.clone(), }, ); Ok(()) @@ -7805,6 +7866,7 @@ fn register_runner_metadata_from_open( start_time_ticks: request.expected_start_time_ticks, binary_path: intent.binary_path.clone(), cgroup_subtree: intent.cgroup_placement.subtree.clone(), + guest_execution: request.guest_execution.clone(), }, ); Ok(()) @@ -7843,11 +7905,54 @@ fn registration_matches( resource_uid: Option<&d2b_contracts_resource::v3::ResourceUid>, pid: Option, start_time_ticks: Option, + guest_execution: Option< + &d2b_contracts_broker::broker_wire::GuestExecutionBinding, + >, ) -> bool { registration.resource_ref.as_ref() == resource_ref && registration.resource_uid.as_ref() == resource_uid && pid.is_none_or(|pid| registration.pid == pid) && start_time_ticks.is_none_or(|ticks| registration.start_time_ticks == ticks) + && registration.guest_execution.as_ref() == guest_execution +} + +#[cfg(not(feature = "layer1-bootstrap"))] +fn rebind_guest_execution_registration( + registration: &mut RunnerRegistration, + requested: Option<&d2b_contracts_broker::broker_wire::GuestExecutionBinding>, +) -> Result { + if requested.is_some_and(|binding| !binding.is_valid()) { + return Err(BrokerError::LiveHandler( + "guest runner execution binding changed".to_owned(), + )); + } + if registration.guest_execution.as_ref() == requested { + return Ok(false); + } + + let (Some(current), Some(next)) = (registration.guest_execution.as_ref(), requested) else { + return Err(BrokerError::LiveHandler( + "guest runner execution binding changed".to_owned(), + )); + }; + if current.target_uid != next.target_uid + || current.boot_identity_digest != next.boot_identity_digest + || current.assignment_epoch != next.assignment_epoch + || current.provider_generation != next.provider_generation + || current.controller_generation != next.controller_generation + { + return Err(BrokerError::LiveHandler( + "guest runner execution binding changed".to_owned(), + )); + } + if next.session_generation <= current.session_generation { + return Err(BrokerError::LiveHandler( + "guest runner session generation is not newer".to_owned(), + )); + } + + registration.guest_execution = Some(next.clone()); + Ok(true) } #[cfg(not(feature = "layer1-bootstrap"))] @@ -7868,79 +7973,110 @@ fn observe_registered_runner( request.resource_ref.as_ref(), request.resource_uid.as_ref(), ); - let registration = { - let registry = runner_metadata_registry().lock().map_err(|_| { + let registered = (|| -> Result< + Option, + BrokerError, + > { + // Keep the lock order aligned with deregistration: pidfd registry + // first, metadata second. This makes the binding update atomic with + // the live pidfd registration. + let pidfd_registry = runner_pidfd_registry().lock().map_err(|_| { + BrokerError::Protocol("runner pidfd registry mutex poisoned".to_owned()) + })?; + let mut metadata_registry = runner_metadata_registry().lock().map_err(|_| { BrokerError::Protocol("runner metadata registry mutex poisoned".to_owned()) })?; - registry.get(&runner_id).cloned() - }; - let Some(registration) = registration else { - return discover_runner_candidate(request, intent); - }; - - let pidfd_registered = runner_pidfd_registry() - .lock() - .map_err(|_| BrokerError::Protocol("runner pidfd registry mutex poisoned".to_owned()))? - .contains_key(&runner_id); - if !pidfd_registered - || registration.vm_id != request.vm_id.as_str() - || registration.role_id != request.role_id.as_str() - || registration.role != request.role - || !registration_matches( - ®istration, - request.resource_ref.as_ref(), - request.resource_uid.as_ref(), - None, - None, - ) - || registration.bundle_runner_intent_ref != request.bundle_runner_intent_ref.as_str() - || registration.pid <= 0 - || registration.start_time_ticks == 0 - || registration.binary_path != intent.binary_path - || registration.cgroup_subtree != intent.cgroup_placement.subtree - { - return discover_runner_candidate(request, intent); - } - - let Some(start_time_ticks) = read_proc_start_time_ticks(registration.pid)? else { - remove_runner_metadata(&runner_id); - return Ok(d2b_contracts_broker::broker_wire::ObserveRunnerResponse { - vm_id: request.vm_id.clone(), - role_id: request.role_id.clone(), - present: false, - pid: 0, - start_time_ticks: 0, - cgroup_verified: false, - executable_verified: false, - }); - }; - if start_time_ticks != registration.start_time_ticks { - return Err(BrokerError::LiveHandler( - "runner process identity changed".to_owned(), - )); - } - let cgroup_verified = proc_cgroup_matches(registration.pid, ®istration.cgroup_subtree); - let executable_path = read_runner_executable(registration.pid); - let executable_verified = match executable_path { - Ok(ref path) => executable_paths_match(path, ®istration.binary_path), - Err(ref error) if error.kind() == io::ErrorKind::PermissionDenied => { - // A registered pidfd and the broker-owned cgroup are the - // executable provenance established by this broker. Linux - // resets dumpability when a runner changes uid, so Yama blocks - // the redundant /proc//exe read without CAP_SYS_PTRACE. - pidfd_registered && cgroup_verified - } - Err(_) => false, - }; - Ok(d2b_contracts_broker::broker_wire::ObserveRunnerResponse { - vm_id: request.vm_id.clone(), - role_id: request.role_id.clone(), - present: true, - pid: registration.pid, - start_time_ticks, - cgroup_verified, - executable_verified, - }) + let registration = match metadata_registry.get(&runner_id).cloned() { + Some(registration) => registration, + None => return Ok(None), + }; + let mut observed_registration = registration.clone(); + let rebound = rebind_guest_execution_registration( + &mut observed_registration, + request.guest_execution.as_ref(), + )?; + let pidfd_registered = pidfd_registry.contains_key(&runner_id); + if !pidfd_registered + || observed_registration.vm_id != request.vm_id.as_str() + || observed_registration.role_id != request.role_id.as_str() + || observed_registration.role != request.role + || !registration_matches( + &observed_registration, + request.resource_ref.as_ref(), + request.resource_uid.as_ref(), + None, + None, + request.guest_execution.as_ref(), + ) + || observed_registration.bundle_runner_intent_ref + != request.bundle_runner_intent_ref.as_str() + || observed_registration.pid <= 0 + || observed_registration.start_time_ticks == 0 + || observed_registration.binary_path != intent.binary_path + || observed_registration.cgroup_subtree != intent.cgroup_placement.subtree + { + Ok(None) + } else { + let Some(start_time_ticks) = read_proc_start_time_ticks(observed_registration.pid)? + else { + metadata_registry.remove(&runner_id); + return Ok(Some( + d2b_contracts_broker::broker_wire::ObserveRunnerResponse { + vm_id: request.vm_id.clone(), + role_id: request.role_id.clone(), + present: false, + pid: 0, + start_time_ticks: 0, + cgroup_verified: false, + executable_verified: false, + }, + )); + }; + if start_time_ticks != observed_registration.start_time_ticks { + return Err(BrokerError::LiveHandler( + "runner process identity changed".to_owned(), + )); + } + let cgroup_verified = proc_cgroup_matches( + observed_registration.pid, + &observed_registration.cgroup_subtree, + ); + let executable_path = read_runner_executable(observed_registration.pid); + let executable_verified = match executable_path { + Ok(ref path) => executable_paths_match(path, &observed_registration.binary_path), + Err(ref error) if error.kind() == io::ErrorKind::PermissionDenied => { + // A registered pidfd and the broker-owned cgroup are the + // executable provenance established by this broker. Linux + // resets dumpability when a runner changes uid, so Yama + // blocks the redundant /proc//exe read without + // CAP_SYS_PTRACE. + pidfd_registered && cgroup_verified + } + Err(_) => false, + }; + if rebound { + if let Some(stored) = metadata_registry.get_mut(&runner_id) { + stored.guest_execution = observed_registration.guest_execution.clone(); + } else { + return Err(BrokerError::LiveHandler( + "runner registration disappeared".to_owned(), + )); + } + } + Ok(Some( + d2b_contracts_broker::broker_wire::ObserveRunnerResponse { + vm_id: request.vm_id.clone(), + role_id: request.role_id.clone(), + present: true, + pid: observed_registration.pid, + start_time_ticks, + cgroup_verified, + executable_verified, + }, + )) + } + })()?; + registered.map_or_else(|| discover_runner_candidate(request, intent), Ok) } #[cfg(not(feature = "layer1-bootstrap"))] @@ -8812,8 +8948,12 @@ impl DispatchBackend for LiveDispatchBackend { self.daemon_uid, self.daemon_gid, )?; - let mut outcome = crate::live_handlers::live_spawn_runner(plan_input, preopened.child_fds) - .map_err(|err| { + let mut outcome = crate::live_handlers::live_spawn_runner( + plan_input, + preopened.child_fds, + req.activation_input.as_ref(), + ) + .map_err(|err| { // Log the actual LiveHandlerError detail before wrapping it // in the opaque BrokerError::LiveHandler envelope so // operators can see WHY the spawn failed in journalctl. @@ -10627,6 +10767,7 @@ fn runner_role_for_process_role( ProcessRole::Audio => Some(RunnerRole::Audio), ProcessRole::CloudHypervisorRunner => Some(RunnerRole::CloudHypervisor), ProcessRole::QemuMediaRunner => Some(RunnerRole::QemuMedia), + ProcessRole::ActivationNixosRunner => Some(RunnerRole::ActivationNixos), ProcessRole::VsockRelay => Some(RunnerRole::VsockRelay), ProcessRole::OtelHostBridge => Some(RunnerRole::OtelHostBridge), ProcessRole::Usbip => Some(RunnerRole::Usbip), @@ -10640,6 +10781,54 @@ fn runner_role_for_process_role( } #[cfg(not(feature = "layer1-bootstrap"))] +fn validate_guest_process_binding( + request: &d2b_contracts_broker::broker_wire::BrokerRequest, +) -> Result<(), BrokerError> { + use d2b_contracts_broker::broker_wire::BrokerRequest; + + let binding = match request { + BrokerRequest::SpawnRunner(request) => request.guest_execution.as_ref(), + BrokerRequest::OpenPidfd(request) => request.guest_execution.as_ref(), + BrokerRequest::ObserveRunner(request) => request.guest_execution.as_ref(), + BrokerRequest::StartSystemdUnit(request) + | BrokerRequest::ObserveSystemdUnit(request) + | BrokerRequest::CheckSystemdUserManager(request) => request.guest_execution.as_ref(), + BrokerRequest::OpenSystemdUnitPidfd(request) => request.unit.guest_execution.as_ref(), + BrokerRequest::StopSystemdUnit(request) => request.unit.guest_execution.as_ref(), + BrokerRequest::SignalRunner(request) => request.guest_execution.as_ref(), + BrokerRequest::DeregisterRunnerPidfd(request) => request.guest_execution.as_ref(), + _ => None, + }; + let Some(binding) = binding else { + return Ok(()); + }; + if !binding.is_valid() { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "guest_execution", + requested: "invalid".to_owned(), + resolved: "nonzero-target-session-boot-assignment-generations".to_owned(), + }); + } + let boot_id = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") + .map_err(|_| BrokerError::SpawnRunnerIntentMismatch { + field: "guest_execution.boot_identity_digest", + requested: "present".to_owned(), + resolved: "kernel-boot-identity-unavailable".to_owned(), + })?; + let mut digest = Sha256::new(); + digest.update(b"d2b-kernel-boot-id-v1\0"); + digest.update(boot_id.trim().as_bytes()); + let expected: [u8; 32] = digest.finalize().into(); + if binding.boot_identity_digest != expected { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "guest_execution.boot_identity_digest", + requested: "mismatch".to_owned(), + resolved: "kernel-boot-identity".to_owned(), + }); + } + Ok(()) +} + #[cfg(not(feature = "layer1-bootstrap"))] fn validate_sandbox_launch_plan( req: &d2b_contracts_broker::broker_wire::SpawnRunnerRequest, @@ -10892,6 +11081,66 @@ fn validate_spawn_runner_request_matches_intent( resolved: expected_role.as_str().to_owned(), }); } + if req.resource_ref.is_some() { + let provider_identity_matches = ["system-minijail", "system-systemd"] + .into_iter() + .map(|provider| { + let mut digest = Sha256::new(); + digest.update(b"d2b-process-provider-v1"); + digest.update(provider.as_bytes()); + let digest: [u8; 32] = digest.finalize().into(); + digest + }) + .any(|digest| req.provider_identity == Some(digest)); + if !provider_identity_matches { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "provider_identity", + requested: "mismatch".to_owned(), + resolved: "signed-process-provider".to_owned(), + }); + } + let template_matches = [ + Some(intent.role_id.as_str()), + Some(intent.profile_id.as_str()), + match intent.role { + d2b_core::processes::ProcessRole::SwtpmPreStartFlush => Some("swtpm-flush"), + d2b_core::processes::ProcessRole::Swtpm => Some("swtpm"), + d2b_core::processes::ProcessRole::Virtiofsd => Some("virtiofsd"), + d2b_core::processes::ProcessRole::Video => Some("video"), + d2b_core::processes::ProcessRole::Gpu => Some("gpu"), + d2b_core::processes::ProcessRole::GpuRenderNode => Some("gpu-render-node"), + d2b_core::processes::ProcessRole::Audio => Some("audio"), + d2b_core::processes::ProcessRole::CloudHypervisorRunner => { + Some("cloud-hypervisor") + } + d2b_core::processes::ProcessRole::QemuMediaRunner => Some("qemu-media"), + d2b_core::processes::ProcessRole::ActivationNixosRunner => { + Some("activation-nixos-runner") + } + d2b_core::processes::ProcessRole::VsockRelay => Some("vsock-relay"), + d2b_core::processes::ProcessRole::OtelHostBridge => Some("otel-host-bridge"), + d2b_core::processes::ProcessRole::Usbip => Some("usbip"), + d2b_core::processes::ProcessRole::WaylandProxy => Some("wayland-proxy"), + _ => None, + }, + ] + .into_iter() + .flatten() + .any(|template| { + let mut digest = Sha256::new(); + digest.update(b"d2b-process-template-v1"); + digest.update(template.as_bytes()); + let digest: [u8; 32] = digest.finalize().into(); + req.template_identity == Some(digest) + }); + if !template_matches { + return Err(BrokerError::SpawnRunnerIntentMismatch { + field: "template_identity", + requested: "mismatch".to_owned(), + resolved: "signed-process-template".to_owned(), + }); + } + } Ok(()) } @@ -13287,7 +13536,8 @@ mod tests { use d2b_contracts::types::BundleOpId; #[cfg(not(feature = "layer1-bootstrap"))] use d2b_contracts_broker::broker_wire::{ - ActivationMode, ActivationPhase, RunActivationRequest, RunActivationResponse, + ActivationMode, ActivationPhase, GuestExecutionBinding, RunActivationRequest, + RunActivationResponse, }; #[cfg(not(feature = "layer1-bootstrap"))] use d2b_core::bundle_resolver::{ResolvedActivationIntent, ResolvedStoreViewIntent}; @@ -13410,6 +13660,107 @@ mod tests { } } + #[cfg(not(feature = "layer1-bootstrap"))] + fn guest_execution_binding(session_generation: u64) -> GuestExecutionBinding { + GuestExecutionBinding { + target_uid: d2b_contracts_resource::v3::ResourceUid::parse( + "123e4567-e89b-42d3-a456-426614174000", + ) + .expect("Guest UID"), + boot_identity_digest: [7; 32], + session_generation, + assignment_epoch: 3, + provider_generation: 4, + controller_generation: 5, + } + } + + #[cfg(not(feature = "layer1-bootstrap"))] + fn guest_runner_registration(binding: GuestExecutionBinding) -> RunnerRegistration { + RunnerRegistration { + vm_id: "guest-vm".to_owned(), + role_id: "guest-process".to_owned(), + resource_ref: None, + resource_uid: None, + role: d2b_contracts_broker::broker_wire::RunnerRole::CloudHypervisor, + bundle_runner_intent_ref: "runner:guest-vm:guest-process".to_owned(), + pid: 1, + start_time_ticks: 1, + binary_path: PathBuf::from("/bin/true"), + cgroup_subtree: "d2b.slice/guest-vm/guest-process".to_owned(), + guest_execution: Some(binding), + } + } + + #[cfg(not(feature = "layer1-bootstrap"))] + #[test] + fn guest_runner_registration_rebinds_to_a_newer_session() { + let mut registration = guest_runner_registration(guest_execution_binding(1)); + let next = guest_execution_binding(2); + + assert!(rebind_guest_execution_registration(&mut registration, Some(&next)) + .expect("newer session generation rebinds")); + assert_eq!(registration.guest_execution, Some(next.clone())); + assert!( + !rebind_guest_execution_registration(&mut registration, Some(&next)) + .expect("same binding remains an exact match") + ); + } + + #[cfg(not(feature = "layer1-bootstrap"))] + #[test] + fn guest_runner_registration_rejects_stale_or_changed_bindings() { + for generation in [1, 0] { + let mut registration = guest_runner_registration(guest_execution_binding(2)); + let stale = guest_execution_binding(generation); + assert!( + rebind_guest_execution_registration(&mut registration, Some(&stale)).is_err(), + "session generation {generation} must not replace generation 2" + ); + } + + let mut registration = guest_runner_registration(guest_execution_binding(2)); + for changed in [ + { + let mut binding = guest_execution_binding(3); + binding.target_uid = d2b_contracts_resource::v3::ResourceUid::parse( + "223e4567-e89b-42d3-a456-426614174000", + ) + .expect("Guest UID"); + binding + }, + { + let mut binding = guest_execution_binding(3); + binding.boot_identity_digest = [8; 32]; + binding + }, + { + let mut binding = guest_execution_binding(3); + binding.assignment_epoch = 4; + binding + }, + { + let mut binding = guest_execution_binding(3); + binding.provider_generation = 5; + binding + }, + { + let mut binding = guest_execution_binding(3); + binding.controller_generation = 6; + binding + }, + ] { + assert!( + rebind_guest_execution_registration(&mut registration, Some(&changed)).is_err(), + "non-session binding changes must not be accepted as a reconnect" + ); + assert_eq!( + registration.guest_execution, + Some(guest_execution_binding(2)) + ); + } + } + #[cfg(not(feature = "layer1-bootstrap"))] #[test] fn qemu_media_enroll_request_fields_redact_raw_busid() { @@ -15668,6 +16019,7 @@ mod tests { expected_start_time_ticks: 123456, resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: Some(TracingSpanId::new("span-pidfd")), }), "OpenPidfd", @@ -15697,6 +16049,7 @@ mod tests { expected_start_time_ticks: None, resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: Some(TracingSpanId::new("span-signal")), }), "SignalRunner", @@ -15721,12 +16074,14 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, resource_ref: None, resource_uid: None, bundle_content_identity: None, provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, workload_identity: None, vm_id: VmId::new("corp-vm"), @@ -16172,12 +16527,14 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, resource_ref: None, resource_uid: None, bundle_content_identity: None, provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, workload_identity: None, vm_id: VmId::new("corp-vm"), @@ -16878,12 +17235,14 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, resource_ref: None, resource_uid: None, bundle_content_identity: None, provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, workload_identity: None, vm_id: VmId::new("corp-vm"), @@ -17017,12 +17376,14 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, resource_ref: None, resource_uid: None, bundle_content_identity: None, provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, workload_identity: None, vm_id: VmId::new("corp-vm"), @@ -17103,6 +17464,7 @@ mod tests { expected_start_time_ticks: None, resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: None, }); let audit_context = DispatchAuditContext::from_request(&request, 5151, &caller_role) diff --git a/packages/d2b-broker/src/sys.rs b/packages/d2b-broker/src/sys.rs index 2350f1e583..ea8afb8417 100644 --- a/packages/d2b-broker/src/sys.rs +++ b/packages/d2b-broker/src/sys.rs @@ -2179,6 +2179,10 @@ pub mod pidfd_sys { /// dropping credentials. Used only for qemu-media runners whose /// trusted argv requests QEMU `mem-lock=on`. pub memlock_limit_bytes: Option, + /// Optional broker-controlled stdin envelope for an activation + /// runner. The broker creates the pipe and writes these validated + /// bytes only after the child has been spawned. + pub activation_stdin: Option>, } /// Well-known fd number for the pre-opened render node. @@ -2953,6 +2957,8 @@ pub mod pidfd_sys { const CHILD_EXIT_PREOPEN_DUP2: libc::c_int = 76; /// setrlimit(RLIMIT_MEMLOCK) failed for a runner that requested it. const CHILD_EXIT_MEMLOCK_RLIMIT: libc::c_int = 77; + /// dup2 of the broker-controlled activation stdin pipe failed. + const CHILD_EXIT_ACTIVATION_STDIN: libc::c_int = 78; /// Spawn a per-role runner with namespace / seccomp / capability /// setup plus `setgroups` + `setgid` + `setuid` + `execve` in a @@ -2972,6 +2978,14 @@ pub mod pidfd_sys { supplementary_groups: Vec, isolation: RunnerIsolationSpec, ) -> io::Result { + if isolation.activation_stdin.as_ref().is_some_and(|payload| { + payload.len() > d2b_contracts_resource::v3::MAX_ACTIVATION_RUNNER_INPUT_BYTES + }) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "SpawnRunner: activation stdin exceeds bounded envelope", + )); + } // NamespaceSet.user is ALLOWED when // RunnerIsolationSpec.user_namespace provides the uid_map/gid_map // values. Caller must set both for the child to be fake-root @@ -3092,6 +3106,26 @@ pub mod pidfd_sys { .map(|p| p.write_fd.as_raw_fd()) .unwrap_or(-1); + // Activation runners receive a broker-created stdin pipe. The child + // owns only the read end; the parent writes the already validated + // typed envelope after clone and closes its end to delimit input. + let (activation_stdin_read_owner, activation_stdin_write_fd) = + match isolation.activation_stdin.as_ref() { + Some(_) => { + let pipe = make_sync_pipe()?; + (Some(pipe.read_fd), Some(pipe.write_fd)) + } + None => (None, None), + }; + let activation_stdin_read_fd = activation_stdin_read_owner + .as_ref() + .map(|fd| fd.as_raw_fd()) + .unwrap_or(-1); + let activation_stdin_write_raw_fd = activation_stdin_write_fd + .as_ref() + .map(|fd| fd.as_raw_fd()) + .unwrap_or(-1); + // When a user namespace is in play, the child must transition // to in-NS UID 0 (the mapped root), NOT to the host stable // principal UID. Calling setuid() inside the new user @@ -3124,6 +3158,25 @@ pub mod pidfd_sys { extra_clone_flags, cgroup_dir_raw_fd, move |_| unsafe { + // Keep the parent's read-end owner live through clone/fork; + // the child inherits the raw descriptor captured below. + let _activation_stdin_read_owner = &activation_stdin_read_owner; + if activation_stdin_read_fd >= 0 { + if activation_stdin_write_raw_fd >= 0 { + libc::close(activation_stdin_write_raw_fd); + } + if activation_stdin_read_fd != libc::STDIN_FILENO { + if libc::dup2(activation_stdin_read_fd, libc::STDIN_FILENO) < 0 { + let m = b"DEBUG: dup2 activation stdin failed\n"; + libc::write(2, m.as_ptr() as *const _, m.len()); + libc::_exit(CHILD_EXIT_ACTIVATION_STDIN); + } + libc::close(activation_stdin_read_fd); + } + // dup2 does not copy CLOEXEC; clear it for the + // read-end-already-at-stdin case. + libc::fcntl(libc::STDIN_FILENO, libc::F_SETFD, 0); + } // If we're in a user NS, FIRST close the inherited write end // of the sync pipe so the parent's death is observable as EOF. // THEN block on the read end until the parent has written @@ -3443,6 +3496,37 @@ pub mod pidfd_sys { } } + if let Some(payload) = isolation.activation_stdin.as_deref() { + let write_fd = activation_stdin_write_fd.ok_or_else(|| { + io::Error::new( + io::ErrorKind::BrokenPipe, + "activation stdin pipe was not created", + ) + })?; + let mut offset = 0; + while offset < payload.len() { + match rustix::io::write(&write_fd, &payload[offset..]) { + Ok(0) => { + let _ = pidfd_send_signal(outcome.pidfd.as_fd(), libc::SIGKILL); + let _ = reap_spawn_runner_error_child(outcome.pid); + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "activation stdin pipe closed before payload delivery", + )); + } + Ok(written) => offset += written, + Err(error) if error.raw_os_error() == libc::EINTR => {} + Err(error) => { + let _ = pidfd_send_signal(outcome.pidfd.as_fd(), libc::SIGKILL); + let _ = reap_spawn_runner_error_child(outcome.pid); + return Err(io::Error::from_raw_os_error(error.raw_os_error())); + } + } + } + // Dropping the write end sends EOF to the helper. + drop(write_fd); + } + Ok(outcome) } @@ -3514,6 +3598,7 @@ pub mod pidfd_sys { #[cfg(test)] mod tests { + use std::ffi::CString; use std::fs; use std::os::fd::{AsRawFd, OwnedFd}; use std::os::unix::fs::{PermissionsExt, symlink}; @@ -3522,6 +3607,8 @@ mod tests { use nix::fcntl::{FcntlArg, FdFlag, fcntl}; use nix::libc; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; + use nix::sys::wait::{WaitStatus, waitpid}; + use nix::unistd::Pid; use tempfile::tempdir; fn test_namespaces(pid: bool) -> NamespaceSet { @@ -3691,7 +3778,6 @@ mod tests { // requires root and is tested by integration tests in live_handlers.rs). use super::pidfd_sys::{RunnerIsolationSpec, UserNamespaceSpec, clone3_spawn_runner}; - use std::ffi::CString; fn empty_mount_policy() -> MountPolicy { MountPolicy { @@ -3723,6 +3809,7 @@ mod tests { umask: None, pre_opened_device_fds: Vec::new(), memlock_limit_bytes: None, + activation_stdin: None, } } @@ -3773,6 +3860,67 @@ mod tests { assert_eq!(iso.umask, Some(7)); } + #[test] + fn activation_stdin_delivery_is_broker_controlled() { + let shell_path = ["/bin/sh", "/usr/bin/sh"] + .iter() + .find(|path| std::path::Path::new(path).exists()) + .copied() + .expect("could not find a shell binary"); + let current_uid = nix::unistd::Uid::current().as_raw(); + let current_gid = nix::unistd::Gid::current().as_raw(); + if current_uid != 0 { + eprintln!("SKIP: activation stdin child test requires broker credentials"); + return; + } + let mut isolation = isolation_with_user_namespace(None); + isolation.activation_stdin = Some(b"expected\n".to_vec()); + let binary = CString::new(shell_path).expect("shell path"); + let supplementary_groups = nix::unistd::getgroups() + .expect("read supplementary groups") + .into_iter() + .map(|group| group.as_raw()) + .collect(); + let outcome = clone3_spawn_runner( + binary.clone(), + vec![ + binary, + CString::new("-c").expect("shell option"), + CString::new(r#"read -r value && [ "$value" = expected ]"#) + .expect("shell script"), + ], + Vec::new(), + current_uid, + current_gid, + supplementary_groups, + isolation, + ) + .expect("activation stdin runner should spawn"); + let status = waitpid(Pid::from_raw(outcome.pid), None).expect("wait for runner"); + assert!(matches!(status, WaitStatus::Exited(_, 0)), "status: {status:?}"); + } + + #[test] + fn activation_stdin_bound_is_enforced_before_spawn() { + let mut isolation = isolation_with_user_namespace(None); + isolation.activation_stdin = Some(vec![ + 0; + d2b_contracts_resource::v3::MAX_ACTIVATION_RUNNER_INPUT_BYTES + 1 + ]); + let binary = CString::new("/bin/true").expect("true path"); + let error = clone3_spawn_runner( + binary, + Vec::new(), + Vec::new(), + nix::unistd::Uid::current().as_raw(), + nix::unistd::Gid::current().as_raw(), + Vec::new(), + isolation, + ) + .expect_err("oversized activation stdin must be refused"); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); + } + #[test] fn child_spawn_path_does_not_log_full_argv_or_env_to_global_file() { let source = include_str!("sys.rs"); @@ -3911,8 +4059,8 @@ mod tests { ); assert_eq!( source.matches(reap_error_child).count(), - 4, - "fallback cgroup, user-NS map, sync-write error, and sync-write short-write paths must each reap before returning Err" + 6, + "fallback cgroup, user-NS map, sync-write error, sync-write short-write, activation stdin write error, and activation stdin short-write paths must each reap before returning Err" ); assert!( !source.contains(removed_child_helper), @@ -4075,6 +4223,7 @@ mod tests { umask: None, pre_opened_device_fds: Vec::new(), memlock_limit_bytes: None, + activation_stdin: None, }; let bin = CString::new(true_path).unwrap(); diff --git a/packages/d2b-broker/tests/guest_profile.rs b/packages/d2b-broker/tests/guest_profile.rs index e5b7abdefd..5e30d0d48b 100644 --- a/packages/d2b-broker/tests/guest_profile.rs +++ b/packages/d2b-broker/tests/guest_profile.rs @@ -114,6 +114,7 @@ fn guest_binary_rejects_spawn_runner_before_bundle_mutation() { provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, role: RunnerRole::CloudHypervisor, bundle_runner_intent_ref: BundleOpId::new("runner:test"), @@ -122,6 +123,7 @@ fn guest_binary_rejects_spawn_runner_before_bundle_mutation() { ), execution_domain: None, user_ref: None, + guest_execution: None, runtime_allocations: Vec::new(), tracing_span_id: None, workload_identity: None, diff --git a/packages/d2b-bus/src/router.rs b/packages/d2b-bus/src/router.rs index 7572737fb9..5b4ccb2ff4 100644 --- a/packages/d2b-bus/src/router.rs +++ b/packages/d2b-bus/src/router.rs @@ -1521,6 +1521,7 @@ impl UnixSubjectRecord { ResourceRef::parse("Provider/clipboard-wayland").ok() } "d2b.notification.v3" => ResourceRef::parse("Provider/notification-desktop").ok(), + "d2b.config-nixos.v3" => ResourceRef::parse("Provider/config-nixos").ok(), _ => self.provider_ref, } } else { @@ -2877,6 +2878,7 @@ fn routes_for_admitted_session( | "d2b.clipboard.bridge.v3" | "d2b.clipboard.picker-coord.v3" | "d2b.notification.v3" + | "d2b.config-nixos.v3" ); if binding.provider_ref().is_none() || (binding.subject_ref().resource_type().as_str() != "Provider" && !guest_provider_service) diff --git a/packages/d2b-contracts-broker/src/broker_wire.rs b/packages/d2b-contracts-broker/src/broker_wire.rs index eb705cab71..9f5c89d5f9 100644 --- a/packages/d2b-contracts-broker/src/broker_wire.rs +++ b/packages/d2b-contracts-broker/src/broker_wire.rs @@ -23,7 +23,8 @@ use d2b_contracts_resource::v3::process::{ CapabilityClass, EnvironmentClass, NamespaceClass, UserNamespaceSpec, }; use d2b_contracts_resource::v3::{ - ArtifactId, IfName, ResourceBundleGenerationId, ResourceGeneration, ResourceRef, ResourceUid, + ActivationRunnerInput, ArtifactId, IfName, ResourceBundleGenerationId, ResourceGeneration, + ResourceRef, ResourceUid, execution_policy::ExecutionDomain, storage::ZoneStoreId, }; use schemars::JsonSchema; @@ -1330,7 +1331,7 @@ impl BrokerProfile { return false; } match self { - Self::Host => true, + Self::Host => !Self::request_targets_guest(request), Self::Guest => match request { BrokerRequest::SpawnRunner(request) => { request @@ -1338,11 +1339,96 @@ impl BrokerProfile { .as_ref() .is_some_and(|target| target.resource_type().as_str() == "Guest") && GUEST_LOCAL_RUNNER_ROLES.contains(&request.role) + && request.guest_execution.as_ref().is_some_and(GuestExecutionBinding::is_valid) } + BrokerRequest::StartSystemdUnit(request) + | BrokerRequest::ObserveSystemdUnit(request) + | BrokerRequest::CheckSystemdUserManager(request) => request + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + && request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::OpenPidfd(request) => request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::ObserveRunner(request) => request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::OpenSystemdUnitPidfd(request) => request + .unit + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + && request + .unit + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::StopSystemdUnit(request) => request + .unit + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + && request + .unit + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::SignalRunner(request) => request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), + BrokerRequest::DeregisterRunnerPidfd(request) => request + .guest_execution + .as_ref() + .is_some_and(GuestExecutionBinding::is_valid), _ => true, }, } } + + fn request_targets_guest(request: &BrokerRequest) -> bool { + match request { + BrokerRequest::SpawnRunner(request) => request + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + || request.guest_execution.is_some(), + BrokerRequest::StartSystemdUnit(request) + | BrokerRequest::ObserveSystemdUnit(request) + | BrokerRequest::CheckSystemdUserManager(request) => request + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + || request.guest_execution.is_some(), + BrokerRequest::OpenSystemdUnitPidfd(request) => { + request.unit.guest_execution.is_some() + || request + .unit + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + } + BrokerRequest::StopSystemdUnit(request) => { + request.unit.guest_execution.is_some() + || request + .unit + .execution_ref + .as_ref() + .is_some_and(|target| target.resource_type().as_str() == "Guest") + } + BrokerRequest::OpenPidfd(request) => request.guest_execution.is_some(), + BrokerRequest::ObserveRunner(request) => request.guest_execution.is_some(), + BrokerRequest::SignalRunner(request) => request.guest_execution.is_some(), + BrokerRequest::DeregisterRunnerPidfd(request) => request.guest_execution.is_some(), + _ => false, + } + } } /// Every request currently defined by the broker wire. Host mode is closed @@ -2359,6 +2445,35 @@ pub struct QemuMediaHotplugResponse { pub events: Vec, } +/// Exact authenticated binding for a Guest-local Process lifecycle. +/// +/// The values are commitments, not raw boot identifiers or transport +/// handles. A Guest broker requires this tuple for every target-local +/// Process operation and revalidates the boot commitment against its own +/// kernel before executing the request. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct GuestExecutionBinding { + pub target_uid: ResourceUid, + pub boot_identity_digest: [u8; 32], + pub session_generation: u64, + pub assignment_epoch: u64, + pub provider_generation: u64, + pub controller_generation: u64, +} + +impl GuestExecutionBinding { + /// Return whether all Guest execution commitments are populated. + pub fn is_valid(&self) -> bool { + self.target_uid.as_str().len() > 0 + && self.boot_identity_digest != [0; 32] + && self.session_generation > 0 + && self.assignment_epoch > 0 + && self.provider_generation > 0 + && self.controller_generation > 0 + } +} + /// OpenPidfd daemon-side reconcile-and-adopt support. The daemon's /// `d2bd::supervisor::state::reconcile_and_adopt` loop sends this /// request for every snapshot the classifier returned `Adopt` for. The @@ -2397,6 +2512,9 @@ pub struct OpenPidfdRequest { pub resource_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_uid: Option, + /// Exact Guest target/session binding for target-local Process adoption. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, #[serde(default)] pub tracing_span_id: Option, } @@ -2446,6 +2564,9 @@ pub struct ObserveRunnerRequest { pub resource_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_uid: Option, + /// Exact Guest target/session binding for target-local Process adoption. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, #[serde(default)] pub tracing_span_id: Option, } @@ -2548,6 +2669,9 @@ pub struct SystemdUnitIdentity { pub generation: u64, /// Content identity of the broker-resolved trusted bundle. pub bundle_content_identity: String, + /// Exact Guest target/session binding, when this is Guest-local. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, } /// Shared trusted request fields for systemd unit operations. @@ -2585,6 +2709,9 @@ pub struct SystemdUnitRequest { /// Canonical User resource bound to a user-domain launch. #[serde(default)] pub user_ref: Option, + /// Exact Guest target/session binding for target-local Process operations. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, /// Typed sandbox requirements enforced by the broker's systemd launch /// adapter. Legacy VM runner callers omit this field. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -3169,6 +3296,9 @@ pub struct SignalRunnerRequest { pub resource_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_uid: Option, + /// Exact Guest target/session binding for target-local Process control. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, #[serde(default)] pub tracing_span_id: Option, } @@ -3205,6 +3335,9 @@ pub struct DeregisterRunnerPidfdRequest { pub resource_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_uid: Option, + /// Exact Guest target/session binding for target-local Process cleanup. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, #[serde(default)] pub tracing_span_id: Option, } @@ -3237,6 +3370,9 @@ pub enum RunnerRole { /// QEMU media runtime scaffold. The runtime Provider owns argv planning; /// the broker consumes the bundle-authoritative launch shape. QemuMedia, + /// Target-local one-shot NixOS activation runner. Guest mode may spawn + /// this role only from the bundle-authoritative process intent. + ActivationNixos, /// virtiofsd sidecar; one per `microvm.shares` row. The daemon/bundle /// provides argv from `nixos-modules/processes-json.nix`. Virtiofsd, @@ -3295,6 +3431,7 @@ impl RunnerRole { match self { Self::CloudHypervisor => "cloud-hypervisor", Self::QemuMedia => "qemu-media", + Self::ActivationNixos => "activation-nixos-runner", Self::Virtiofsd => "virtiofsd", Self::Swtpm => "swtpm", Self::SwtpmFlush => "swtpm-flush", @@ -3311,11 +3448,11 @@ impl RunnerRole { /// Closed set of runner roles that a Guest broker may spawn locally. /// -/// The current runner vocabulary contains only host-side VM, device, relay, -/// observability, and compositor helpers. Guest Process resources use the -/// dedicated local effect classes instead, so this set is intentionally empty -/// until a runner with an explicitly Guest-local contract is added. -pub const GUEST_LOCAL_RUNNER_ROLES: &[RunnerRole] = &[]; +/// The current runner vocabulary contains mostly host-side VM, device, relay, +/// observability, and compositor helpers. The activation runner is the one +/// explicitly Guest-local role; future Guest Process roles must be added here +/// together with their signed bundle contract and identity fencing. +pub const GUEST_LOCAL_RUNNER_ROLES: &[RunnerRole] = &[RunnerRole::ActivationNixos]; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -3343,6 +3480,9 @@ pub struct SpawnRunnerRequest { pub template_identity: Option<[u8; 32]>, #[serde(default, skip_serializing_if = "Option::is_none")] pub generation: Option, + /// Typed stdin input admitted only for the activation-nixos runner role. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub activation_input: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sandbox_plan: Option, /// Role selector - picks the argv generator the broker applies to @@ -3364,6 +3504,9 @@ pub struct SpawnRunnerRequest { /// Canonical User resource bound to a user-domain launch. #[serde(default)] pub user_ref: Option, + /// Exact Guest target/session binding for target-local Process launches. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, /// Optional vsock CID / TAP fd slot allocated by the daemon at /// host-prepare time. The broker validates each entry against the /// bundle row and refuses any unexpected allocation slot. None @@ -3434,6 +3577,8 @@ pub struct SpawnRunnerResponse { #[serde(default, skip_serializing_if = "Option::is_none")] pub user_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub guest_execution: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub provider_identity: Option<[u8; 32]>, #[serde(default, skip_serializing_if = "Option::is_none")] pub template_identity: Option<[u8; 32]>, @@ -4592,12 +4737,14 @@ mod tests { provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, role, bundle_runner_intent_ref: BundleOpId::new("runner:test"), execution_ref: Some(ResourceRef::parse(execution_ref).expect("valid execution ref")), execution_domain: None, user_ref: None, + guest_execution: None, runtime_allocations: Vec::new(), tracing_span_id: None, workload_identity: None, @@ -4641,9 +4788,9 @@ mod tests { role.as_str() ); assert!( - spawn_runner_for_profile(role, "Guest/guest-vm") + !spawn_runner_for_profile(role, "Guest/guest-vm") .allowed_by_profile(BrokerProfile::Host), - "Host profile behavior changed for Guest execution ref {}", + "Host profile must reject Guest execution ref {}", role.as_str() ); assert!( @@ -4654,6 +4801,39 @@ mod tests { } } + #[test] + fn guest_profile_requires_a_complete_target_execution_binding() { + let mut request = spawn_runner_for_profile( + RunnerRole::ActivationNixos, + "Guest/guest-vm", + ); + request = match request { + BrokerRequest::SpawnRunner(mut request) => { + request.guest_execution = Some(GuestExecutionBinding { + target_uid: ResourceUid::parse( + "123e4567-e89b-42d3-a456-426614174000", + ) + .expect("Guest UID"), + boot_identity_digest: [7; 32], + session_generation: 2, + assignment_epoch: 3, + provider_generation: 4, + controller_generation: 5, + }); + BrokerRequest::SpawnRunner(request) + } + _ => unreachable!("helper always builds SpawnRunner"), + }; + assert!(request.allowed_by_profile(BrokerProfile::Guest)); + assert!(!request.allowed_by_profile(BrokerProfile::Host)); + + let BrokerRequest::SpawnRunner(mut invalid) = request else { + unreachable!("helper always builds SpawnRunner"); + }; + invalid.guest_execution.as_mut().unwrap().assignment_epoch = 0; + assert!(!BrokerRequest::SpawnRunner(invalid).allowed_by_profile(BrokerProfile::Guest)); + } + #[test] fn spawn_runner_rejects_each_legacy_authority_field() { // argv, env, uid, gid, caps, seccomp_profile, @@ -4837,6 +5017,7 @@ mod tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, provider_identity: None, template_identity: None, generation: None, diff --git a/packages/d2b-contracts-control/src/public_wire.rs b/packages/d2b-contracts-control/src/public_wire.rs index 802f49a21c..3d20ecca4f 100644 --- a/packages/d2b-contracts-control/src/public_wire.rs +++ b/packages/d2b-contracts-control/src/public_wire.rs @@ -515,6 +515,9 @@ fn is_false(value: &bool) -> bool { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ActivationRequest { pub vm: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[schemars(range(min = 1))] + pub to_generation: Option, #[serde(default, flatten)] pub flags: MutationFlags, } diff --git a/packages/d2b-contracts-resource/src/v3/activation_nixos.rs b/packages/d2b-contracts-resource/src/v3/activation_nixos.rs index aacf865971..26ef5e1ad0 100644 --- a/packages/d2b-contracts-resource/src/v3/activation_nixos.rs +++ b/packages/d2b-contracts-resource/src/v3/activation_nixos.rs @@ -1,6 +1,10 @@ //! Contracts for the activation-nixos Provider. -use schemars::JsonSchema; +use schemars::{ + JsonSchema, + r#gen::SchemaGenerator, + schema::{Schema, SchemaObject}, +}; use serde::{Deserialize, Deserializer, Serialize}; use super::{ArtifactId, ResourceRef, ResourceTypeName, execution_policy::require_execution_ref}; @@ -9,6 +13,8 @@ use super::{ArtifactId, ResourceRef, ResourceTypeName, execution_policy::require pub const NIXOS_GENERATION_RESOURCE_TYPE: &str = "activation-nixos.d2bus.org.NixosGeneration"; /// The only Provider admitted by the activation generation schema. pub const ACTIVATION_PROVIDER_REF: &str = "Provider/activation-nixos"; +/// Maximum serialized stdin envelope delivered to the activation helper. +pub const MAX_ACTIVATION_RUNNER_INPUT_BYTES: usize = 2048; /// Target-local activation mode. #[derive( @@ -26,6 +32,65 @@ pub enum ActivationMode { Adopt, } +/// Closed input delivered to an activation-runner `EphemeralProcess`. +/// +/// The broker serializes this value into the bounded stdin envelope consumed +/// by `d2b-activation-helper`. It deliberately carries no executable, +/// store path, target path, or caller-controlled bytes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ActivationRunnerInput { + /// Private-catalog artifact identifier. + pub system_artifact_id: ArtifactId, + /// Target generation ordinal bound to the owning `NixosGeneration`. + #[schemars(schema_with = "nonzero_u64_schema")] + pub target_generation: u64, + /// Closed activation mode. + pub activation_mode: ActivationMode, +} + +fn nonzero_u64_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema: SchemaObject = ::json_schema(generator).into(); + schema + .number + .get_or_insert_with(Default::default) + .minimum = Some(1.0); + schema.into() +} + +/// Validation failure for a typed activation-runner input. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ActivationRunnerInputError { + /// Target generation ordinal was zero. + GenerationInvalid, +} + +impl core::fmt::Display for ActivationRunnerInputError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("activation-runner-generation-invalid") + } +} + +impl std::error::Error for ActivationRunnerInputError {} + +impl ActivationRunnerInput { + /// Construct and validate one runner input. + pub fn new( + system_artifact_id: ArtifactId, + target_generation: u64, + activation_mode: ActivationMode, + ) -> Result { + if target_generation == 0 { + return Err(ActivationRunnerInputError::GenerationInvalid); + } + Ok(Self { + system_artifact_id, + target_generation, + activation_mode, + }) + } +} + /// Provider-specific progress detail. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, @@ -92,6 +157,8 @@ pub enum NixosGenerationSpecError { PriorGenerationRefInvalid, /// Artifact identifier was invalid or empty. ArtifactIdInvalid, + /// Target generation ordinal was zero. + GenerationInvalid, } impl core::fmt::Display for NixosGenerationSpecError { @@ -101,6 +168,7 @@ impl core::fmt::Display for NixosGenerationSpecError { Self::ExecutionRefInvalid => "activation-execution-ref-invalid", Self::PriorGenerationRefInvalid => "activation-prior-generation-ref-invalid", Self::ArtifactIdInvalid => "activation-artifact-id-invalid", + Self::GenerationInvalid => "activation-generation-invalid", }) } } diff --git a/packages/d2b-contracts-resource/src/v3/process.rs b/packages/d2b-contracts-resource/src/v3/process.rs index cd031bbd54..5981c69b79 100644 --- a/packages/d2b-contracts-resource/src/v3/process.rs +++ b/packages/d2b-contracts-resource/src/v3/process.rs @@ -18,7 +18,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; use super::{ - ResourceRef, + ActivationRunnerInput, ResourceRef, execution_policy::{ BoundedToken, BudgetSpec, DurationMs, ExecutionDomain, PrimitiveSpecError, redacted_debug, require_execution_ref, require_resource_type, @@ -1383,6 +1383,8 @@ impl<'de> Deserialize<'de> for ProcessSpec { pub struct EphemeralProcessSpec { #[serde(flatten)] execution: ExecutionSpec, + #[serde(skip_serializing_if = "Option::is_none")] + activation_input: Option, start_deadline: DurationMs, runtime_deadline: DurationMs, successful_ttl: DurationMs, @@ -1409,6 +1411,7 @@ impl EphemeralProcessSpec { check_duration(&failed_ttl, 0, 30 * 86_400_000)?; Ok(Self { execution, + activation_input: None, start_deadline, runtime_deadline, successful_ttl, @@ -1434,6 +1437,28 @@ impl EphemeralProcessSpec { &self.execution } + /// Attach the only typed stdin input admitted to an activation runner. + pub fn with_activation_input( + mut self, + input: ActivationRunnerInput, + ) -> Result { + if self.execution.template().as_str() != "activation-nixos-runner" + || !matches!( + self.execution.execution_ref().resource_type().as_str(), + "Host" | "Guest" + ) + { + return Err(PrimitiveSpecError::ConflictingFields); + } + self.activation_input = Some(input); + Ok(self) + } + + /// Borrow the typed activation-runner stdin input, when present. + pub const fn activation_input(&self) -> Option<&ActivationRunnerInput> { + self.activation_input.as_ref() + } + /// Borrow the successful terminal retention. pub const fn successful_ttl(&self) -> &DurationMs { &self.successful_ttl @@ -1491,6 +1516,8 @@ impl<'de> Deserialize<'de> for EphemeralProcessSpec { #[serde(default)] telemetry: TelemetrySpec, #[serde(default)] + activation_input: Option, + #[serde(default)] start_deadline: Option, #[serde(default)] runtime_deadline: Option, @@ -1519,7 +1546,7 @@ impl<'de> Deserialize<'de> for EphemeralProcessSpec { } .into_execution() .map_err(serde::de::Error::custom)?; - Self::new( + let spec = Self::new( execution, wire.start_deadline .unwrap_or_else(|| duration("60s", 1_000, 3_600_000)), @@ -1531,7 +1558,13 @@ impl<'de> Deserialize<'de> for EphemeralProcessSpec { .unwrap_or_else(|| duration("24h", 0, 30 * 86_400_000)), wire.incident_hold, ) - .map_err(serde::de::Error::custom) + .map_err(serde::de::Error::custom)?; + match wire.activation_input { + Some(input) => spec + .with_activation_input(input) + .map_err(serde::de::Error::custom), + None => Ok(spec), + } } } diff --git a/packages/d2b-contracts-zone-session/src/v3/component_session.rs b/packages/d2b-contracts-zone-session/src/v3/component_session.rs index 8676cbd95c..f90a01d8c6 100644 --- a/packages/d2b-contracts-zone-session/src/v3/component_session.rs +++ b/packages/d2b-contracts-zone-session/src/v3/component_session.rs @@ -347,7 +347,8 @@ closed_enum!(ServicePackage { ClipboardV3 = 10 => "d2b.clipboard.v3", ClipboardBridgeV3 = 11 => "d2b.clipboard.bridge.v3", ClipboardPickerCoordV3 = 12 => "d2b.clipboard.picker-coord.v3", - NotificationV3 = 13 => "d2b.notification.v3" + NotificationV3 = 13 => "d2b.notification.v3", + ConfigNixosV3 = 14 => "d2b.config-nixos.v3" }); closed_enum!(NoiseProfile { @@ -782,6 +783,37 @@ impl EndpointPolicyIdentity { Ok(()) } + /// Validate an identity permitted to request reconnect generation + /// discovery. Discovery does not authenticate static keys; the subsequent + /// handshake remains responsible for exact policy and key authentication. + pub fn validate_generation_discovery(&self) -> Result<(), ContractError> { + self.validate()?; + let local = self.purpose_class == PurposeClass::Local + && self.noise_profile == NoiseProfile::Nn25519ChaChaPolySha256 + && self.transport_binding.identity_evidence + == IdentityEvidenceRequirement::DirectionalUnix + && matches!( + self.transport_binding.transport, + TransportClass::UnixStream | TransportClass::UnixSeqpacket + ); + let enrolled_guest = self.purpose == EndpointPurpose::ZoneLink + && self.purpose_class == PurposeClass::Enrolled + && self.initiator_role == EndpointRole::ZoneController + && self.responder_role == EndpointRole::GuestAgent + && self.service == ServicePackage::ResourceV3 + && self.noise_profile == NoiseProfile::Kk25519ChaChaPolySha256 + && self.transport_binding.transport == TransportClass::NativeVsock + && self.transport_binding.locality == Locality::GuestLocal + && self.transport_binding.identity_evidence + == IdentityEvidenceRequirement::EnrolledStaticKeys + && self.attachment_policy == AttachmentPolicy::disabled(); + if local || enrolled_guest { + Ok(()) + } else { + Err(ContractError::IdentityEvidenceMismatch) + } + } + pub fn with_generation( &self, reconnect_generation: u64, diff --git a/packages/d2b-contracts-zone-session/src/v3/services.rs b/packages/d2b-contracts-zone-session/src/v3/services.rs index ea3073684a..e7127cfb6a 100644 --- a/packages/d2b-contracts-zone-session/src/v3/services.rs +++ b/packages/d2b-contracts-zone-session/src/v3/services.rs @@ -33,6 +33,7 @@ pub enum V3Service { Audit, Support, Credential, + ConfigNixos, } impl V3Service { @@ -47,6 +48,7 @@ impl V3Service { Self::Audit => "d2b.audit.v3", Self::Support => "d2b.support.v3", Self::Credential => "d2b.credential.v3", + Self::ConfigNixos => "d2b.config-nixos.v3", } } @@ -58,7 +60,7 @@ impl V3Service { } /// Every service package in stable order. - pub const ALL: [Self; 8] = [ + pub const ALL: [Self; 9] = [ Self::Resource, Self::Zone, Self::ZoneLink, @@ -67,6 +69,7 @@ impl V3Service { Self::Audit, Self::Support, Self::Credential, + Self::ConfigNixos, ]; } diff --git a/packages/d2b-contracts-zone-session/src/v3/zone_session.rs b/packages/d2b-contracts-zone-session/src/v3/zone_session.rs index a989ec1e25..820edfe31c 100644 --- a/packages/d2b-contracts-zone-session/src/v3/zone_session.rs +++ b/packages/d2b-contracts-zone-session/src/v3/zone_session.rs @@ -232,7 +232,8 @@ zone_closed_enum!( /// Tags 1 through 6 are preserved from the component-session assignment. /// Tags 7 and 8 are the appended Zone service packages. Tags 9 through /// 13 carry the interaction Provider packages shared with - /// ComponentSession. Protobuf field numbers for the v3 services are + /// ComponentSession. Tag 14 carries the service-only config-nixos + /// Provider. Protobuf field numbers for the v3 services are /// frozen independently of the v2 assignments and are not restated by /// this module. ServicePackage { @@ -262,6 +263,8 @@ zone_closed_enum!( ClipboardPickerCoordV3 = 12 => "d2b.clipboard.picker-coord.v3", /// The desktop notification Provider package. NotificationV3 = 13 => "d2b.notification.v3", + /// The service-only NixOS configuration Provider package. + ConfigNixosV3 = 14 => "d2b.config-nixos.v3", } ); @@ -395,6 +398,11 @@ mod tests { "d2b.clipboard.picker-coord.v3", ), (ServicePackage::NotificationV3, 13, "d2b.notification.v3"), + ( + ServicePackage::ConfigNixosV3, + 14, + "d2b.config-nixos.v3", + ), ]; #[test] @@ -493,7 +501,7 @@ mod tests { Err(BinaryError::UnknownEnumTag) ); assert_eq!( - ServicePackage::from_tag(14), + ServicePackage::from_tag(15), Err(BinaryError::UnknownEnumTag) ); } diff --git a/packages/d2b-contracts-zone-session/tests/contracts.rs b/packages/d2b-contracts-zone-session/tests/contracts.rs index df1a97b94e..9527a4ed27 100644 --- a/packages/d2b-contracts-zone-session/tests/contracts.rs +++ b/packages/d2b-contracts-zone-session/tests/contracts.rs @@ -1,5 +1,12 @@ -use d2b_contracts_zone_session::v3::ZoneSpec; use d2b_contracts_resource::v3::ZoneId; +use d2b_contracts_zone_session::v3::{ + ZoneSpec, + component_session::{ + AttachmentPolicy, AttachmentPolicyKind, EndpointPolicyIdentity, EndpointPurpose, + EndpointRole, IdentityEvidenceRequirement, LimitProfile, Locality, NoiseProfile, + PurposeClass, ServicePackage, TransportBinding, TransportClass, + }, +}; #[test] fn zone_session_contracts_preserve_zone_identity() { @@ -7,3 +14,73 @@ fn zone_session_contracts_preserve_zone_identity() { let _ = std::any::type_name::(); assert_eq!(zone.as_str(), "work"); } + +fn enrolled_guest_discovery_identity() -> EndpointPolicyIdentity { + EndpointPolicyIdentity { + purpose: EndpointPurpose::ZoneLink, + purpose_class: PurposeClass::Enrolled, + initiator_role: EndpointRole::ZoneController, + responder_role: EndpointRole::GuestAgent, + service: ServicePackage::ResourceV3, + schema_fingerprint: [0x11; 32], + noise_profile: NoiseProfile::Kk25519ChaChaPolySha256, + limits: LimitProfile::remote_default(), + transport_binding: TransportBinding { + transport: TransportClass::NativeVsock, + locality: Locality::GuestLocal, + channel_binding: [0x22; 32], + identity_evidence: IdentityEvidenceRequirement::EnrolledStaticKeys, + }, + attachment_policy: AttachmentPolicy::disabled(), + } +} + +#[test] +fn generation_discovery_accepts_only_the_exact_enrolled_guest_profile() { + let identity = enrolled_guest_discovery_identity(); + assert!(identity.validate_generation_discovery().is_ok()); + + let mut wrong_role = identity.clone(); + wrong_role.responder_role = EndpointRole::Relay; + assert!(wrong_role.validate_generation_discovery().is_err()); + + let mut wrong_purpose = identity.clone(); + wrong_purpose.purpose = EndpointPurpose::ResourceService; + assert!(wrong_purpose.validate_generation_discovery().is_err()); + + let mut wrong_noise = identity.clone(); + wrong_noise.noise_profile = NoiseProfile::Nn25519ChaChaPolySha256; + assert!(wrong_noise.validate_generation_discovery().is_err()); + + let mut wrong_evidence = identity.clone(); + wrong_evidence.transport_binding.identity_evidence = + IdentityEvidenceRequirement::DirectionalUnix; + assert!(wrong_evidence.validate_generation_discovery().is_err()); + + let mut wrong_transport = identity.clone(); + wrong_transport.transport_binding.transport = TransportClass::ProviderStream; + assert!(wrong_transport.validate_generation_discovery().is_err()); + + let mut wrong_locality = identity.clone(); + wrong_locality.transport_binding.locality = Locality::Remote; + assert!(wrong_locality.validate_generation_discovery().is_err()); + + let mut attachments = identity.clone(); + attachments.attachment_policy = AttachmentPolicy { + kind: AttachmentPolicyKind::PacketAtomic, + max_per_packet: 1, + max_per_request: 1, + max_per_operation: 1, + max_per_session: 1, + credentials_allowed: false, + }; + assert!(attachments.validate_generation_discovery().is_err()); + + let mut zero_schema = identity.clone(); + zero_schema.schema_fingerprint = [0; 32]; + assert!(zero_schema.validate_generation_discovery().is_err()); + + let mut zero_channel_binding = identity; + zero_channel_binding.transport_binding.channel_binding = [0; 32]; + assert!(zero_channel_binding.validate_generation_discovery().is_err()); +} diff --git a/packages/d2b-core/src/bundle_resolver.rs b/packages/d2b-core/src/bundle_resolver.rs index de6cfb1564..977f3be2b8 100644 --- a/packages/d2b-core/src/bundle_resolver.rs +++ b/packages/d2b-core/src/bundle_resolver.rs @@ -379,7 +379,7 @@ impl ResolvedRunnerIntent { let execution_ref = node .execution_ref .clone() - .unwrap_or_else(|| format!("Guest/{vm_name}")); + .unwrap_or_else(|| default_execution_ref(vm_name, &node.role)); let valid_execution_ref = execution_ref .split_once('/') .is_some_and(|(kind, name)| matches!(kind, "Host" | "Guest") && !name.is_empty()); @@ -458,6 +458,20 @@ impl ResolvedRunnerIntent { } } +/// Resolve the legacy placement default for one trusted VM-DAG role. +/// +/// Host-side infrastructure roles execute in the singleton Host target. +/// The activation runner is the only legacy role whose default placement is +/// the owning Guest. New resource-backed Process rows must carry an explicit +/// `executionRef`. +pub fn default_execution_ref(vm_name: &str, role: &ProcessRole) -> String { + if matches!(role, ProcessRole::ActivationNixosRunner) { + format!("Guest/{vm_name}") + } else { + "Host/host-system".to_owned() + } +} + // Convenience From impls across the wire (`UserNamespaceProfile`) and // intent (`UserNamespaceSpec`) types so layer boundaries can `.into()` // instead of hand-copying fields. @@ -2756,6 +2770,7 @@ fn runner_role_name(role: &ProcessRole) -> Option<&'static str> { ProcessRole::Audio => Some("audio"), ProcessRole::CloudHypervisorRunner => Some("cloud-hypervisor"), ProcessRole::QemuMediaRunner => Some("qemu-media"), + ProcessRole::ActivationNixosRunner => Some("activation-nixos-runner"), ProcessRole::VsockRelay => Some("vsock-relay"), ProcessRole::OtelHostBridge => Some("otel-host-bridge"), ProcessRole::Usbip => Some("usbip"), @@ -2789,6 +2804,10 @@ fn legacy_runner_spec(vm_name: &str, role: &ProcessRole) -> Option<(String, Vec< // processes.json. There is no Cloud Hypervisor-compatible legacy // fallback for this runtime kind. ProcessRole::QemuMediaRunner => return None, + // Activation runners are Guest-local and have no host-side legacy + // fallback. Their executable and argv must be present in the + // trusted per-Guest process DAG. + ProcessRole::ActivationNixosRunner => return None, ProcessRole::VsockRelay => ("socat", format!("d2b-otel-relay@{vm_name}")), // OtelHostBridge must always carry the closed argv from // processes.json; it has no legacy singleton fallback. diff --git a/packages/d2b-core/src/processes.rs b/packages/d2b-core/src/processes.rs index 187237c43f..9435ffe4af 100644 --- a/packages/d2b-core/src/processes.rs +++ b/packages/d2b-core/src/processes.rs @@ -72,7 +72,8 @@ pub struct ProcessNode { pub id: NodeId, /// Canonical Host or Guest execution target for this trusted runner. /// - /// Omitted by legacy bundles, which resolve to `Guest/`. + /// Omitted by legacy bundles, which resolve to the singleton Host target + /// except for the Guest-local activation runner. #[serde(default, skip_serializing_if = "Option::is_none")] pub execution_ref: Option, /// Canonical execution domain for this trusted runner. @@ -255,6 +256,13 @@ pub enum ProcessRole { CloudHypervisorRunner, /// QEMU media runner. QemuMediaRunner, + /// Target-local one-shot NixOS activation runner. + /// + /// This role is emitted only for the Guest execution target. The + /// activation Provider creates an EphemeralProcess resource and the + /// target-local process Provider resolves this role from the trusted + /// bundle. + ActivationNixosRunner, /// vsock relay sidecar. VsockRelay, /// Host-to-observability-VM OTLP bridge. diff --git a/packages/d2b-core/src/runtime.rs b/packages/d2b-core/src/runtime.rs index a64fe5b610..9321c27527 100644 --- a/packages/d2b-core/src/runtime.rs +++ b/packages/d2b-core/src/runtime.rs @@ -347,6 +347,7 @@ impl From<&ProcessRole> for RuntimeServiceRole { } ProcessRole::Audio => Self::Audio, ProcessRole::CloudHypervisorRunner | ProcessRole::QemuMediaRunner => Self::Hypervisor, + ProcessRole::ActivationNixosRunner => Self::GuestControl, ProcessRole::VsockRelay => Self::Network, ProcessRole::GuestSshReadiness | ProcessRole::GuestControlHealth => Self::GuestControl, ProcessRole::Usbip | ProcessRole::SecurityKeyFrontend => Self::Usb, diff --git a/packages/d2b-core/tests/bundle_resolver_runner_intent_parity.rs b/packages/d2b-core/tests/bundle_resolver_runner_intent_parity.rs index e7fa631140..e8b890d54b 100644 --- a/packages/d2b-core/tests/bundle_resolver_runner_intent_parity.rs +++ b/packages/d2b-core/tests/bundle_resolver_runner_intent_parity.rs @@ -311,7 +311,7 @@ fn generic_process_template_lookup_does_not_require_resource_name_to_match_role_ let resource_name = "audio-resource"; let intent = resolver .find_runner_intent_for_process( - "Guest/test-vm", + "Host/host-system", d2b_core::processes::ProcessExecutionDomain::System, None, "audio", diff --git a/packages/d2b-process-conformance/src/identity.rs b/packages/d2b-process-conformance/src/identity.rs index b4aefc2522..5970c6cc3d 100644 --- a/packages/d2b-process-conformance/src/identity.rs +++ b/packages/d2b-process-conformance/src/identity.rs @@ -110,6 +110,11 @@ macro_rules! opaque_digest { Self(bytes) } + /// Return the opaque digest bytes for a typed contract adapter. + pub const fn as_bytes(self) -> [u8; 32] { + self.0 + } + /// Render the digest as lowercase hex. pub fn to_hex(self) -> String { let mut out = String::with_capacity(64); diff --git a/packages/d2b-process-conformance/src/lib.rs b/packages/d2b-process-conformance/src/lib.rs index 068794ad65..34e873a8ef 100644 --- a/packages/d2b-process-conformance/src/lib.rs +++ b/packages/d2b-process-conformance/src/lib.rs @@ -53,5 +53,5 @@ pub use terminal::{ }; pub use ticket::{ CancellationBinding, CompiledDigests, InheritedFdTable, LaunchTicket, MAX_INHERITED_FDS, - MAX_LAUNCH_DEADLINE_MS, OperationBinding, ReadinessExpectation, + MAX_LAUNCH_DEADLINE_MS, GuestExecutionBinding, OperationBinding, ReadinessExpectation, }; diff --git a/packages/d2b-process-conformance/src/testing.rs b/packages/d2b-process-conformance/src/testing.rs index 616cea9681..d40b96b1be 100644 --- a/packages/d2b-process-conformance/src/testing.rs +++ b/packages/d2b-process-conformance/src/testing.rs @@ -26,7 +26,9 @@ use crate::identity::{ WaitReapOwner, }; use crate::port::{AdoptionCandidate, LaunchedProcess, ProcessLaunchEffectPort, StopClass}; -use crate::ticket::{CompiledDigests, LaunchTicket, OperationBinding}; +use crate::ticket::{ + CompiledDigests, GuestExecutionBinding, LaunchTicket, OperationBinding, +}; /// Drive a future to completion on the calling thread. /// @@ -209,6 +211,7 @@ pub mod fixtures { user_ref: Option, selected_provider: BoundedToken, expected_identity: BTreeSet, + guest_execution_binding: bool, } impl TicketBuilder { @@ -251,9 +254,16 @@ pub mod fixtures { self } + /// Omit the default Guest target binding for negative tests. + pub fn without_guest_execution_binding(mut self) -> Self { + self.guest_execution_binding = false; + self + } + /// Build the ticket. pub fn build(self) -> Result { - LaunchTicket::new( + let is_guest = self.execution_ref.resource_type().as_str() == "Guest"; + let ticket = LaunchTicket::new( self.process_ref, operation_uid(), ResourceGeneration::new(1).expect("nonzero"), @@ -268,7 +278,21 @@ pub mod fixtures { compiled_digests(), OperationBinding::new(operation_uid(), 30_000)?, self.expected_identity, - ) + )?; + if is_guest && self.guest_execution_binding { + ticket.with_guest_execution_binding(GuestExecutionBinding::new( + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000") + .expect("fixture Guest UID"), + ConfigurationDigest::from_bytes([8; 32]), + d2b_contracts_resource::v3::identity::ReconnectGeneration::new(1) + .expect("fixture session generation"), + 1, + ResourceGeneration::new(1).expect("fixture Provider generation"), + ControllerGeneration::new(1).expect("fixture controller generation"), + )?) + } else { + Ok(ticket) + } } } @@ -281,6 +305,7 @@ pub mod fixtures { user_ref: None, selected_provider: token("system-systemd"), expected_identity: BTreeSet::from([IdentityBinding::Cgroup]), + guest_execution_binding: true, } } } diff --git a/packages/d2b-process-conformance/src/ticket.rs b/packages/d2b-process-conformance/src/ticket.rs index aa40bd9649..739493a8b7 100644 --- a/packages/d2b-process-conformance/src/ticket.rs +++ b/packages/d2b-process-conformance/src/ticket.rs @@ -4,6 +4,7 @@ use std::collections::BTreeSet; use std::fmt; use d2b_contracts_resource::v3::{ + ActivationRunnerInput, execution_policy::{BoundedToken, ExecutionDomain}, }; use d2b_contracts_resource::v3::{ @@ -205,6 +206,76 @@ struct ControllerAssignmentBinding { resource_client_binding: ConfigurationDigest, } +/// Authenticated target binding required for Guest Process execution. +/// +/// The Guest daemon derives this commitment from its enrolled Guest identity +/// and current ComponentSession. It carries no raw boot id or transport +/// handle, but fences every launch against Guest replacement, reconnect, +/// controller, and assignment generations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GuestExecutionBinding { + target_uid: ResourceUid, + boot_identity_digest: ConfigurationDigest, + session_generation: ReconnectGeneration, + assignment_epoch: u64, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, +} + +impl GuestExecutionBinding { + /// Construct an exact Guest target binding. + pub fn new( + target_uid: ResourceUid, + boot_identity_digest: ConfigurationDigest, + session_generation: ReconnectGeneration, + assignment_epoch: u64, + provider_generation: ResourceGeneration, + controller_generation: ControllerGeneration, + ) -> Result { + if boot_identity_digest.is_zero() || assignment_epoch == 0 { + return Err(ProcessConformanceError::InvalidTicket); + } + Ok(Self { + target_uid, + boot_identity_digest, + session_generation, + assignment_epoch, + provider_generation, + controller_generation, + }) + } + + /// Borrow the exact Guest Resource UID. + pub const fn target_uid(&self) -> &ResourceUid { + &self.target_uid + } + + /// Return the kernel boot-identity commitment. + pub const fn boot_identity_digest(&self) -> ConfigurationDigest { + self.boot_identity_digest + } + + /// Return the authenticated parent ComponentSession generation. + pub const fn session_generation(&self) -> ReconnectGeneration { + self.session_generation + } + + /// Return the Core assignment epoch. + pub const fn assignment_epoch(&self) -> u64 { + self.assignment_epoch + } + + /// Return the installed Provider generation. + pub const fn provider_generation(&self) -> ResourceGeneration { + self.provider_generation + } + + /// Return the controller generation. + pub const fn controller_generation(&self) -> ControllerGeneration { + self.controller_generation + } +} + impl InheritedFdTable { /// Build a bounded private table binding. pub fn new(digest: ConfigurationDigest, count: u16) -> Result { @@ -257,8 +328,10 @@ pub struct LaunchTicket { expected_identity: BTreeSet, expected_identity_digest: Option, readiness: ReadinessExpectation, + activation_input: Option, inherited_fd_table: InheritedFdTable, sandbox: Option, + guest_execution: Option, controller_launch: Option, assignment: Option, } @@ -334,8 +407,10 @@ impl LaunchTicket { expected_identity, expected_identity_digest: None, readiness: ReadinessExpectation::None, + activation_input: None, inherited_fd_table, sandbox: None, + guest_execution: None, controller_launch: None, assignment: None, }) @@ -426,14 +501,51 @@ impl LaunchTicket { { return Err(ProcessConformanceError::InvalidTicket); } + if self.activation_input.is_some() + && (self.process_ref.resource_type().as_str() != "EphemeralProcess" + || self.template.as_str() != "activation-nixos-runner" + || !matches!( + self.execution_ref.resource_type().as_str(), + "Host" | "Guest" + ) + || self + .activation_input + .as_ref() + .is_some_and(|input| input.target_generation == 0)) + { + return Err(ProcessConformanceError::InvalidTicket); + } if self .resource_revision .is_some_and(|revision| revision.get() == 0) { return Err(ProcessConformanceError::InvalidTicket); } + if self.execution_ref.resource_type().as_str() == "Guest" + && self.controller_launch.is_none() + && self.guest_execution.is_none() + { + return Err(ProcessConformanceError::InvalidTicket); + } + if self.execution_ref.resource_type().as_str() == "Host" + && self.guest_execution.is_some() + { + return Err(ProcessConformanceError::InvalidTicket); + } self.digests.validate()?; self.readiness.validate()?; + if let Some(binding) = &self.guest_execution { + if binding.controller_generation != self.controller_generation { + return Err(ProcessConformanceError::InvalidTicket); + } + if let Some(assignment) = &self.assignment + && (assignment.provider_generation != binding.provider_generation + || assignment.session_generation != binding.session_generation + || assignment.assignment_epoch != binding.assignment_epoch) + { + return Err(ProcessConformanceError::InvalidTicket); + } + } if let Some(binding) = self.controller_launch { if binding.provider_generation.get() == 0 || binding.target_session_generation.get() == 0 @@ -506,6 +618,49 @@ impl LaunchTicket { self } + /// Bind the closed activation-runner stdin input. + pub fn with_activation_input( + mut self, + input: ActivationRunnerInput, + ) -> Result { + if self.process_ref.resource_type().as_str() != "EphemeralProcess" + || self.template.as_str() != "activation-nixos-runner" + || !matches!( + self.execution_ref.resource_type().as_str(), + "Host" | "Guest" + ) + || input.target_generation == 0 + { + return Err(ProcessConformanceError::InvalidTicket); + } + self.activation_input = Some(input); + Ok(self) + } + + /// Bind a generic Guest Process to its authenticated target/session. + pub fn with_guest_execution_binding( + mut self, + binding: GuestExecutionBinding, + ) -> Result { + if self.execution_ref.resource_type().as_str() != "Guest" + || self.guest_execution.is_some() + { + return Err(ProcessConformanceError::InvalidTicket); + } + self.guest_execution = Some(binding); + Ok(self) + } + + /// Borrow the authenticated Guest execution binding, when present. + pub const fn guest_execution_binding(&self) -> Option<&GuestExecutionBinding> { + self.guest_execution.as_ref() + } + + /// Borrow the closed activation-runner stdin input, when present. + pub const fn activation_input(&self) -> Option<&ActivationRunnerInput> { + self.activation_input.as_ref() + } + /// Borrow the typed sandbox plan, when this launch is a generic Process. pub const fn sandbox_plan(&self) -> Option<&SandboxPlan> { self.sandbox.as_ref() @@ -770,6 +925,50 @@ mod tests { ); } + #[test] + fn guest_process_tickets_require_target_execution_binding() { + let ticket = fixtures::ticket_builder() + .execution_ref(ResourceRef::parse("Guest/dev-vm").unwrap()) + .without_guest_execution_binding() + .build() + .unwrap(); + assert_eq!( + ticket.validate(), + Err(ProcessConformanceError::InvalidTicket) + ); + } + + #[test] + fn guest_execution_binding_matches_assignment_identity() { + let session = d2b_contracts_resource::v3::identity::ReconnectGeneration::new(3).unwrap(); + let binding = GuestExecutionBinding::new( + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ConfigurationDigest::from_bytes([9; 32]), + session, + 7, + ResourceGeneration::new(2).unwrap(), + ControllerGeneration::new(1).unwrap(), + ) + .unwrap(); + let ticket = fixtures::ticket_builder() + .execution_ref(ResourceRef::parse("Guest/dev-vm").unwrap()) + .without_guest_execution_binding() + .build() + .unwrap() + .with_guest_execution_binding(binding) + .unwrap() + .with_resource_revision(ZoneRevision::new(1)) + .unwrap() + .with_assignment_binding( + ResourceGeneration::new(2).unwrap(), + session, + 7, + ConfigurationDigest::from_bytes([10; 32]), + ) + .unwrap(); + assert!(ticket.validate_assignment().is_ok()); + } + #[test] fn the_deadline_is_bounded_and_the_ticket_debug_is_redacted() { let uid = fixtures::operation_uid(); diff --git a/packages/d2b-provider-activation-nixos/Cargo.toml b/packages/d2b-provider-activation-nixos/Cargo.toml index 0a2b159b68..96deffb6f1 100644 --- a/packages/d2b-provider-activation-nixos/Cargo.toml +++ b/packages/d2b-provider-activation-nixos/Cargo.toml @@ -14,3 +14,4 @@ d2b-contracts-zone-session = { path = "../d2b-contracts-zone-session", version = d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } serde = { workspace = true } serde_json = { workspace = true } +sha2 = { workspace = true } diff --git a/packages/d2b-provider-activation-nixos/src/controller.rs b/packages/d2b-provider-activation-nixos/src/controller.rs index 4da3ef8577..b05d453190 100644 --- a/packages/d2b-provider-activation-nixos/src/controller.rs +++ b/packages/d2b-provider-activation-nixos/src/controller.rs @@ -6,10 +6,23 @@ use d2b_contracts_resource::v3::{ ActivationMode, ActivationOutcomeCode, ArtifactId, + ActivationRunnerInput, + process::{ + EphemeralProcessSpec, ExecutionSpec, NamespaceClass, ProcessClass, SandboxSpec, + }, + EnvironmentClass, + ExecutionDomain, + ResourceName, NixosGenerationSpec, ResourcePhase, ResourceRef, }; +use sha2::{Digest, Sha256}; + +/// The target-local Process template used for activation effects. +pub const ACTIVATION_RUNNER_TEMPLATE: &str = "activation-nixos-runner"; +/// The generic one-shot process resource type used for activation effects. +pub const ACTIVATION_RUNNER_RESOURCE_TYPE: &str = "EphemeralProcess"; /// Caller role derived from the authenticated daemon request. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -98,7 +111,13 @@ pub struct GenerationObservation { impl GenerationObservation { /// Construct a bounded observation. pub fn new(name: impl Into, phase: GenerationPhase) -> Self { - Self::terminal(name, phase, 0) + let name = name.into(); + let ordinal = name + .rsplit('-') + .next() + .and_then(|value| value.parse::().ok()) + .unwrap_or(0); + Self::terminal(name, phase, ordinal) } /// Construct a bounded terminal observation. @@ -131,16 +150,140 @@ impl GenerationObservation { /// A typed runner launch request. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RunnerRequest { + /// Deterministic child resource name, derived from the generation. + pub runner_name: ResourceName, /// Target execution context. pub execution_ref: ResourceRef, /// Private-catalog artifact identifier. pub system_artifact_id: ArtifactId, /// Requested activation mode. pub activation_mode: ActivationMode, + /// Target generation ordinal bound to the runner stdin envelope. + pub target_generation: u64, /// Activation runners start without an in-namespace root UID. pub start_root: bool, } +/// Return the deterministic target-local child name for one generation. +/// +/// The name is derived from the qualified generation reference rather than +/// an operator-provided value, so retries and daemon restarts converge on the +/// same EphemeralProcess. +pub fn activation_runner_name(generation: &ResourceRef) -> ResourceName { + let readable = format!("activation-nixos--runner--{}", generation.name().as_str()); + if let Ok(name) = ResourceName::parse(readable) { + return name; + } + let mut digest = Sha256::new(); + digest.update(b"d2b-activation-runner-v1"); + digest.update([0]); + digest.update(generation.to_canonical_string().as_bytes()); + let digest = digest.finalize(); + let suffix = digest[..12] + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + ResourceName::parse(format!("activation-runner-{suffix}")) + .expect("activation runner name is bounded and lowercase") +} + +/// Return the deterministic child reference owned by one generation. +pub fn activation_runner_ref(generation: &ResourceRef) -> ResourceRef { + let canonical = format!( + "{ACTIVATION_RUNNER_RESOURCE_TYPE}/{}", + activation_runner_name(generation).as_str() + ); + ResourceRef::parse(&canonical) + .expect("activation runner reference is valid") +} + +/// Build the closed EphemeralProcess contract for one activation request. +/// +pub fn activation_runner_spec(request: &RunnerRequest) -> EphemeralProcessSpec { + let template = d2b_contracts_resource::v3::BoundedToken::parse(ACTIVATION_RUNNER_TEMPLATE) + .expect("static activation runner template"); + let sandbox = SandboxSpec::new( + vec![NamespaceClass::Pid, NamespaceClass::Mount, NamespaceClass::Ipc], + Vec::new(), + d2b_contracts_resource::v3::BoundedToken::parse("activation-nixos-runner") + .expect("static activation runner seccomp class"), + true, + request.start_root, + EnvironmentClass::Minimal, + true, + Some("0022".to_owned()), + 0, + None, + ) + .expect("static activation runner sandbox"); + let budget = d2b_contracts_resource::v3::BudgetSpec::new( + Some(d2b_contracts_resource::v3::CpuBudget { + request: Some( + d2b_contracts_resource::v3::MilliCpu::parse("100m") + .expect("static activation runner cpu request"), + ), + limit: Some( + d2b_contracts_resource::v3::MilliCpu::parse("2000m") + .expect("static activation runner cpu limit"), + ), + }), + Some(d2b_contracts_resource::v3::MemoryBudget { + request: Some( + d2b_contracts_resource::v3::ByteQuantity::parse("32Mi") + .expect("static activation runner memory request"), + ), + limit: Some( + d2b_contracts_resource::v3::ByteQuantity::parse("128Mi") + .expect("static activation runner memory limit"), + ), + }), + Some(d2b_contracts_resource::v3::CountBudget { limit: Some(128) }), + Some(d2b_contracts_resource::v3::CountBudget { limit: Some(512) }), + None, + None, + None, + ) + .expect("static activation runner budget"); + let execution = ExecutionSpec::new( + request.execution_ref.clone(), + Some(ExecutionDomain::System), + None, + ProcessClass::Worker, + template, + None, + Vec::new(), + Vec::new(), + sandbox, + budget, + None, + Vec::new(), + Default::default(), + ) + .expect("static activation runner execution"); + let spec = EphemeralProcessSpec::new( + execution, + d2b_contracts_resource::v3::DurationMs::parse("120s", 1_000, 3_600_000) + .expect("static activation runner start deadline"), + d2b_contracts_resource::v3::DurationMs::parse("600s", 1_000, 86_400_000) + .expect("static activation runner runtime deadline"), + d2b_contracts_resource::v3::DurationMs::parse("1h", 0, 7 * 86_400_000) + .expect("static activation runner success ttl"), + d2b_contracts_resource::v3::DurationMs::parse("24h", 0, 30 * 86_400_000) + .expect("static activation runner failure ttl"), + false, + ) + .expect("static activation runner process"); + spec.with_activation_input( + ActivationRunnerInput::new( + request.system_artifact_id.clone(), + request.target_generation, + request.activation_mode, + ) + .expect("activation runner generation is nonzero"), + ) + .expect("activation runner accepts its typed input") +} + /// Stable controller failures. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ActivationError { @@ -258,10 +401,18 @@ impl ActivationController { GenerationPhase::Pending | GenerationPhase::Degraded ) && spec.activation_mode() != ActivationMode::Adopt { + let generation_ref = format!( + "activation-nixos.d2bus.org.NixosGeneration/{}", + observed.name() + ); vec![RunnerRequest { + runner_name: activation_runner_name( + &ResourceRef::parse(&generation_ref).expect("generation reference is valid"), + ), execution_ref: spec.execution_ref().clone(), system_artifact_id: spec.system_artifact_id().clone(), activation_mode: spec.activation_mode(), + target_generation: observed.ordinal(), start_root: true, }] } else { diff --git a/packages/d2b-provider-activation-nixos/src/lib.rs b/packages/d2b-provider-activation-nixos/src/lib.rs index 1105b517d4..7f8887d715 100644 --- a/packages/d2b-provider-activation-nixos/src/lib.rs +++ b/packages/d2b-provider-activation-nixos/src/lib.rs @@ -8,8 +8,9 @@ pub mod manifest; pub mod runner; pub use controller::{ - ActivationCaller, ActivationController, ActivationError, CallerRole, GenerationObservation, - GenerationPhase, RetentionPlan, RunnerRequest, RunnerResult, + activation_runner_name, activation_runner_ref, activation_runner_spec, ActivationCaller, + ActivationController, ActivationError, CallerRole, GenerationObservation, GenerationPhase, + RetentionPlan, RunnerRequest, RunnerResult, }; pub use manifest::ActivationManifest; pub use runner::{ diff --git a/packages/d2b-provider-activation-nixos/src/runner.rs b/packages/d2b-provider-activation-nixos/src/runner.rs index 80d5ffa2fa..cbee34520c 100644 --- a/packages/d2b-provider-activation-nixos/src/runner.rs +++ b/packages/d2b-provider-activation-nixos/src/runner.rs @@ -16,6 +16,8 @@ pub struct ActivationRunnerRequest { pub system_artifact_id: ArtifactId, /// Host or Guest target. pub execution_ref: ResourceRef, + /// Target generation ordinal. + pub target_generation: u64, /// Requested activation mode. pub activation_mode: ActivationMode, } @@ -101,7 +103,8 @@ impl ActivationRunner { if !matches!( request.execution_ref.resource_type().as_str(), "Host" | "Guest" - ) { + ) || request.target_generation == 0 + { return Err(ActivationRunnerError::InvalidRequest); } let outcome = helper.activate(request)?; diff --git a/packages/d2b-provider-activation-nixos/tests/reconcile.rs b/packages/d2b-provider-activation-nixos/tests/reconcile.rs index 577dfd930a..2289959399 100644 --- a/packages/d2b-provider-activation-nixos/tests/reconcile.rs +++ b/packages/d2b-provider-activation-nixos/tests/reconcile.rs @@ -6,7 +6,8 @@ use d2b_contracts_resource::v3::{ ResourceRef, }; use d2b_provider_activation_nixos::{ - ActivationCaller, ActivationController, CallerRole, GenerationObservation, GenerationPhase, + activation_runner_name, activation_runner_ref, ActivationCaller, ActivationController, + CallerRole, GenerationObservation, GenerationPhase, }; fn spec() -> NixosGenerationSpec { @@ -51,9 +52,74 @@ fn compatible_generation_starts_one_typed_runner() { .unwrap(); assert_eq!(result.runner_requests().len(), 1); assert!(result.runner_requests()[0].start_root); + assert_eq!( + result.runner_requests()[0].runner_name, + activation_runner_name( + &ResourceRef::parse( + "activation-nixos.d2bus.org.NixosGeneration/gen-7" + ) + .unwrap() + ) + ); assert_eq!(result.phase(), ResourcePhase::Pending); } +#[test] +fn activation_runner_reference_is_stable_and_target_local() { + let generation = + ResourceRef::parse("activation-nixos.d2bus.org.NixosGeneration/gen-7").unwrap(); + assert_eq!(activation_runner_ref(&generation), activation_runner_ref(&generation)); + assert_eq!( + activation_runner_ref(&generation).resource_type().as_str(), + "EphemeralProcess" + ); + assert_ne!( + activation_runner_ref(&generation), + activation_runner_ref( + &ResourceRef::parse("activation-nixos.d2bus.org.NixosGeneration/gen-8").unwrap() + ) + ); +} + +#[test] +fn activation_runner_spec_is_closed_and_bounded() { + let generation = + ResourceRef::parse("activation-nixos.d2bus.org.NixosGeneration/gen-7").unwrap(); + let controller = ActivationController::new(3); + let planned = controller + .reconcile( + &spec(), + &caller(), + &[], + GenerationObservation::new("gen-7", GenerationPhase::Pending), + ) + .unwrap(); + let runner = d2b_provider_activation_nixos::activation_runner_spec( + &planned.runner_requests()[0], + ); + let rendered = serde_json::to_value(&runner).expect("runner spec is serializable"); + assert_eq!( + rendered["activationInput"]["systemArtifactId"], + "dev-vm-system" + ); + assert_eq!(rendered["activationInput"]["targetGeneration"], 7); + assert_eq!(rendered["activationInput"]["activationMode"], "switch"); + assert_eq!( + runner.execution().execution_ref(), + &ResourceRef::parse("Guest/dev-vm").unwrap() + ); + assert_eq!(runner.execution().template().as_str(), "activation-nixos-runner"); + assert_eq!(runner.execution().process_class(), d2b_contracts_resource::v3::ProcessClass::Worker); + assert!(runner.execution().sandbox().start_root()); + assert!(runner.execution().sandbox().no_new_privileges()); + assert_eq!(runner.start_deadline().as_str(), "120s"); + assert_eq!(runner.runtime_deadline().as_str(), "600s"); + assert_eq!( + activation_runner_name(&generation).as_str(), + "activation-nixos--runner--gen-7" + ); +} + #[test] fn unauthorized_or_foreign_callers_refuse_before_runner_creation() { let controller = ActivationController::new(3); diff --git a/packages/d2b-provider-activation-nixos/tests/runner.rs b/packages/d2b-provider-activation-nixos/tests/runner.rs index 2c2cdd04dc..2aa41e38c6 100644 --- a/packages/d2b-provider-activation-nixos/tests/runner.rs +++ b/packages/d2b-provider-activation-nixos/tests/runner.rs @@ -24,6 +24,7 @@ fn request(execution_ref: &str) -> ActivationRunnerRequest { ActivationRunnerRequest { system_artifact_id: ArtifactId::parse("dev-vm-system").unwrap(), execution_ref: ResourceRef::parse(execution_ref).expect("valid execution reference"), + target_generation: 7, activation_mode: ActivationMode::Switch, } } diff --git a/packages/d2b-provider-config-nixos/BUILD.bazel b/packages/d2b-provider-config-nixos/BUILD.bazel new file mode 100644 index 0000000000..7400ffd533 --- /dev/null +++ b/packages/d2b-provider-config-nixos/BUILD.bazel @@ -0,0 +1,70 @@ +load("@crates//:defs.bzl", "all_crate_deps") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test", "rust_doc_test") + +exports_files(["BUILD.bazel", "Cargo.toml"], visibility = ["//visibility:public"]) + +rust_library( + name = "d2b_provider_config_nixos", + srcs = glob(["src/**/*.rs"]), + compile_data = ["Cargo.toml"], + deps = [ + "//packages/d2b-contracts-control:d2b_contracts_control", + "//packages/d2b-contracts-resource:d2b_contracts_resource", + "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", + ] + all_crate_deps(normal = True, cargo_only = True), + visibility = ["//visibility:public"], +) + +rust_test( + name = "service_contract", + srcs = ["tests/service_contract.rs"], + compile_data = ["Cargo.toml"], + deps = [ + ":d2b_provider_config_nixos", + "//packages/d2b-contracts-resource:d2b_contracts_resource", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_test( + name = "config_lifecycle", + srcs = ["tests/config_lifecycle.rs"], + compile_data = ["Cargo.toml"], + deps = [ + ":d2b_provider_config_nixos", + "//packages/d2b-contracts-resource:d2b_contracts_resource", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_test( + name = "redaction", + srcs = ["tests/redaction.rs"], + compile_data = ["Cargo.toml"], + deps = [ + ":d2b_provider_config_nixos", + "//packages/d2b-contracts-resource:d2b_contracts_resource", + ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), +) + +rust_doc_test( + name = "d2b_provider_config_nixos_doc_test", + crate = ":d2b_provider_config_nixos", +) + +filegroup( + name = "cargo_workspace_sources", + srcs = glob( + [ + "Cargo.lock", + "Cargo.toml", + "benches/**/*.rs", + "build.rs", + "examples/**/*.rs", + "src/**/*.rs", + "tests/**/*.rs", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) + +test_suite(name = "all-tests", tests = [":service_contract", ":config_lifecycle", ":redaction"]) diff --git a/packages/d2b-provider-config-nixos/Cargo.toml b/packages/d2b-provider-config-nixos/Cargo.toml new file mode 100644 index 0000000000..5b38d653e1 --- /dev/null +++ b/packages/d2b-provider-config-nixos/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "d2b-provider-config-nixos" +version = "0.0.0-bootstrap" +edition = "2024" +publish = false +license.workspace = true + +[lints] +workspace = true + +[dependencies] +async-trait = "0.1" +d2b-contracts-control = { path = "../d2b-contracts-control", version = "0.0.0-bootstrap" } +d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } +d2b-contracts-zone-session = { path = "../d2b-contracts-zone-session", version = "0.0.0-bootstrap" } +base64 = { workspace = true } +libc = "0.2" +schemars = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +rustix = { workspace = true } +ttrpc = { version = "0.9.0", default-features = false, features = ["async", "sync"] } diff --git a/packages/d2b-provider-config-nixos/src/controller.rs b/packages/d2b-provider-config-nixos/src/controller.rs new file mode 100644 index 0000000000..73d61434f9 --- /dev/null +++ b/packages/d2b-provider-config-nixos/src/controller.rs @@ -0,0 +1,399 @@ +//! Authorization and bounded document policy for the config-nixos service. + +use std::fmt; + +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use d2b_contracts_resource::v3::ResourceRef; +use sha2::{Digest, Sha256}; + +use crate::{ + GUEST_CONFIG_IDENTIFIER, MAX_CONFIG_BYTES, SERVICE_NAME, SERVICE_PACKAGE, + service::{ + ConfigApproveRequest, ConfigDiffRequest, ConfigRejectRequest, ConfigServiceDescriptor, + ConfigStageRequest, ConfigStatusRequest, ConfigSyncRequest, ConfigSyncResponse, + validate_destination, validate_guest_ref, validate_identifier, validate_view_identifier, + }, +}; + +/// Caller role carried by the authenticated ComponentSession or local +/// operator session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigCaller { + /// The owning Guest session, allowed to read its own document only. + Guest, + /// An administrator, allowed to operate on host staging state. + Admin, + /// A lifecycle-authorized operator, allowed to operate on host staging. + Lifecycle, + /// An ordinary user, which has no config-management authority. + User, +} + +impl ConfigCaller { + fn can_read(self) -> bool { + matches!(self, Self::Guest | Self::Admin | Self::Lifecycle) + } + + fn can_stage(self) -> bool { + matches!(self, Self::Admin | Self::Lifecycle) + } +} + +/// Redacted evidence proving that a Guest read belongs to the current +/// authenticated ComponentSession. +#[derive(Clone, PartialEq, Eq)] +pub struct GuestSessionEvidence { + guest_ref: ResourceRef, + boot_identity: String, + reconnect_generation: u64, + authenticated: bool, +} + +impl GuestSessionEvidence { + /// Construct evidence after binding the Guest and reconnect generation. + pub fn new( + guest_ref: ResourceRef, + boot_identity: impl Into, + reconnect_generation: u64, + ) -> Result { + if guest_ref.resource_type().as_str() != "Guest" + || guest_ref.name().as_str().is_empty() + || reconnect_generation == 0 + { + return Err(ConfigError::SessionMismatch); + } + let boot_identity = boot_identity.into(); + if boot_identity.is_empty() || boot_identity.len() > 128 { + return Err(ConfigError::SessionMismatch); + } + Ok(Self { + guest_ref, + boot_identity, + reconnect_generation, + authenticated: true, + }) + } + + /// Borrow the authenticated Guest reference. + pub fn guest_ref(&self) -> &ResourceRef { + &self.guest_ref + } + + /// Borrow the bounded boot identity commitment. + pub fn boot_identity(&self) -> &str { + &self.boot_identity + } + + /// Return the reconnect generation bound by admission. + pub const fn reconnect_generation(&self) -> u64 { + self.reconnect_generation + } + + /// Return whether the evidence is still current. + pub const fn authenticated(&self) -> bool { + self.authenticated + } + + /// Mark the session stale without retaining a usable read capability. + pub fn stale(mut self) -> Self { + self.authenticated = false; + self + } +} + +impl fmt::Debug for GuestSessionEvidence { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("GuestSessionEvidence") + .field("guest_ref", &self.guest_ref) + .field("boot_identity", &"") + .field("reconnect_generation", &self.reconnect_generation) + .field("authenticated", &self.authenticated) + .finish() + } +} + +/// A validated Guest configuration document. +#[derive(Clone, PartialEq, Eq)] +pub struct GuestConfigDocument { + bytes: Vec, +} + +impl GuestConfigDocument { + /// Validate and retain one bounded non-empty UTF-8 Nix document. + pub fn new(bytes: impl Into>) -> Result { + let bytes = bytes.into(); + if bytes.is_empty() { + return Err(ConfigError::EmptyDocument); + } + if bytes.len() > MAX_CONFIG_BYTES { + return Err(ConfigError::DocumentTooLarge); + } + if std::str::from_utf8(&bytes).is_err() { + return Err(ConfigError::InvalidUtf8); + } + Ok(Self { bytes }) + } + + /// Borrow the validated document bytes. + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + + /// Return the document size. + pub const fn len(&self) -> usize { + self.bytes.len() + } + + /// Return whether the document is empty. + pub const fn is_empty(&self) -> bool { + self.bytes.is_empty() + } + + /// Return a content digest computed from the received bytes. + pub fn sha256(&self) -> String { + let digest = Sha256::digest(&self.bytes); + format!("sha256:{digest:x}") + } + + /// Return the bounded base64 representation used by the service DTO. + pub fn content_base64(&self) -> String { + STANDARD.encode(&self.bytes) + } +} + +impl fmt::Debug for GuestConfigDocument { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("GuestConfigDocument") + .field("bytes", &self.bytes.len()) + .field("sha256", &self.sha256()) + .finish() + } +} + +/// Closed config service operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ConfigOperation { + /// Read the Guest's canonical config document. + ReadGuestConfig, + /// Stage a received document in the host-side staging client. + Stage, + /// Compare a staged document with the caller-selected local view. + Diff, + /// Approve a staged document for a caller-authorized destination. + Approve, + /// Reject and remove a staged document. + Reject, + /// Report pending staging state. + Status, +} + +impl ConfigOperation { + /// Return the exact generated service member. + pub const fn as_str(self) -> &'static str { + match self { + Self::ReadGuestConfig => "ConfigNixosService/ReadGuestConfig", + Self::Stage => "ConfigNixosService/Stage", + Self::Diff => "ConfigNixosService/Diff", + Self::Approve => "ConfigNixosService/Approve", + Self::Reject => "ConfigNixosService/Reject", + Self::Status => "ConfigNixosService/Status", + } + } + + /// Return every operation in stable order. + pub const ALL: [Self; 6] = [ + Self::ReadGuestConfig, + Self::Stage, + Self::Diff, + Self::Approve, + Self::Reject, + Self::Status, + ]; +} + +/// Stable errors emitted by config-nixos without guest content or paths. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigError { + /// The request named something other than a Guest or used a wrong + /// closed identifier. + InvalidRequest, + /// The caller is not allowed to perform this operation. + Unauthorized, + /// The authenticated Guest session is absent, stale, or mismatched. + SessionMismatch, + /// The document is empty. + EmptyDocument, + /// The document exceeded the fixed raw-byte bound. + DocumentTooLarge, + /// The document is not UTF-8. + InvalidUtf8, + /// The service response could not be encoded under its fixed bound. + EncodingFailed, + /// The Guest's host-declared configuration working copy is unavailable. + Unavailable, + /// A staging record was requested but does not exist. + StagingMissing, + /// An already approved record was retried for a different destination. + ApprovalConflict, + /// The local comparison view identifier is not a content commitment. + InvalidView, + /// The host destination identifier is not a bounded opaque value. + InvalidDestination, +} + +impl ConfigError { + /// Return a stable machine-readable error code. + pub const fn code(self) -> &'static str { + match self { + Self::InvalidRequest => "config-request-invalid", + Self::Unauthorized => "config-unauthorized", + Self::SessionMismatch => "config-session-stale", + Self::EmptyDocument => "config-document-empty", + Self::DocumentTooLarge => "config-document-too-large", + Self::InvalidUtf8 => "config-document-invalid-utf8", + Self::EncodingFailed => "config-document-encoding-failed", + Self::Unavailable => "config-document-unavailable", + Self::StagingMissing => "config-stage-missing", + Self::ApprovalConflict => "config-approval-conflict", + Self::InvalidView => "config-view-invalid", + Self::InvalidDestination => "config-destination-invalid", + } + } +} + +impl fmt::Display for ConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.code()) + } +} + +impl std::error::Error for ConfigError {} + +/// Service-only config Provider policy. +#[derive(Debug, Clone, Copy, Default)] +pub struct ConfigService; + +impl ConfigService { + /// Return the closed service descriptor. + pub fn descriptor() -> ConfigServiceDescriptor { + ConfigServiceDescriptor::canonical() + } + + /// Read one Guest document through current authenticated session evidence. + pub fn read_guest_config( + &self, + caller: ConfigCaller, + request: &ConfigSyncRequest, + evidence: &GuestSessionEvidence, + bytes: impl Into>, + ) -> Result { + if !caller.can_read() + || caller != ConfigCaller::Guest + || request.guest_ref != *evidence.guest_ref() + || request.identifier != GUEST_CONFIG_IDENTIFIER + || !evidence.authenticated() + { + return Err(if !caller.can_read() { + ConfigError::Unauthorized + } else { + ConfigError::SessionMismatch + }); + } + let document = GuestConfigDocument::new(bytes)?; + Ok(ConfigSyncResponse::from_document( + request.guest_ref.clone(), + document, + )) + } + + /// Validate a host-side stage request without accepting a path or + /// arbitrary document identifier. + pub fn stage( + &self, + caller: ConfigCaller, + request: &ConfigSyncRequest, + document: GuestConfigDocument, + ) -> Result { + if !caller.can_stage() { + return Err(ConfigError::Unauthorized); + } + validate_guest_ref(&request.guest_ref)?; + validate_identifier(&request.identifier)?; + Ok(ConfigSyncResponse::from_document( + request.guest_ref.clone(), + document, + )) + } + + /// Authorize an admin-only staging mutation. + pub fn authorize_admin( + &self, + caller: ConfigCaller, + guest_ref: &ResourceRef, + ) -> Result<(), ConfigError> { + if !caller.can_stage() || guest_ref.resource_type().as_str() != "Guest" { + return Err(if caller.can_stage() { + ConfigError::InvalidRequest + } else { + ConfigError::Unauthorized + }); + } + Ok(()) + } + + /// Validate a typed operation payload against the closed service. + pub fn validate_operation( + &self, + operation: ConfigOperation, + payload: &serde_json::Value, + ) -> Result<(), ConfigError> { + match operation { + ConfigOperation::ReadGuestConfig => { + let request = serde_json::from_value::(payload.clone()) + .map_err(|_| ConfigError::InvalidRequest)?; + validate_guest_ref(&request.guest_ref)?; + validate_identifier(&request.identifier) + } + ConfigOperation::Stage => { + let request = serde_json::from_value::(payload.clone()) + .map_err(|_| ConfigError::InvalidRequest)?; + validate_guest_ref(&request.guest_ref)?; + validate_identifier(&request.identifier)?; + request.document().map(|_| ()) + } + ConfigOperation::Diff => { + let request = serde_json::from_value::(payload.clone()) + .map_err(|_| ConfigError::InvalidRequest)?; + validate_guest_ref(&request.guest_ref)?; + validate_identifier(&request.identifier)?; + validate_view_identifier(&request.against) + } + ConfigOperation::Approve => { + let request = serde_json::from_value::(payload.clone()) + .map_err(|_| ConfigError::InvalidRequest)?; + validate_guest_ref(&request.guest_ref)?; + validate_identifier(&request.identifier)?; + validate_destination(&request.destination) + } + ConfigOperation::Reject => { + let request = serde_json::from_value::(payload.clone()) + .map_err(|_| ConfigError::InvalidRequest)?; + validate_guest_ref(&request.guest_ref)?; + validate_identifier(&request.identifier) + } + ConfigOperation::Status => { + let request = serde_json::from_value::(payload.clone()) + .map_err(|_| ConfigError::InvalidRequest)?; + validate_guest_ref(&request.guest_ref)?; + validate_identifier(&request.identifier) + } + } + } + + /// Ensure service identity remains tied to the canonical descriptor. + pub fn service_identity(&self) -> (&'static str, &'static str) { + (SERVICE_PACKAGE, SERVICE_NAME) + } +} diff --git a/packages/d2b-provider-config-nixos/src/lib.rs b/packages/d2b-provider-config-nixos/src/lib.rs new file mode 100644 index 0000000000..2d4a129f7c --- /dev/null +++ b/packages/d2b-provider-config-nixos/src/lib.rs @@ -0,0 +1,35 @@ +//! Typed, service-only configuration exchange for NixOS Guests. +//! +//! This Provider deliberately owns no ResourceType. It exposes the one +//! closed guest-config document through an authenticated ComponentSession and +//! leaves host-side staging to the caller's already-authorized local client. + +#![deny(missing_docs)] +#![forbid(unsafe_code)] + +mod controller; +mod service; +mod ttrpc; + +pub use controller::{ + ConfigCaller, ConfigError, ConfigOperation, ConfigService, GuestConfigDocument, + GuestSessionEvidence, +}; +pub use service::{ + ConfigApproveRequest, ConfigApproveResponse, ConfigDiffRequest, ConfigDiffResponse, + ConfigRejectRequest, ConfigRejectResponse, ConfigServiceDescriptor, ConfigStageRequest, + ConfigStageResponse, ConfigStagingStore, ConfigStatusRequest, ConfigStatusResponse, + ConfigSyncRequest, ConfigSyncResponse, GUEST_CONFIG_IDENTIFIER, MAX_CONFIG_BYTES, +}; +pub use ttrpc::{ + ConfigNixosClient, ConfigServiceBackend, GuestConfigReader, create_ttrpc_services, +}; + +/// Canonical Provider reference. +pub const PROVIDER_REF: &str = "Provider/config-nixos"; +/// Canonical Provider artifact identifier. +pub const ARTIFACT_ID: &str = "config-nixos"; +/// Canonical ComponentSession service package. +pub const SERVICE_PACKAGE: &str = "d2b.config-nixos.v3"; +/// Canonical service member prefix. +pub const SERVICE_NAME: &str = "ConfigNixosService"; diff --git a/packages/d2b-provider-config-nixos/src/service.rs b/packages/d2b-provider-config-nixos/src/service.rs new file mode 100644 index 0000000000..0c8a23e4de --- /dev/null +++ b/packages/d2b-provider-config-nixos/src/service.rs @@ -0,0 +1,515 @@ +//! Closed config-nixos service DTOs. + +use std::collections::BTreeMap; + +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use d2b_contracts_resource::v3::{ResourceRef, ZoneId}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ConfigCaller, ConfigError, ConfigOperation, GuestConfigDocument, SERVICE_PACKAGE}; + +/// The only Guest configuration identifier accepted by this service. +pub const GUEST_CONFIG_IDENTIFIER: &str = "guest-config"; +/// Maximum raw document size. +pub const MAX_CONFIG_BYTES: usize = + d2b_contracts_control::guest_wire::READ_GUEST_FILE_MAX_BYTES as usize; + +/// Typed request for reading or staging the canonical Guest document. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigSyncRequest { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Closed document identifier. + pub identifier: String, +} + +impl ConfigSyncRequest { + /// Construct and validate a closed request. + pub fn new(guest_ref: ResourceRef) -> Result { + if guest_ref.resource_type().as_str() != "Guest" { + return Err(ConfigError::InvalidRequest); + } + Ok(Self { + guest_ref, + identifier: GUEST_CONFIG_IDENTIFIER.to_owned(), + }) + } +} + +/// Typed response containing only the bounded canonical Guest document. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigSyncResponse { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Closed document identifier. + pub identifier: String, + /// Base64-encoded UTF-8 document. + pub content_base64: String, + /// Byte count computed by the Provider. + pub bytes: usize, + /// SHA-256 computed by the Provider. + pub sha256: String, +} + +impl ConfigSyncResponse { + pub(crate) fn from_document(guest_ref: ResourceRef, document: GuestConfigDocument) -> Self { + Self { + guest_ref, + identifier: GUEST_CONFIG_IDENTIFIER.to_owned(), + content_base64: document.content_base64(), + bytes: document.len(), + sha256: document.sha256(), + } + } + + /// Decode the response and reapply all document bounds. + pub fn document(&self) -> Result { + if self.identifier != GUEST_CONFIG_IDENTIFIER + || self.content_base64.len() + > d2b_contracts_control::guest_wire::READ_GUEST_CONFIG_ENCODED_MAX_BYTES + { + return Err(ConfigError::InvalidRequest); + } + let bytes = STANDARD + .decode(&self.content_base64) + .map_err(|_| ConfigError::EncodingFailed)?; + let document = GuestConfigDocument::new(bytes)?; + if document.len() != self.bytes || document.sha256() != self.sha256 { + return Err(ConfigError::EncodingFailed); + } + Ok(document) + } +} + +/// Typed host staging request. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigStageRequest { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Closed document identifier. + pub identifier: String, + /// Base64-encoded document to validate and stage. + pub content_base64: String, +} + +impl ConfigStageRequest { + /// Construct a stage request from one already validated document. + pub fn new( + guest_ref: ResourceRef, + document: &GuestConfigDocument, + ) -> Result { + validate_guest_ref(&guest_ref)?; + Ok(Self { + guest_ref, + identifier: GUEST_CONFIG_IDENTIFIER.to_owned(), + content_base64: document.content_base64(), + }) + } + + /// Decode and validate the staged document. + pub fn document(&self) -> Result { + validate_identifier(&self.identifier)?; + if self.content_base64.len() + > d2b_contracts_control::guest_wire::READ_GUEST_CONFIG_ENCODED_MAX_BYTES + { + return Err(ConfigError::DocumentTooLarge); + } + let bytes = STANDARD + .decode(&self.content_base64) + .map_err(|_| ConfigError::EncodingFailed)?; + GuestConfigDocument::new(bytes) + } +} + +/// Typed staging response. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigStageResponse { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Number of bytes staged. + pub bytes: usize, + /// Digest of staged bytes. + pub sha256: String, +} + +/// Typed request for a local diff operation. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigDiffRequest { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Closed staging document identifier. + pub identifier: String, + /// Stable local view identifier, not a file path. + pub against: String, +} + +impl ConfigDiffRequest { + /// Construct a diff request from a stable content-view digest. + pub fn new(guest_ref: ResourceRef, against: impl Into) -> Result { + validate_guest_ref(&guest_ref)?; + let against = against.into(); + validate_view_identifier(&against)?; + Ok(Self { + guest_ref, + identifier: GUEST_CONFIG_IDENTIFIER.to_owned(), + against, + }) + } +} + +/// Typed diff result. Diff text is deliberately not carried by the service. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigDiffResponse { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Whether the local views differ. + pub differs: bool, +} + +/// Typed request for approval of staged content. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigApproveRequest { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Closed staging document identifier. + pub identifier: String, + /// Stable host configuration target identifier. + pub destination: String, +} + +impl ConfigApproveRequest { + /// Construct an approval request for one opaque host target. + pub fn new( + guest_ref: ResourceRef, + destination: impl Into, + ) -> Result { + validate_guest_ref(&guest_ref)?; + let destination = destination.into(); + validate_destination(&destination)?; + Ok(Self { + guest_ref, + identifier: GUEST_CONFIG_IDENTIFIER.to_owned(), + destination, + }) + } +} + +/// Typed approval result. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigApproveResponse { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Number of bytes approved. + pub bytes: usize, + /// Digest of the exact approved document. + pub sha256: String, +} + +/// Typed request for rejecting staged content. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigRejectRequest { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Closed staging document identifier. + pub identifier: String, +} + +impl ConfigRejectRequest { + /// Construct a rejection request. + pub fn new(guest_ref: ResourceRef) -> Result { + validate_guest_ref(&guest_ref)?; + Ok(Self { + guest_ref, + identifier: GUEST_CONFIG_IDENTIFIER.to_owned(), + }) + } +} + +/// Typed rejection result. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigRejectResponse { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Whether content was removed. + pub removed: bool, +} + +/// Typed request for staging status. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigStatusRequest { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Closed staging document identifier. + pub identifier: String, +} + +impl ConfigStatusRequest { + /// Construct a status request. + pub fn new(guest_ref: ResourceRef) -> Result { + validate_guest_ref(&guest_ref)?; + Ok(Self { + guest_ref, + identifier: GUEST_CONFIG_IDENTIFIER.to_owned(), + }) + } +} + +/// Typed status result. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigStatusResponse { + /// Owning Guest resource. + pub guest_ref: ResourceRef, + /// Whether unapproved content is staged. + pub pending: bool, + /// Size of staged content when present. + pub bytes: Option, + /// Digest of staged content when present. + pub sha256: Option, +} + +/// Closed descriptor for the service-only Provider. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfigServiceDescriptor { + /// Service package. + pub package: String, + /// Exact generated method names. + pub methods: Vec, + /// Whether this Provider owns no ResourceType. + pub service_only: bool, +} + +impl ConfigServiceDescriptor { + /// Return the canonical descriptor. + pub fn canonical() -> Self { + Self { + package: SERVICE_PACKAGE.to_owned(), + methods: ConfigOperation::ALL + .into_iter() + .map(|operation| operation.as_str().to_owned()) + .collect(), + service_only: true, + } + } + + /// Validate an incoming descriptor. + pub fn validate(&self) -> Result<(), ConfigError> { + let expected = Self::canonical(); + if self == &expected { + Ok(()) + } else { + Err(ConfigError::InvalidRequest) + } + } +} + +/// Convert one response into a validated document. +pub fn decode_document(response: &ConfigSyncResponse) -> Result { + response.document() +} + +pub(crate) fn validate_guest_ref(guest_ref: &ResourceRef) -> Result<(), ConfigError> { + if guest_ref.resource_type().as_str() == "Guest" && !guest_ref.name().as_str().is_empty() { + Ok(()) + } else { + Err(ConfigError::InvalidRequest) + } +} + +pub(crate) fn validate_identifier(identifier: &str) -> Result<(), ConfigError> { + if identifier == GUEST_CONFIG_IDENTIFIER { + Ok(()) + } else { + Err(ConfigError::InvalidRequest) + } +} + +pub(crate) fn validate_view_identifier(value: &str) -> Result<(), ConfigError> { + let Some(digest) = value.strip_prefix("sha256:") else { + return Err(ConfigError::InvalidView); + }; + if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(ConfigError::InvalidView); + } + Ok(()) +} + +pub(crate) fn validate_destination(value: &str) -> Result<(), ConfigError> { + if value.is_empty() + || value.len() > 128 + || !value.is_ascii() + || value.contains('/') + || value.contains('\\') + || value.chars().any(char::is_whitespace) + { + return Err(ConfigError::InvalidDestination); + } + Ok(()) +} + +/// In-memory host staging owner for one daemon authority. +/// +/// The store contains only bounded, validated documents indexed by canonical +/// Guest reference. It never accepts paths, guest-provided identifiers, or +/// unvalidated bytes. +#[derive(Debug, Default)] +pub struct ConfigStagingStore { + pending: BTreeMap, + approved: BTreeMap, +} + +#[derive(Debug, Clone)] +struct ApprovedConfig { + destination: String, + bytes: usize, + sha256: String, +} + +impl ConfigStagingStore { + /// Stage or replace one Guest document. + pub fn stage( + &mut self, + caller: ConfigCaller, + zone: &ZoneId, + request: &ConfigStageRequest, + ) -> Result { + authorize_host_operation(caller, &request.guest_ref, &request.identifier)?; + let document = request.document()?; + let guest_key = guest_key(zone, &request.guest_ref); + let response = ConfigStageResponse { + guest_ref: request.guest_ref.clone(), + bytes: document.len(), + sha256: document.sha256(), + }; + self.approved.remove(&guest_key); + self.pending.insert(guest_key, document); + Ok(response) + } + + /// Compare staged content to a stable local-view digest. + pub fn diff( + &self, + caller: ConfigCaller, + zone: &ZoneId, + request: &ConfigDiffRequest, + ) -> Result { + authorize_host_operation(caller, &request.guest_ref, &request.identifier)?; + validate_view_identifier(&request.against)?; + let staged = self + .pending + .get(&guest_key(zone, &request.guest_ref)) + .ok_or(ConfigError::StagingMissing)?; + Ok(ConfigDiffResponse { + guest_ref: request.guest_ref.clone(), + differs: staged.sha256() != request.against, + }) + } + + /// Approve staged content for one opaque host target. + /// + /// Approval is idempotent so a caller can retry after the downstream + /// host publish fails. The staged bytes are consumed into an internal + /// approval receipt, and a matching retry returns the same response. + pub fn approve( + &mut self, + caller: ConfigCaller, + zone: &ZoneId, + request: &ConfigApproveRequest, + ) -> Result { + authorize_host_operation(caller, &request.guest_ref, &request.identifier)?; + validate_destination(&request.destination)?; + let guest_key = guest_key(zone, &request.guest_ref); + if let Some(staged) = self.pending.remove(&guest_key) { + let bytes = staged.len(); + let sha256 = staged.sha256(); + self.approved.insert( + guest_key, + ApprovedConfig { + destination: request.destination.clone(), + bytes, + sha256: sha256.clone(), + }, + ); + return Ok(ConfigApproveResponse { + guest_ref: request.guest_ref.clone(), + bytes, + sha256, + }); + } + let approved = self + .approved + .get(&guest_key) + .ok_or(ConfigError::StagingMissing)?; + if approved.destination != request.destination { + return Err(ConfigError::ApprovalConflict); + } + Ok(ConfigApproveResponse { + guest_ref: request.guest_ref.clone(), + bytes: approved.bytes, + sha256: approved.sha256.clone(), + }) + } + + /// Reject staged content, returning whether anything was removed. + pub fn reject( + &mut self, + caller: ConfigCaller, + zone: &ZoneId, + request: &ConfigRejectRequest, + ) -> Result { + authorize_host_operation(caller, &request.guest_ref, &request.identifier)?; + let guest_key = guest_key(zone, &request.guest_ref); + let removed_pending = self.pending.remove(&guest_key).is_some(); + let removed_approved = self.approved.remove(&guest_key).is_some(); + Ok(ConfigRejectResponse { + guest_ref: request.guest_ref.clone(), + removed: removed_pending || removed_approved, + }) + } + + /// Return bounded staging metadata without returning document bytes. + pub fn status( + &self, + caller: ConfigCaller, + zone: &ZoneId, + request: &ConfigStatusRequest, + ) -> Result { + authorize_host_operation(caller, &request.guest_ref, &request.identifier)?; + let staged = self.pending.get(&guest_key(zone, &request.guest_ref)); + Ok(ConfigStatusResponse { + guest_ref: request.guest_ref.clone(), + pending: staged.is_some(), + bytes: staged.map(GuestConfigDocument::len), + sha256: staged.map(GuestConfigDocument::sha256), + }) + } +} + +fn guest_key(zone: &ZoneId, guest_ref: &ResourceRef) -> String { + format!("{}/{}", zone.as_str(), guest_ref.to_canonical_string()) +} + +fn authorize_host_operation( + caller: ConfigCaller, + guest_ref: &ResourceRef, + identifier: &str, +) -> Result<(), ConfigError> { + if !matches!(caller, ConfigCaller::Admin | ConfigCaller::Lifecycle) { + return Err(ConfigError::Unauthorized); + } + validate_guest_ref(guest_ref)?; + validate_identifier(identifier) +} diff --git a/packages/d2b-provider-config-nixos/src/ttrpc.rs b/packages/d2b-provider-config-nixos/src/ttrpc.rs new file mode 100644 index 0000000000..ee87800637 --- /dev/null +++ b/packages/d2b-provider-config-nixos/src/ttrpc.rs @@ -0,0 +1,303 @@ +//! Typed config-nixos service transport. + +use std::{ + collections::HashMap, + path::{Component, Path, PathBuf}, + sync::Arc, +}; + +use async_trait::async_trait; +use serde::{Serialize, de::DeserializeOwned}; +use serde_json::Value; + +use crate::{ + ConfigCaller, ConfigError, ConfigOperation, ConfigService, ConfigServiceDescriptor, + ConfigSyncRequest, GuestConfigDocument, GuestSessionEvidence, SERVICE_NAME, SERVICE_PACKAGE, +}; +use d2b_contracts_resource::v3::ResourceRef; + +/// Backend for the closed config-nixos service. +/// +/// A backend is bound to one authority and, for Guest reads, one authenticated +/// ComponentSession generation before its service map is registered. +pub trait ConfigServiceBackend: Send + Sync { + /// Dispatch one already decoded operation payload. + fn dispatch( + &self, + operation: ConfigOperation, + payload: Value, + ) -> Result; +} + +/// Guest-side backend for the single host-declared configuration working copy. +pub struct GuestConfigReader { + guest_ref: ResourceRef, + evidence: GuestSessionEvidence, + path: PathBuf, +} + +impl std::fmt::Debug for GuestConfigReader { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("GuestConfigReader") + .field("guest_ref", &self.guest_ref) + .field("path", &"") + .field("evidence", &self.evidence) + .finish() + } +} + +impl GuestConfigReader { + /// Bind the reader to one admitted Guest ComponentSession generation. + pub fn new( + guest_ref: ResourceRef, + boot_identity_digest: impl Into, + reconnect_generation: u64, + path: impl Into, + ) -> Result { + let request = ConfigSyncRequest::new(guest_ref.clone())?; + let path = path.into(); + validate_reader_path(&path)?; + let evidence = + GuestSessionEvidence::new(request.guest_ref.clone(), boot_identity_digest, reconnect_generation)?; + Ok(Self { + guest_ref, + evidence, + path, + }) + } + + /// Return the canonical service-only descriptor. + pub fn descriptor() -> ConfigServiceDescriptor { + ConfigService::descriptor() + } +} + +impl ConfigServiceBackend for GuestConfigReader { + fn dispatch( + &self, + operation: ConfigOperation, + payload: Value, + ) -> Result { + if operation != ConfigOperation::ReadGuestConfig { + return Err(ConfigError::Unauthorized); + } + let request: ConfigSyncRequest = + serde_json::from_value(payload).map_err(|_| ConfigError::InvalidRequest)?; + if request.guest_ref != self.guest_ref { + return Err(ConfigError::SessionMismatch); + } + let document = GuestConfigDocument::new(read_bounded_file(&self.path)?)?; + let response = ConfigService.read_guest_config( + ConfigCaller::Guest, + &request, + &self.evidence, + document.bytes().to_vec(), + )?; + serde_json::to_value(response).map_err(|_| ConfigError::EncodingFailed) + } +} + +/// Build the only ttrpc service exposed by Provider/config-nixos. +pub fn create_ttrpc_services( + backend: Arc, +) -> HashMap { + let mut methods = HashMap::new(); + for operation in ConfigOperation::ALL { + let method = operation + .as_str() + .strip_prefix("ConfigNixosService/") + .expect("canonical config operation prefix") + .to_owned(); + methods.insert( + method, + Box::new(ConfigMethod { + backend: Arc::clone(&backend), + operation, + }) as Box, + ); + } + let mut services = HashMap::new(); + services.insert( + format!("{SERVICE_PACKAGE}.{SERVICE_NAME}"), + ttrpc::r#async::Service { + methods, + streams: HashMap::new(), + }, + ); + services +} + +/// Typed client for the closed config-nixos service. +#[derive(Clone)] +pub struct ConfigNixosClient { + client: ttrpc::r#async::Client, +} + +impl std::fmt::Debug for ConfigNixosClient { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("ConfigNixosClient()") + } +} + +impl ConfigNixosClient { + /// Bind a client to an authenticated ComponentSession ttrpc client. + pub fn new(client: ttrpc::r#async::Client) -> Self { + Self { client } + } + + /// Invoke one closed, typed service method. + pub async fn call( + &self, + context: ttrpc::context::Context, + operation: ConfigOperation, + request: &Request, + ) -> ttrpc::Result + where + Request: Serialize, + Response: DeserializeOwned, + { + let payload = serde_json::to_vec(request) + .map_err(|_| ttrpc::Error::RpcStatus(invalid_status()))?; + let method = operation + .as_str() + .strip_prefix("ConfigNixosService/") + .expect("canonical config operation prefix"); + let response = self + .client + .request(ttrpc::Request { + service: format!("{SERVICE_PACKAGE}.{SERVICE_NAME}"), + method: method.to_owned(), + timeout_nano: context.timeout_nano, + metadata: ttrpc::context::to_pb(context.metadata), + payload, + ..Default::default() + }) + .await?; + serde_json::from_slice(&response.payload) + .map_err(|_| ttrpc::Error::RpcStatus(invalid_status())) + } +} + +struct ConfigMethod { + backend: Arc, + operation: ConfigOperation, +} + +#[async_trait] +impl ttrpc::r#async::MethodHandler for ConfigMethod { + async fn handler( + &self, + _context: ttrpc::r#async::TtrpcContext, + request: ttrpc::Request, + ) -> ttrpc::Result { + let payload: Value = serde_json::from_slice(&request.payload) + .map_err(|_| rpc_error(ConfigError::InvalidRequest))?; + ConfigService + .validate_operation(self.operation, &payload) + .map_err(rpc_error)?; + let value = self + .backend + .dispatch(self.operation, payload) + .map_err(rpc_error)?; + let mut response = ttrpc::Response::new(); + response.set_status(ttrpc::get_status(ttrpc::Code::OK, "")); + response.payload = + serde_json::to_vec(&value).map_err(|_| rpc_error(ConfigError::EncodingFailed))?; + Ok(response) + } +} + +fn rpc_error(error: ConfigError) -> ttrpc::Error { + let code = match error { + ConfigError::Unauthorized => ttrpc::Code::PERMISSION_DENIED, + ConfigError::SessionMismatch => ttrpc::Code::UNAUTHENTICATED, + ConfigError::Unavailable => ttrpc::Code::UNAVAILABLE, + ConfigError::DocumentTooLarge => ttrpc::Code::RESOURCE_EXHAUSTED, + ConfigError::StagingMissing => ttrpc::Code::NOT_FOUND, + ConfigError::EncodingFailed => ttrpc::Code::INTERNAL, + _ => ttrpc::Code::INVALID_ARGUMENT, + }; + ttrpc::Error::RpcStatus(ttrpc::get_status(code, error.code())) +} + +fn invalid_status() -> ttrpc::Status { + ttrpc::get_status( + ttrpc::Code::INVALID_ARGUMENT, + ConfigError::EncodingFailed.code(), + ) +} + +fn validate_reader_path(path: &Path) -> Result<(), ConfigError> { + if !path.is_absolute() { + return Err(ConfigError::InvalidRequest); + } + for component in path.components() { + if matches!(component, Component::CurDir | Component::ParentDir | Component::Prefix(_)) { + return Err(ConfigError::InvalidRequest); + } + } + Ok(()) +} + +fn read_bounded_file(path: &Path) -> Result, ConfigError> { + use rustix::{ + fs::{FileType, Mode, OFlags, fstat, open, openat}, + io::{Errno, read}, + }; + + fn map_open_error(error: Errno) -> ConfigError { + match error { + Errno::LOOP | Errno::NOTDIR => ConfigError::InvalidRequest, + _ => ConfigError::Unavailable, + } + } + + if !path.is_absolute() { + return Err(ConfigError::InvalidRequest); + } + let mut components = Vec::new(); + for component in path.components() { + match component { + Component::RootDir => {} + Component::Normal(value) => components.push(value), + Component::CurDir | Component::ParentDir | Component::Prefix(_) => { + return Err(ConfigError::InvalidRequest); + } + } + } + let Some((leaf, parents)) = components.split_last() else { + return Err(ConfigError::InvalidRequest); + }; + let directory_flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC; + let file_flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC; + let mut directory = open("/", directory_flags, Mode::empty()).map_err(map_open_error)?; + for parent in parents { + directory = openat(&directory, *parent, directory_flags, Mode::empty()) + .map_err(map_open_error)?; + } + let file = openat(&directory, *leaf, file_flags, Mode::empty()).map_err(map_open_error)?; + let metadata = fstat(&file).map_err(|_| ConfigError::Unavailable)?; + if FileType::from_raw_mode(metadata.st_mode) != FileType::RegularFile + || metadata.st_nlink != 1 + { + return Err(ConfigError::InvalidRequest); + } + let size = usize::try_from(metadata.st_size).unwrap_or(crate::MAX_CONFIG_BYTES + 1); + if size > crate::MAX_CONFIG_BYTES { + return Err(ConfigError::DocumentTooLarge); + } + let mut bytes = Vec::with_capacity(size); + let mut chunk = [0_u8; 64 * 1024]; + loop { + let count = read(&file, &mut chunk).map_err(|_| ConfigError::Unavailable)?; + if count == 0 { + break; + } + if bytes.len() + count > crate::MAX_CONFIG_BYTES { + return Err(ConfigError::DocumentTooLarge); + } + bytes.extend_from_slice(&chunk[..count]); + } + Ok(bytes) +} diff --git a/packages/d2b-provider-config-nixos/tests/config_lifecycle.rs b/packages/d2b-provider-config-nixos/tests/config_lifecycle.rs new file mode 100644 index 0000000000..588ee905ea --- /dev/null +++ b/packages/d2b-provider-config-nixos/tests/config_lifecycle.rs @@ -0,0 +1,271 @@ +use d2b_contracts_resource::v3::{ResourceRef, ZoneId}; +use d2b_provider_config_nixos::{ + ConfigApproveRequest, ConfigCaller, ConfigDiffRequest, ConfigRejectRequest, ConfigService, + ConfigServiceBackend, ConfigStageRequest, ConfigStatusRequest, ConfigStagingStore, + ConfigSyncRequest, GuestConfigDocument, GuestConfigReader, GuestSessionEvidence, +}; + +#[test] +fn guest_read_requires_current_matching_session() { + let guest = ResourceRef::parse("Guest/work").expect("guest ref"); + let request = ConfigSyncRequest::new(guest.clone()).expect("request"); + let evidence = GuestSessionEvidence::new(guest, "boot-commitment", 1).expect("evidence"); + let result = ConfigService.read_guest_config( + ConfigCaller::Guest, + &request, + &evidence, + b"{}", + ).expect("read"); + assert_eq!(result.identifier, "guest-config"); + assert_eq!(result.bytes, 2); + assert!(result.document().is_ok()); +} + +fn zone() -> ZoneId { + ZoneId::parse("work").expect("zone") +} + +#[test] +fn stale_session_and_non_guest_callers_fail_closed() { + let guest = ResourceRef::parse("Guest/work").expect("guest ref"); + let request = ConfigSyncRequest::new(guest.clone()).expect("request"); + let evidence = GuestSessionEvidence::new(guest, "boot-commitment", 1) + .expect("evidence") + .stale(); + assert_eq!( + ConfigService + .read_guest_config(ConfigCaller::Guest, &request, &evidence, b"{}") + .expect_err("stale read") + .code(), + "config-session-stale" + ); + assert_eq!( + ConfigService + .read_guest_config( + ConfigCaller::User, + &request, + &GuestSessionEvidence::new( + ResourceRef::parse("Guest/work").expect("guest ref"), + "boot-commitment", + 1 + ).expect("evidence"), + b"{}" + ) + .expect_err("user read") + .code(), + "config-unauthorized" + ); +} + +#[test] +fn host_staging_lifecycle_is_typed_and_consumes_approved_content() { + let guest = ResourceRef::parse("Guest/work").expect("guest ref"); + let document = GuestConfigDocument::new(b"services.foo.enable = true;\n".to_vec()) + .expect("document"); + let stage = ConfigStageRequest::new(guest.clone(), &document).expect("stage request"); + let mut store = ConfigStagingStore::default(); + + let staged = store + .stage(ConfigCaller::Admin, &zone(), &stage) + .expect("stage content"); + assert_eq!(staged.bytes, document.len()); + assert_eq!(staged.sha256, document.sha256()); + + let status = store + .status( + ConfigCaller::Lifecycle, + &zone(), + &ConfigStatusRequest::new(guest.clone()).expect("status request"), + ) + .expect("status"); + assert!(status.pending); + assert_eq!(status.bytes, Some(document.len())); + + let diff = store + .diff( + ConfigCaller::Admin, + &zone(), + &ConfigDiffRequest::new(guest.clone(), "sha256:0000000000000000000000000000000000000000000000000000000000000000") + .expect("diff request"), + ) + .expect("diff"); + assert!(diff.differs); + + let approved = store + .approve( + ConfigCaller::Admin, + &zone(), + &ConfigApproveRequest::new(guest.clone(), "host-config") + .expect("approve request"), + ) + .expect("approve"); + assert_eq!(approved.bytes, document.len()); + assert_eq!(approved.sha256, document.sha256()); + assert!( + !store + .status( + ConfigCaller::Admin, + &zone(), + &ConfigStatusRequest::new(guest.clone()).expect("status request"), + ) + .expect("status after approve") + .pending + ); +} + +#[test] +fn approval_retry_is_idempotent_after_downstream_publish_failure() { + let guest = ResourceRef::parse("Guest/work").expect("guest ref"); + let document = GuestConfigDocument::new(b"true\n".to_vec()).expect("document"); + let mut store = ConfigStagingStore::default(); + store + .stage( + ConfigCaller::Admin, + &zone(), + &ConfigStageRequest::new(guest.clone(), &document).expect("stage request"), + ) + .expect("stage"); + + let request = + ConfigApproveRequest::new(guest.clone(), "host-config").expect("approve request"); + let first = store + .approve(ConfigCaller::Admin, &zone(), &request) + .expect("first approval"); + // The host publish may fail after this receipt is recorded. Retrying the + // same approval must not require the consumed staging bytes again. + let retry = store + .approve(ConfigCaller::Admin, &zone(), &request) + .expect("retry approval"); + assert_eq!(retry, first); + assert_eq!( + store + .approve( + ConfigCaller::Admin, + &zone(), + &ConfigApproveRequest::new(guest.clone(), "other-target") + .expect("second destination request"), + ) + .expect_err("different destination must not reuse approval") + .code(), + "config-approval-conflict" + ); + assert!( + store + .reject( + ConfigCaller::Admin, + &zone(), + &ConfigRejectRequest::new(guest).expect("reject request"), + ) + .expect("reject approval receipt") + .removed + ); +} + +#[test] +fn staging_isolated_by_zone_for_same_guest_name() { + let guest = ResourceRef::parse("Guest/work").expect("guest ref"); + let work_zone = zone(); + let personal_zone = ZoneId::parse("personal").expect("zone"); + let work_document = GuestConfigDocument::new(b"work = true\n".to_vec()).expect("document"); + let personal_document = + GuestConfigDocument::new(b"personal = true\n".to_vec()).expect("document"); + let mut store = ConfigStagingStore::default(); + + store + .stage( + ConfigCaller::Admin, + &work_zone, + &ConfigStageRequest::new(guest.clone(), &work_document).expect("stage request"), + ) + .expect("work stage"); + store + .stage( + ConfigCaller::Admin, + &personal_zone, + &ConfigStageRequest::new(guest.clone(), &personal_document).expect("stage request"), + ) + .expect("personal stage"); + + assert_eq!( + store + .status( + ConfigCaller::Admin, + &work_zone, + &ConfigStatusRequest::new(guest.clone()).expect("status request"), + ) + .expect("work status") + .sha256, + Some(work_document.sha256()) + ); + assert_eq!( + store + .status( + ConfigCaller::Admin, + &personal_zone, + &ConfigStatusRequest::new(guest).expect("status request"), + ) + .expect("personal status") + .sha256, + Some(personal_document.sha256()) + ); +} + +#[test] +fn staging_rejects_paths_invalid_views_and_unauthorized_callers() { + let guest = ResourceRef::parse("Guest/work").expect("guest ref"); + let document = GuestConfigDocument::new(b"true\n".to_vec()).expect("document"); + let mut store = ConfigStagingStore::default(); + let stage = ConfigStageRequest::new(guest.clone(), &document).expect("stage request"); + assert_eq!( + store + .stage(ConfigCaller::User, &zone(), &stage) + .expect_err("user must be denied") + .code(), + "config-unauthorized" + ); + assert!(ConfigDiffRequest::new(guest.clone(), "/etc/host.nix").is_err()); + assert!(ConfigApproveRequest::new(guest.clone(), "/etc/host.nix").is_err()); + assert_eq!( + store + .reject( + ConfigCaller::Admin, + &zone(), + &ConfigRejectRequest::new(guest).expect("reject request"), + ) + .expect("reject empty store") + .removed, + false + ); +} + +#[cfg(unix)] +#[test] +fn guest_reader_rejects_hardlinked_config_files() { + let root = std::env::current_dir() + .expect("test working directory") + .join(".scratch") + .join(format!("config-reader-{}", std::process::id())); + std::fs::create_dir_all(&root).expect("scratch directory"); + let source = root.join("source.nix"); + let hardlink = root.join("guest-config.nix"); + std::fs::write(&source, b"true\n").expect("source"); + std::fs::hard_link(&source, &hardlink).expect("hardlink"); + + let reader = GuestConfigReader::new( + ResourceRef::parse("Guest/work").expect("guest ref"), + "boot-commitment", + 1, + &hardlink, + ) + .expect("reader"); + let request = ConfigSyncRequest::new(ResourceRef::parse("Guest/work").expect("guest ref")) + .expect("request"); + let error = reader + .dispatch( + d2b_provider_config_nixos::ConfigOperation::ReadGuestConfig, + serde_json::to_value(request).expect("request JSON"), + ) + .expect_err("hardlinked files must fail closed"); + assert_eq!(error.code(), "config-request-invalid"); + std::fs::remove_dir_all(root).expect("cleanup"); +} diff --git a/packages/d2b-provider-config-nixos/tests/redaction.rs b/packages/d2b-provider-config-nixos/tests/redaction.rs new file mode 100644 index 0000000000..e1c7acf7be --- /dev/null +++ b/packages/d2b-provider-config-nixos/tests/redaction.rs @@ -0,0 +1,18 @@ +use d2b_contracts_resource::v3::ResourceRef; +use d2b_provider_config_nixos::{ConfigService, GuestConfigDocument, GuestSessionEvidence}; + +#[test] +fn debug_does_not_include_guest_content_or_boot_identity() { + let document = GuestConfigDocument::new(b"secret = true;".to_vec()).expect("document"); + let document_debug = format!("{document:?}"); + assert!(!document_debug.contains("secret")); + + let evidence = GuestSessionEvidence::new( + ResourceRef::parse("Guest/work").expect("guest ref"), + "private-boot-id", + 1, + ).expect("evidence"); + let evidence_debug = format!("{evidence:?}"); + assert!(!evidence_debug.contains("private-boot-id")); + let _ = ConfigService; +} diff --git a/packages/d2b-provider-config-nixos/tests/service_contract.rs b/packages/d2b-provider-config-nixos/tests/service_contract.rs new file mode 100644 index 0000000000..f88d496bf7 --- /dev/null +++ b/packages/d2b-provider-config-nixos/tests/service_contract.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use d2b_contracts_resource::v3::ResourceRef; +use d2b_provider_config_nixos::{ + ConfigError, ConfigOperation, ConfigService, ConfigServiceBackend, ConfigServiceDescriptor, + ConfigSyncRequest, GuestConfigDocument, create_ttrpc_services, +}; + +struct TestBackend; + +impl ConfigServiceBackend for TestBackend { + fn dispatch( + &self, + _operation: ConfigOperation, + _payload: serde_json::Value, + ) -> Result { + Ok(serde_json::json!({})) + } +} + +#[test] +fn descriptor_is_closed_and_service_only() { + let descriptor = ConfigServiceDescriptor::canonical(); + descriptor.validate().expect("canonical descriptor"); + assert!(descriptor.service_only); + assert_eq!(descriptor.methods.len(), ConfigOperation::ALL.len()); + assert!(descriptor.methods.iter().all(|method| method.starts_with("ConfigNixosService/"))); +} + +#[test] +fn request_and_document_bounds_are_enforced() { + let guest = ResourceRef::parse("Guest/work").expect("guest ref"); + let request = ConfigSyncRequest::new(guest).expect("request"); + let service = ConfigService; + assert!(service.validate_operation( + ConfigOperation::ReadGuestConfig, + &serde_json::to_value(&request).expect("request JSON") + ).is_ok()); + assert!(GuestConfigDocument::new(vec![b'x'; d2b_provider_config_nixos::MAX_CONFIG_BYTES + 1]).is_err()); +} + +#[test] +fn operation_validation_enforces_closed_identifiers_and_semantic_bounds() { + let service = ConfigService; + let wrong_guest = serde_json::json!({ + "guestRef": "Host/work", + "identifier": "guest-config" + }); + assert_eq!( + service + .validate_operation(ConfigOperation::ReadGuestConfig, &wrong_guest) + .expect_err("non-Guest references must fail") + .code(), + "config-request-invalid" + ); + + let wrong_identifier = serde_json::json!({ + "guestRef": "Guest/work", + "identifier": "other-config" + }); + assert_eq!( + service + .validate_operation(ConfigOperation::ReadGuestConfig, &wrong_identifier) + .expect_err("open identifiers must fail") + .code(), + "config-request-invalid" + ); + + let invalid_view = serde_json::json!({ + "guestRef": "Guest/work", + "identifier": "guest-config", + "against": "not-a-digest" + }); + assert_eq!( + service + .validate_operation(ConfigOperation::Diff, &invalid_view) + .expect_err("diff views must be commitments") + .code(), + "config-view-invalid" + ); +} + +#[test] +fn config_service_publishes_only_the_closed_typed_methods() { + let services = create_ttrpc_services(Arc::new(TestBackend)); + let service = services + .get("d2b.config-nixos.v3.ConfigNixosService") + .expect("config service"); + assert_eq!(service.methods.len(), ConfigOperation::ALL.len()); + for operation in ConfigOperation::ALL { + let method = operation + .as_str() + .strip_prefix("ConfigNixosService/") + .expect("method prefix"); + assert!(service.methods.contains_key(method)); + } + assert!(services + .keys() + .all(|name| name == "d2b.config-nixos.v3.ConfigNixosService")); +} diff --git a/packages/d2b-provider-runtime-cloud-hypervisor/src/controller.rs b/packages/d2b-provider-runtime-cloud-hypervisor/src/controller.rs index 0ca1f57c9a..f1c6c741e4 100644 --- a/packages/d2b-provider-runtime-cloud-hypervisor/src/controller.rs +++ b/packages/d2b-provider-runtime-cloud-hypervisor/src/controller.rs @@ -10,7 +10,10 @@ use crate::{ adoption::{AdoptionOutcome, ProcessIdentity, verify_identity}, bootstrap_graph::{BootstrapGraph, DependencyReadiness}, config::{CloudHypervisorConfig, CloudHypervisorGuestSettings}, - health::{GuestControlHealth, GuestControlHealthError, GuestControlProbe}, + health::{ + GuestControlHealth, GuestControlHealthError, GuestSessionEvidence, + GuestSessionEvidenceProbe, + }, }; /// Cloud Hypervisor lifecycle phase. @@ -153,6 +156,7 @@ pub struct CloudHypervisorController { identity: Option, expected_identity: Option, health_failures: u8, + session_evidence: Option, finalizer: bool, adoption_started_at_unix_ms: Option, startup_started_at_unix_ms: Option, @@ -163,7 +167,7 @@ pub struct CloudHypervisorController { impl CloudHypervisorController where E: CloudHypervisorEffectPort + 'static, - P: GuestControlProbe + 'static, + P: GuestSessionEvidenceProbe + 'static, { /// Construct a controller with explicit dependency graph and ports. pub fn new( @@ -189,6 +193,7 @@ where identity: None, expected_identity: None, health_failures: 0, + session_evidence: None, finalizer: true, adoption_started_at_unix_ms: None, startup_started_at_unix_ms: None, @@ -245,6 +250,11 @@ where self.finalizer } + /// Return the latest authenticated Guest session evidence. + pub fn session_evidence(&self) -> Option<&GuestSessionEvidence> { + self.session_evidence.as_ref() + } + fn apply_health( &mut self, health: GuestControlHealth, @@ -434,24 +444,30 @@ where } else { Duration::from_millis(u64::from(self.config.health_check_timeout_ms)) }; - let health = match timeout( + let evidence = match timeout( probe_timeout, self.probe - .probe(expected_cid, self.config.health_check_timeout_ms), + .observe(expected_cid, self.config.health_check_timeout_ms), ) .await { - Ok(result) => result.map_err(CloudHypervisorError::GuestControl)?, + Ok(result) => result.map_err(|error| { + self.session_evidence = Some(GuestSessionEvidence::failed()); + CloudHypervisorError::GuestControl(error) + })?, Err(_) if self.phase == CloudHypervisorPhase::Bootstrapping => { + self.session_evidence = Some(GuestSessionEvidence::failed()); return Err(self.startup_timeout()); } Err(_) => { + self.session_evidence = Some(GuestSessionEvidence::failed()); return Err(CloudHypervisorError::GuestControl( GuestControlHealthError::Timeout, )); } }; - self.apply_health(health) + self.session_evidence = Some(evidence.clone()); + self.apply_health(evidence.health()) } /// Adopt a process after the caller has rehydrated the expected identity @@ -493,17 +509,21 @@ where self.identity = Some(candidate); self.guest_control_cid = Some(expected_cid); self.phase = CloudHypervisorPhase::VmmReady; - let health = match timeout( + let evidence = match timeout( self.startup_budget()?, self.probe - .probe(expected_cid, self.config.health_check_timeout_ms), + .observe(expected_cid, self.config.health_check_timeout_ms), ) .await { - Ok(result) => result.map_err(CloudHypervisorError::GuestControl)?, + Ok(result) => result.map_err(|error| { + self.session_evidence = Some(GuestSessionEvidence::failed()); + CloudHypervisorError::GuestControl(error) + })?, Err(_) => return Err(self.startup_timeout()), }; - self.apply_health(health) + self.session_evidence = Some(evidence.clone()); + self.apply_health(evidence.health()) } /// Stop guest-control first, then the VMM process. diff --git a/packages/d2b-provider-runtime-cloud-hypervisor/src/health.rs b/packages/d2b-provider-runtime-cloud-hypervisor/src/health.rs index 439df2f146..a342dfe8e6 100644 --- a/packages/d2b-provider-runtime-cloud-hypervisor/src/health.rs +++ b/packages/d2b-provider-runtime-cloud-hypervisor/src/health.rs @@ -1,6 +1,9 @@ //! Authenticated guest-control health contract. +use std::fmt; + use async_trait::async_trait; +use d2b_contracts_resource::v3::ResourceRef; /// Health result for the authenticated guest-control session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -13,6 +16,166 @@ pub enum GuestControlHealth { Failed, } +/// Redacted evidence produced by an authenticated Guest ComponentSession. +/// +/// The optional identity fields are absent only for the temporary +/// compatibility adapter below. New probes must construct evidence with +/// [`GuestSessionEvidence::current`], which binds the Guest, boot identity, +/// reconnect generation, capabilities, and controller readiness together. +#[derive(Clone, PartialEq, Eq)] +pub struct GuestSessionEvidence { + guest_ref: Option, + boot_identity_digest: Option, + reconnect_generation: Option, + capabilities: Vec, + controller_ready: bool, + endpoint_ready: bool, + health: GuestControlHealth, +} + +impl GuestSessionEvidence { + /// Construct current evidence from an authenticated ComponentSession. + pub fn current( + guest_ref: ResourceRef, + boot_identity_digest: impl Into, + reconnect_generation: u64, + capabilities: impl IntoIterator, + controller_ready: bool, + endpoint_ready: bool, + ) -> Result { + let boot_identity_digest = boot_identity_digest.into(); + if guest_ref.resource_type().as_str() != "Guest" + || guest_ref.name().as_str().is_empty() + || reconnect_generation == 0 + || !valid_digest(&boot_identity_digest) + { + return Err(GuestControlHealthError::AuthenticationFailed); + } + let capabilities = validate_capabilities(capabilities)?; + let health = if controller_ready && endpoint_ready { + GuestControlHealth::Ready + } else { + GuestControlHealth::Degraded + }; + Ok(Self { + guest_ref: Some(guest_ref), + boot_identity_digest: Some(boot_identity_digest), + reconnect_generation: Some(reconnect_generation), + capabilities, + controller_ready, + endpoint_ready, + health, + }) + } + + /// Construct a stale evidence snapshot after a disconnected session. + pub fn stale( + guest_ref: ResourceRef, + reconnect_generation: u64, + ) -> Result { + let mut evidence = Self::current( + guest_ref, + "sha256:".to_owned() + &"0".repeat(64), + reconnect_generation, + [], + false, + false, + )?; + evidence.health = GuestControlHealth::Degraded; + Ok(evidence) + } + + /// Return the current health projection. + pub const fn health(&self) -> GuestControlHealth { + self.health + } + + /// Return the bound Guest identity, when available. + pub fn guest_ref(&self) -> Option<&ResourceRef> { + self.guest_ref.as_ref() + } + + /// Return the redacted boot-identity commitment, when available. + pub fn boot_identity_digest(&self) -> Option<&str> { + self.boot_identity_digest.as_deref() + } + + /// Return the authenticated reconnect generation, when available. + pub const fn reconnect_generation(&self) -> Option { + self.reconnect_generation + } + + /// Return the bounded capability names. + pub fn capabilities(&self) -> &[String] { + &self.capabilities + } + + /// Return whether the Guest controller reported readiness. + pub const fn controller_ready(&self) -> bool { + self.controller_ready + } + + /// Return whether the authenticated Endpoint reported readiness. + pub const fn endpoint_ready(&self) -> bool { + self.endpoint_ready + } + + pub(crate) fn compatibility(health: GuestControlHealth) -> Self { + Self { + guest_ref: None, + boot_identity_digest: None, + reconnect_generation: None, + capabilities: Vec::new(), + controller_ready: matches!(health, GuestControlHealth::Ready), + endpoint_ready: matches!(health, GuestControlHealth::Ready), + health, + } + } + + pub(crate) fn failed() -> Self { + Self::compatibility(GuestControlHealth::Failed) + } +} + +impl fmt::Debug for GuestSessionEvidence { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("GuestSessionEvidence") + .field("guest_ref", &"") + .field("boot_identity_digest", &"") + .field("reconnect_generation", &self.reconnect_generation) + .field("capabilities", &self.capabilities.len()) + .field("controller_ready", &self.controller_ready) + .field("endpoint_ready", &self.endpoint_ready) + .field("health", &self.health) + .finish() + } +} + +fn valid_digest(value: &str) -> bool { + let Some(hex) = value.strip_prefix("sha256:") else { + return false; + }; + hex.len() == 64 && hex.bytes().all(|byte| byte.is_ascii_hexdigit()) +} + +fn validate_capabilities( + capabilities: impl IntoIterator, +) -> Result, GuestControlHealthError> { + let capabilities = capabilities.into_iter().collect::>(); + if capabilities.len() > 64 + || capabilities.iter().any(|capability| { + capability.is_empty() + || capability.len() > 128 + || !capability.is_ascii() + || capability.chars().any(char::is_whitespace) + }) + { + return Err(GuestControlHealthError::Protocol); + } + Ok(capabilities) +} + /// Stable guest-control health failures. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GuestControlHealthError { @@ -54,3 +217,38 @@ pub trait GuestControlProbe: Send + Sync { /// Close the authenticated guest-control session before VMM teardown. async fn close(&self, expected_cid: u32) -> Result<(), GuestControlHealthError>; } + +/// Authenticated Guest ComponentSession evidence probe. +#[async_trait] +pub trait GuestSessionEvidenceProbe: Send + Sync { + /// Observe the current authenticated Guest session and its capabilities. + async fn observe( + &self, + expected_cid: u32, + deadline_ms: u32, + ) -> Result; + + /// Close the authenticated Guest session before VMM teardown. + async fn close(&self, expected_cid: u32) -> Result<(), GuestControlHealthError>; +} + +/// Keep the retired probe seam as a narrow compatibility adapter while +/// callers migrate to [`GuestSessionEvidenceProbe`]. +#[async_trait] +impl GuestSessionEvidenceProbe for T +where + T: GuestControlProbe + ?Sized, +{ + async fn observe( + &self, + expected_cid: u32, + deadline_ms: u32, + ) -> Result { + let health = self.probe(expected_cid, deadline_ms).await?; + Ok(GuestSessionEvidence::compatibility(health)) + } + + async fn close(&self, expected_cid: u32) -> Result<(), GuestControlHealthError> { + GuestControlProbe::close(self, expected_cid).await + } +} diff --git a/packages/d2b-provider-runtime-cloud-hypervisor/src/lib.rs b/packages/d2b-provider-runtime-cloud-hypervisor/src/lib.rs index 7bc07bd80e..e7e18592a5 100644 --- a/packages/d2b-provider-runtime-cloud-hypervisor/src/lib.rs +++ b/packages/d2b-provider-runtime-cloud-hypervisor/src/lib.rs @@ -19,7 +19,10 @@ pub use controller::{ CloudHypervisorError, CloudHypervisorPhase, CloudHypervisorReconcileOutcome, CloudHypervisorRecoveryState, SystemCloudHypervisorClock, }; -pub use health::{GuestControlHealth, GuestControlHealthError, GuestControlProbe}; +pub use health::{ + GuestControlHealth, GuestControlHealthError, GuestControlProbe, GuestSessionEvidence, + GuestSessionEvidenceProbe, +}; pub use vmm_argv::{ ChArgvError, ChArgvInput, ChNetIface, ChVsock, exec_arg0, generate_ch_argv, }; diff --git a/packages/d2b-provider-supervisor/src/broker.rs b/packages/d2b-provider-supervisor/src/broker.rs index e89ae1abe4..ef47b83d5d 100644 --- a/packages/d2b-provider-supervisor/src/broker.rs +++ b/packages/d2b-provider-supervisor/src/broker.rs @@ -12,11 +12,14 @@ use d2b_contracts::types::{BundleOpId, RoleId, VmId}; use d2b_contracts_broker::broker_wire::{ AuditJoinContext, BrokerCallerRole, BrokerProfile, BrokerRequest, BrokerRequestEnvelope, BrokerResponse, CanonicalAuditDigest, DeregisterRunnerPidfdRequest, ObserveRunnerRequest, - OpenPidfdRequest, RunnerRole, RunnerSignal, SandboxLaunchPlan, SignalRunnerRequest, - SpawnRunnerRequest, + GuestExecutionBinding as BrokerGuestExecutionBinding, OpenPidfdRequest, RunnerRole, + RunnerSignal, SandboxLaunchPlan, SignalRunnerRequest, SpawnRunnerRequest, }; use d2b_contracts_resource::v3::ResourceRef; -use d2b_contracts_resource::v3::execution_policy::ExecutionDomain; +use d2b_contracts_resource::v3::{ + ActivationRunnerInput, + execution_policy::ExecutionDomain, +}; use d2b_core::bundle_resolver::{BundleResolver, intent_id_runner}; use d2b_core::processes::ProcessRole; use d2b_process::{ @@ -64,6 +67,10 @@ pub struct BrokerLaunchIntent { pub bundle_content_identity: String, /// Complete semantic sandbox plan for generic Process launches. pub sandbox_plan: Option, + /// Typed stdin input for the activation-nixos runner, when applicable. + pub activation_input: Option, + /// Exact authenticated binding for a Guest-local Process. + pub guest_execution: Option, } impl std::fmt::Debug for BrokerLaunchIntent { @@ -102,6 +109,11 @@ impl BrokerObservedProcess { || self.intent.provider_identity == [0; 32] || self.intent.template_identity == [0; 32] || self.intent.generation == 0 + || self + .intent + .guest_execution + .as_ref() + .is_some_and(|binding| !binding.is_valid()) { return Err(ProcessEffectError::IdentityChanged); } @@ -119,6 +131,14 @@ impl BrokerObservedProcess { digest.update(self.intent.provider_identity); digest.update(self.intent.template_identity); digest.update(self.intent.generation.to_le_bytes()); + if let Some(binding) = &self.intent.guest_execution { + digest.update(binding.target_uid.as_str().as_bytes()); + digest.update(binding.boot_identity_digest); + digest.update(binding.session_generation.to_le_bytes()); + digest.update(binding.assignment_epoch.to_le_bytes()); + digest.update(binding.provider_generation.to_le_bytes()); + digest.update(binding.controller_generation.to_le_bytes()); + } digest.update(self.pid.to_le_bytes()); digest.update(self.start_time_ticks.to_le_bytes()); ProcessIdentityDigest::from_bytes(digest.finalize().into()) @@ -254,6 +274,7 @@ impl BrokerLaunchResolver for BundleBackedLaunchResolver { bundle_runner_intent_ref: intent.bundle_runner_intent_ref.clone(), resource_ref: Some(intent.resource_ref.clone()), resource_uid: Some(intent.resource_uid.clone()), + guest_execution: intent.guest_execution.clone(), tracing_span_id: None, }), observation.caller_role.clone(), @@ -344,6 +365,26 @@ impl BundleBackedLaunchResolver { if ticket.user_ref() != expected_user_ref.as_ref() { return Err(ProcessEffectError::IdentityChanged); } + if ticket.execution_ref().resource_type().as_str() == "Guest" + && ticket.guest_execution_binding().is_none() + { + return Err(ProcessEffectError::IdentityChanged); + } + if ticket.execution_ref().resource_type().as_str() == "Host" + && ticket.guest_execution_binding().is_some() + { + return Err(ProcessEffectError::IdentityChanged); + } + let guest_execution = ticket.guest_execution_binding().map(|binding| { + BrokerGuestExecutionBinding { + target_uid: binding.target_uid().clone(), + boot_identity_digest: binding.boot_identity_digest().as_bytes(), + session_generation: binding.session_generation().get(), + assignment_epoch: binding.assignment_epoch(), + provider_generation: binding.provider_generation().get(), + controller_generation: binding.controller_generation().get(), + } + }); Ok(BrokerLaunchIntent { vm_id: VmId::new(vm_name), execution_ref: ticket.execution_ref().clone(), @@ -369,6 +410,8 @@ impl BundleBackedLaunchResolver { .bundle_hash .clone() .ok_or(ProcessEffectError::IdentityChanged)?, + activation_input: ticket.activation_input().cloned(), + guest_execution, sandbox_plan: ticket.sandbox_plan().map(|plan| { let spec = plan.spec(); SandboxLaunchPlan { @@ -405,6 +448,7 @@ pub fn runner_role_for_process_role(role: &ProcessRole) -> Option { ProcessRole::Usbip => Some(RunnerRole::Usbip), ProcessRole::OtelHostBridge => Some(RunnerRole::OtelHostBridge), ProcessRole::WaylandProxy => Some(RunnerRole::WaylandProxy), + ProcessRole::ActivationNixosRunner => Some(RunnerRole::ActivationNixos), ProcessRole::HostReconcile | ProcessRole::StoreVirtiofsPreflight | ProcessRole::GuestSshReadiness @@ -565,7 +609,9 @@ impl ProcessEffectBackend for BrokerProcessBackend { provider_identity: Some(intent.provider_identity), template_identity: Some(intent.template_identity), generation: Some(intent.generation), + guest_execution: intent.guest_execution.clone(), sandbox_plan: intent.sandbox_plan.clone(), + activation_input: intent.activation_input.clone(), role: intent.role, bundle_runner_intent_ref: intent.bundle_runner_intent_ref.clone(), runtime_allocations: Vec::new(), @@ -586,6 +632,7 @@ impl ProcessEffectBackend for BrokerProcessBackend { || response.provider_identity != Some(intent.provider_identity) || response.template_identity != Some(intent.template_identity) || response.generation != Some(intent.generation) + || response.guest_execution != intent.guest_execution || response.bundle_content_identity.as_deref() != Some(intent.bundle_content_identity.as_str()) { @@ -645,6 +692,7 @@ impl ProcessEffectBackend for BrokerProcessBackend { role_id: observed.intent.role_id.clone(), resource_ref: Some(observed.intent.resource_ref.clone()), resource_uid: Some(observed.intent.resource_uid.clone()), + guest_execution: observed.intent.guest_execution.clone(), pid: observed.pid, expected_start_time_ticks: observed.start_time_ticks, tracing_span_id: None, @@ -683,6 +731,7 @@ impl ProcessEffectBackend for BrokerProcessBackend { role_id: handle.observed.intent.role_id.clone(), resource_ref: Some(handle.observed.intent.resource_ref.clone()), resource_uid: Some(handle.observed.intent.resource_uid.clone()), + guest_execution: handle.observed.intent.guest_execution.clone(), signal, pid: Some(handle.observed.pid), expected_start_time_ticks: Some(handle.observed.start_time_ticks), @@ -708,6 +757,7 @@ impl ProcessEffectBackend for BrokerProcessBackend { expected_start_time_ticks: Some(handle.observed.start_time_ticks), resource_ref: Some(handle.observed.intent.resource_ref.clone()), resource_uid: Some(handle.observed.intent.resource_uid.clone()), + guest_execution: handle.observed.intent.guest_execution.clone(), tracing_span_id: None, }, ))?; @@ -731,6 +781,7 @@ impl ProcessEffectBackend for BrokerProcessBackend { expected_start_time_ticks: Some(handle.observed.start_time_ticks), resource_ref: Some(handle.observed.intent.resource_ref.clone()), resource_uid: Some(handle.observed.intent.resource_uid.clone()), + guest_execution: handle.observed.intent.guest_execution.clone(), tracing_span_id: None, }, ))?; @@ -851,6 +902,8 @@ mod tests { .unwrap(), bundle_content_identity: "bundle".to_owned(), sandbox_plan: None, + activation_input: None, + guest_execution: None, }, pid: i32::from(seed) + 1, start_time_ticks: u64::from(seed) + 1, diff --git a/packages/d2b-provider-supervisor/src/systemd.rs b/packages/d2b-provider-supervisor/src/systemd.rs index 631140eb31..1790cc6fa7 100644 --- a/packages/d2b-provider-supervisor/src/systemd.rs +++ b/packages/d2b-provider-supervisor/src/systemd.rs @@ -7,8 +7,8 @@ use std::sync::Mutex; use d2b_contracts_broker::broker_wire::{ BrokerCallerRole, BrokerProfile, BrokerRequest, BrokerResponse, OpenSystemdUnitPidfdRequest, - StopSystemdUnitRequest, SystemdStopClass, SystemdUnitDomain, SystemdUnitIdentity, - SystemdUnitRequest, + GuestExecutionBinding, StopSystemdUnitRequest, SystemdStopClass, SystemdUnitDomain, + SystemdUnitIdentity, SystemdUnitRequest, }; use d2b_contracts_resource::v3::execution_policy::ExecutionDomain; use d2b_process::{ @@ -40,6 +40,7 @@ pub struct SystemdInvocationIdentity { template_identity: [u8; 32], generation: u64, bundle_content_identity: String, + guest_execution: Option, } /// Immutable bundle binding carried by a systemd runtime identity. @@ -94,6 +95,7 @@ impl SystemdInvocationIdentity { template_identity, generation: context.generation, bundle_content_identity: context.bundle_content_identity, + guest_execution: None, }) } @@ -108,6 +110,14 @@ impl SystemdInvocationIdentity { digest.update(self.template_identity); digest.update(self.generation.to_le_bytes()); digest.update(self.bundle_content_identity.as_bytes()); + if let Some(binding) = &self.guest_execution { + digest.update(binding.target_uid.as_str().as_bytes()); + digest.update(binding.boot_identity_digest); + digest.update(binding.session_generation.to_le_bytes()); + digest.update(binding.assignment_epoch.to_le_bytes()); + digest.update(binding.provider_generation.to_le_bytes()); + digest.update(binding.controller_generation.to_le_bytes()); + } ProcessIdentityDigest::from_bytes(digest.finalize().into()) } @@ -136,11 +146,12 @@ impl SystemdInvocationIdentity { template_identity: self.template_identity, generation: self.generation, bundle_content_identity: self.bundle_content_identity.clone(), + guest_execution: self.guest_execution.clone(), } } pub(crate) fn from_wire(identity: &SystemdUnitIdentity) -> Result { - Self::new( + let mut value = Self::new( identity.invocation_id, identity.cgroup_identity, NonZeroU32::new(identity.main_pid).ok_or(ProcessEffectError::IdentityChanged)?, @@ -151,7 +162,9 @@ impl SystemdInvocationIdentity { identity.generation, identity.bundle_content_identity.clone(), )?, - ) + )?; + value.guest_execution = identity.guest_execution.clone(); + Ok(value) } } @@ -549,6 +562,7 @@ impl BrokerSystemdEffectOwner { template_identity: intent.template_identity, generation: intent.generation, domain, + guest_execution: intent.guest_execution.clone(), sandbox_plan: intent.sandbox_plan.clone(), tracing_span_id: None, }; @@ -599,6 +613,7 @@ impl BrokerSystemdEffectOwner { || wire.template_identity != intent.template_identity || wire.generation != intent.generation || wire.bundle_content_identity != intent.bundle_content_identity + || wire.guest_execution != intent.guest_execution || wire.main_pid == 0 { return Err(ProcessEffectError::IdentityChanged); diff --git a/packages/d2b-provider-supervisor/tests/production_adapter.rs b/packages/d2b-provider-supervisor/tests/production_adapter.rs index 46056fec6e..1af72bcfe6 100644 --- a/packages/d2b-provider-supervisor/tests/production_adapter.rs +++ b/packages/d2b-provider-supervisor/tests/production_adapter.rs @@ -536,12 +536,13 @@ fn broker_backend_uses_the_production_spawn_wire_and_pidfd_handoff() { pidfd_index: 0, console_fd_index: None, execution_ref: Some( - d2b_contracts_resource::v3::ResourceRef::parse("Guest/corp-vm").unwrap(), + d2b_contracts_resource::v3::ResourceRef::parse("Host/host-system").unwrap(), ), execution_domain: Some( d2b_contracts_resource::v3::execution_policy::ExecutionDomain::System, ), user_ref: None, + guest_execution: None, provider_identity: Some([0x11; 32]), template_identity: Some([0x22; 32]), generation: Some(1), @@ -561,7 +562,7 @@ fn broker_backend_uses_the_production_spawn_wire_and_pidfd_handoff() { let intent = BrokerLaunchIntent { vm_id, - execution_ref: d2b_contracts_resource::v3::ResourceRef::parse("Guest/corp-vm").unwrap(), + execution_ref: d2b_contracts_resource::v3::ResourceRef::parse("Host/host-system").unwrap(), domain: d2b_contracts_resource::v3::execution_policy::ExecutionDomain::System, user_ref: None, role_id, @@ -570,6 +571,8 @@ fn broker_backend_uses_the_production_spawn_wire_and_pidfd_handoff() { provider_identity: [0x11; 32], template_identity: [0x22; 32], generation: 1, + activation_input: None, + guest_execution: None, resource_ref: d2b_contracts_resource::v3::ResourceRef::parse("Process/corp-vm-worker") .unwrap(), resource_uid: d2b_contracts_resource::v3::ResourceUid::parse( diff --git a/packages/d2b-provider-system-minijail/tests/execution_parents.rs b/packages/d2b-provider-system-minijail/tests/execution_parents.rs index 2ac18d0d82..aa1acf120b 100644 --- a/packages/d2b-provider-system-minijail/tests/execution_parents.rs +++ b/packages/d2b-provider-system-minijail/tests/execution_parents.rs @@ -10,12 +10,14 @@ use d2b_contracts_resource::v3::ResourceRef; use d2b_contracts_resource::v3::execution_policy::ExecutionDomain; use d2b_contracts_resource::v3::identity::ReconnectGeneration; -use d2b_contracts_resource::v3::{ResourceGeneration, ZoneRevision}; +use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceUid, ZoneRevision, +}; use d2b_process_conformance::testing::{PortCall, ScriptedEffectPort, block_on, fixtures}; use d2b_process_conformance::{ - AdoptionCondition, AdoptionOutcome, ConfigurationDigest, IdentityBinding, - ProcessConformanceError, ProcessIdentityDigest, ProcessPhaseClass, ProcessProvider, - ReadinessExpectation, WaitReapOwner, + AdoptionCondition, AdoptionOutcome, ConfigurationDigest, GuestExecutionBinding, + IdentityBinding, ProcessConformanceError, ProcessIdentityDigest, ProcessPhaseClass, + ProcessProvider, ReadinessExpectation, WaitReapOwner, }; use d2b_provider_system_minijail::{MinijailProcessProvider, PROVIDER_NAME}; @@ -34,6 +36,30 @@ fn provider(port: ScriptedEffectPort) -> MinijailProcessProvider GuestExecutionBinding { + GuestExecutionBinding::new( + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ConfigurationDigest::from_bytes([8; 32]), + ReconnectGeneration::new(session).unwrap(), + epoch, + ResourceGeneration::new(provider).unwrap(), + ControllerGeneration::new(1).unwrap(), + ) + .unwrap() +} + +fn bind_guest( + ticket: d2b_process_conformance::LaunchTicket, + execution_ref: &ResourceRef, + binding: GuestExecutionBinding, +) -> d2b_process_conformance::LaunchTicket { + if execution_ref.resource_type().as_str() == "Guest" { + ticket.with_guest_execution_binding(binding).unwrap() + } else { + ticket + } +} + fn launching() -> MinijailProcessProvider { provider(ScriptedEffectPort::launching( required(), @@ -198,11 +224,16 @@ fn readiness_and_identity_seals_are_parent_neutral() { #[test] fn controller_launch_and_assignment_authority_remain_disjoint_on_each_parent() { for execution_ref in [host_ref(), guest_ref()] { - let launch = fixtures::ticket_builder() + let launch = bind_guest( + fixtures::ticket_builder() .selected_provider(PROVIDER_NAME) .execution_ref(execution_ref.clone()) + .without_guest_execution_binding() .build() - .expect("conformant ticket") + .expect("conformant ticket"), + &execution_ref, + guest_binding(2, 4, 1), + ) .with_resource_revision(ZoneRevision::new(1)) .expect("revision binding") .with_controller_launch_binding( @@ -214,11 +245,16 @@ fn controller_launch_and_assignment_authority_remain_disjoint_on_each_parent() { .expect("controller launch binding"); d2b_process_conformance::suite::assert_controller_launch_has_no_resource_client(&launch); - let assignment = fixtures::ticket_builder() + let assignment = bind_guest( + fixtures::ticket_builder() .selected_provider(PROVIDER_NAME) - .execution_ref(execution_ref) + .execution_ref(execution_ref.clone()) + .without_guest_execution_binding() .build() - .expect("conformant ticket") + .expect("conformant ticket"), + &execution_ref, + guest_binding(2, 4, 9), + ) .with_resource_revision(ZoneRevision::new(1)) .expect("revision binding") .with_assignment_binding( diff --git a/packages/d2b-provider-system-systemd/tests/execution_parents.rs b/packages/d2b-provider-system-systemd/tests/execution_parents.rs index 8f0fb9a9d2..7667c8b312 100644 --- a/packages/d2b-provider-system-systemd/tests/execution_parents.rs +++ b/packages/d2b-provider-system-systemd/tests/execution_parents.rs @@ -10,12 +10,14 @@ use d2b_contracts_resource::v3::ResourceRef; use d2b_contracts_resource::v3::execution_policy::ExecutionDomain; use d2b_contracts_resource::v3::identity::ReconnectGeneration; -use d2b_contracts_resource::v3::{ResourceGeneration, ZoneRevision}; +use d2b_contracts_resource::v3::{ + ControllerGeneration, ResourceGeneration, ResourceUid, ZoneRevision, +}; use d2b_process_conformance::testing::{PortCall, ScriptedEffectPort, block_on, fixtures}; use d2b_process_conformance::{ - AdoptionCondition, AdoptionOutcome, ConfigurationDigest, IdentityBinding, - ProcessConformanceError, ProcessIdentityDigest, ProcessPhaseClass, ProcessProvider, - ReadinessExpectation, WaitReapOwner, + AdoptionCondition, AdoptionOutcome, ConfigurationDigest, GuestExecutionBinding, + IdentityBinding, ProcessConformanceError, ProcessIdentityDigest, ProcessPhaseClass, + ProcessProvider, ReadinessExpectation, WaitReapOwner, }; use d2b_provider_system_systemd::{PROVIDER_NAME, SystemdProcessProvider}; @@ -41,6 +43,30 @@ fn launching() -> SystemdProcessProvider { )) } +fn guest_binding(provider: u64, session: u64, epoch: u64) -> GuestExecutionBinding { + GuestExecutionBinding::new( + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").unwrap(), + ConfigurationDigest::from_bytes([8; 32]), + ReconnectGeneration::new(session).unwrap(), + epoch, + ResourceGeneration::new(provider).unwrap(), + ControllerGeneration::new(1).unwrap(), + ) + .unwrap() +} + +fn bind_guest( + ticket: d2b_process_conformance::LaunchTicket, + execution_ref: &ResourceRef, + binding: GuestExecutionBinding, +) -> d2b_process_conformance::LaunchTicket { + if execution_ref.resource_type().as_str() == "Guest" { + ticket.with_guest_execution_binding(binding).unwrap() + } else { + ticket + } +} + fn host_ref() -> ResourceRef { ResourceRef::parse("Host/host-system").expect("valid reference") } @@ -195,11 +221,16 @@ fn readiness_and_identity_seals_are_parent_neutral() { #[test] fn controller_launch_and_assignment_authority_remain_disjoint_on_each_parent() { for execution_ref in [host_ref(), guest_ref()] { - let launch = fixtures::ticket_builder() + let launch = bind_guest( + fixtures::ticket_builder() .selected_provider(PROVIDER_NAME) .execution_ref(execution_ref.clone()) + .without_guest_execution_binding() .build() - .expect("conformant ticket") + .expect("conformant ticket"), + &execution_ref, + guest_binding(2, 4, 1), + ) .with_resource_revision(ZoneRevision::new(1)) .expect("revision binding") .with_controller_launch_binding( @@ -211,11 +242,16 @@ fn controller_launch_and_assignment_authority_remain_disjoint_on_each_parent() { .expect("controller launch binding"); d2b_process_conformance::suite::assert_controller_launch_has_no_resource_client(&launch); - let assignment = fixtures::ticket_builder() + let assignment = bind_guest( + fixtures::ticket_builder() .selected_provider(PROVIDER_NAME) - .execution_ref(execution_ref) + .execution_ref(execution_ref.clone()) + .without_guest_execution_binding() .build() - .expect("conformant ticket") + .expect("conformant ticket"), + &execution_ref, + guest_binding(2, 4, 9), + ) .with_resource_revision(ZoneRevision::new(1)) .expect("revision binding") .with_assignment_binding( diff --git a/packages/d2b-resource-client/src/target.rs b/packages/d2b-resource-client/src/target.rs index fe0f20a9d9..380083e0bd 100644 --- a/packages/d2b-resource-client/src/target.rs +++ b/packages/d2b-resource-client/src/target.rs @@ -37,11 +37,13 @@ pub enum ZoneServiceKind { Support, /// The credential service. Credential, + /// The service-only NixOS Guest configuration Provider. + ConfigNixos, } impl ZoneServiceKind { /// Exhaustive stable variant order. - pub const fn all() -> &'static [Self; 8] { + pub const fn all() -> &'static [Self; 9] { &[ Self::Resource, Self::Zone, @@ -51,6 +53,7 @@ impl ZoneServiceKind { Self::Audit, Self::Support, Self::Credential, + Self::ConfigNixos, ] } @@ -65,6 +68,7 @@ impl ZoneServiceKind { Self::Audit => "d2b.audit.v3", Self::Support => "d2b.support.v3", Self::Credential => "d2b.credential.v3", + Self::ConfigNixos => "d2b.config-nixos.v3", } } @@ -79,6 +83,7 @@ impl ZoneServiceKind { Self::Audit => "audit", Self::Support => "support", Self::Credential => "credential", + Self::ConfigNixos => "config-nixos", } } } @@ -542,7 +547,8 @@ mod tests { "controller", "audit", "support", - "credential" + "credential", + "config-nixos" ] ); services.sort_unstable(); diff --git a/packages/d2b-resource-store-redb/src/actor.rs b/packages/d2b-resource-store-redb/src/actor.rs index 3a1e8e1deb..44394ba254 100644 --- a/packages/d2b-resource-store-redb/src/actor.rs +++ b/packages/d2b-resource-store-redb/src/actor.rs @@ -52,6 +52,8 @@ pub const MAX_CONCURRENT_READS: usize = 16; /// Worker-enforced lifetime ceiling for an admitted read transaction. pub const READ_LIFETIME: Duration = Duration::from_millis(250); +pub(crate) type CommitFence = Arc Result<(), StoreError> + Send + Sync>; + /// Lightweight filtered view over one immutable decoded batch. #[derive(Clone)] pub struct SharedChangeBatch { @@ -421,6 +423,14 @@ impl WriterHandle { pub(crate) async fn commit( &self, opened: OpenedMutation, + ) -> Result { + self.commit_with_fence(opened, None).await + } + + pub(crate) async fn commit_with_fence( + &self, + opened: OpenedMutation, + commit_fence: Option, ) -> Result { if self.quarantined.load(Ordering::Acquire) { return Err(crate::transaction::quarantined()); @@ -485,6 +495,7 @@ impl WriterHandle { principal, resources, mutation: VerifiedWrite::from_opened(opened), + commit_fence, queue_permit, response, }))) { @@ -714,6 +725,7 @@ pub(crate) struct WriteRequest { pub(crate) principal: String, pub(crate) resources: Vec, pub(crate) mutation: VerifiedWrite, + pub(crate) commit_fence: Option, pub(crate) queue_permit: OwnedSemaphorePermit, pub(crate) response: oneshot::Sender>, } @@ -1103,13 +1115,31 @@ impl WriterActor { u64::try_from(requests.len()).unwrap_or(u64::MAX), Ordering::Relaxed, ); + let mut accepted = Vec::with_capacity(requests.len()); + for request in requests { + let sequence = request.sequence; + let fence_result = request + .commit_fence + .as_ref() + .map_or(Ok(()), |fence| fence()); + if let Err(error) = fence_result { + self.clear_audit_intents(&[sequence]); + drop(request.queue_permit); + let _ = request.response.send(Err(error)); + } else { + accepted.push(request); + } + } + if accepted.is_empty() { + continue; + } let started = Instant::now(); - let request_count = requests.len(); - let sequences = requests + let request_count = accepted.len(); + let sequences = accepted .iter() .map(|request| request.sequence) .collect::>(); - let owned = requests + let owned = accepted .into_iter() .map(|request| { drop(request.queue_permit); diff --git a/packages/d2b-resource-store-redb/src/lib.rs b/packages/d2b-resource-store-redb/src/lib.rs index 30619bc4d2..977d5688ce 100644 --- a/packages/d2b-resource-store-redb/src/lib.rs +++ b/packages/d2b-resource-store-redb/src/lib.rs @@ -31,6 +31,7 @@ use d2b_contracts_resource::v3::{ canonical_digest, identity::STANDARD_RESOURCE_TYPES, }; use d2b_resource_store::mutation_seal::{MutationSealAcceptor, SealedMutation}; +use d2b_resource_store::MutationSealBody; use d2b_resource_store::{ PolicySnapshot, StoreCommitResult, StoreError, StoreGetRequest, StoreInspectSchemaRequest, StoreListRequest, StoreListResult, StoreResolveRequest, StoreResolvedIdentity, @@ -1205,6 +1206,38 @@ impl RedbResourceStore { let opened = self.seal.open(sealed)?; self.writer.commit(opened).await } + + /// Commit evidence after an owner has applied additional validation to + /// the opened mutation body. + pub async fn commit_verified_with( + &self, + sealed: SealedMutation, + validate: F, + ) -> Result + where + F: FnOnce(&MutationSealBody) -> Result<(), StoreError>, + { + let opened = self.seal.open(sealed)?; + validate(opened.body())?; + self.writer.commit(opened).await + } + + /// Commit evidence with a final serialized-writer fence. + pub async fn commit_verified_with_fence( + &self, + sealed: SealedMutation, + validate: F, + commit_fence: impl Fn() -> Result<(), StoreError> + Send + Sync + 'static, + ) -> Result + where + F: FnOnce(&MutationSealBody) -> Result<(), StoreError>, + { + let opened = self.seal.open(sealed)?; + validate(opened.body())?; + self.writer + .commit_with_fence(opened, Some(Arc::new(commit_fence))) + .await + } } fn validate_acceptor( diff --git a/packages/d2b-resource-store-redb/src/tests.rs b/packages/d2b-resource-store-redb/src/tests.rs index 4b3dc38e7a..31b98ffafc 100644 --- a/packages/d2b-resource-store-redb/src/tests.rs +++ b/packages/d2b-resource-store-redb/src/tests.rs @@ -15,7 +15,8 @@ use d2b_resource_store::mutation_seal::{ use d2b_resource_store::{ AdmittedAuthorization, AdmittedAuthorizationTarget, AdmittedVerb, ExpectedRevision, PolicySnapshot, PreparedStoreMutation, ResourceMutationKind, StoreGetRequest, StoreListRequest, - StoreMutation, StoreOperationContext, StoreProjection, StoreSlot, StoreWatchRequest, + StoreError, StoreErrorKind, StoreMutation, StoreOperationContext, StoreProjection, StoreSlot, + StoreWatchRequest, }; use redb::{Database, Durability, ReadableDatabase, ReadableTable, ReadableTableMetadata}; use rustix::io::{FdFlags, fcntl_getfd, fcntl_setfd}; @@ -779,6 +780,59 @@ fn resource_mutation_audit_class_is_not_privileged_by_default() { ); } +#[tokio::test] +async fn serialized_commit_fence_rejects_revoked_mutation() { + let (_directory, file, marker) = provisioned_store(); + let store_identity = identity(); + let (issuer, acceptor) = mutation_seal_pair(store_identity.seal_identity()); + let store = RedbResourceStore::provision_owned( + file, + marker, + store_identity, + acceptor, + ) + .await + .unwrap(); + let canonical = create_body("revoked"); + let payload_digest = canonical_digest(RESOURCE_ENVELOPE_DOMAIN_TAG, &canonical); + let error = store + .commit_verified_with_fence( + issuer.seal(create_seal_body( + "revoked-commit", + "revoked", + payload_digest, + )), + |_| Ok(()), + || { + Err(StoreError::new( + StoreErrorKind::ResourcePlaneUnavailable, + None, + None, + d2b_contracts_resource::v3::RetryClass::AfterDelay, + "session-revoked", + )) + }, + ) + .await + .expect_err("revoked mutation must not reach the writer"); + assert_eq!(error.reason_code(), "session-revoked"); + assert_eq!( + store + .get(StoreGetRequest { + operation: operation("revoked-read"), + zone: ZoneId::parse("work").unwrap(), + target: ResourceRef::parse("Host/revoked").unwrap(), + expected_uid: None, + projection: StoreProjection::Full, + }) + .await + .expect_err("revoked resource must not be committed") + .reason_code(), + "resource-not-found" + ); + store.shutdown().await.unwrap(); +} + #[tokio::test] async fn initialized_schema_catalog_is_digest_bound_and_complete() { let (_directory, file, marker) = provisioned_store(); diff --git a/packages/d2b-resource-store-redb/src/transaction.rs b/packages/d2b-resource-store-redb/src/transaction.rs index e1a1dcbe35..d0bd0b54c8 100644 --- a/packages/d2b-resource-store-redb/src/transaction.rs +++ b/packages/d2b-resource-store-redb/src/transaction.rs @@ -797,6 +797,7 @@ pub(crate) fn empty_write_request_for_test( }, mutations: Vec::new(), }, + commit_fence: None, queue_permit, response, } diff --git a/packages/d2b-session-unix/src/vsock.rs b/packages/d2b-session-unix/src/vsock.rs index 6846b086da..77337f50fd 100644 --- a/packages/d2b-session-unix/src/vsock.rs +++ b/packages/d2b-session-unix/src/vsock.rs @@ -13,6 +13,7 @@ use tokio_vsock::{VMADDR_CID_ANY, VMADDR_CID_HOST, VsockAddr, VsockListener, Vso pub struct FramedVsockTransport { stream: S, + descriptor: TransportDescriptor, receive_header: Vec, receive_body: Vec, receive_declared: Option, @@ -22,8 +23,24 @@ pub struct FramedVsockTransport { impl FramedVsockTransport { pub fn new(stream: S) -> Self { + Self::with_descriptor( + stream, + TransportDescriptor { + class: TransportClass::NativeVsock, + locality: Locality::GuestLocal, + packet_atomic: false, + supports_attachments: false, + }, + ) + } + + /// Construct a framed stream with an explicitly bound transport + /// descriptor. This is used by the host-side Cloud Hypervisor CONNECT + /// adapter after the socket path and peer have been validated. + pub fn with_descriptor(stream: S, descriptor: TransportDescriptor) -> Self { Self { stream, + descriptor, receive_header: Vec::with_capacity(4), receive_body: Vec::new(), receive_declared: None, @@ -48,17 +65,13 @@ where S: AsyncRead + AsyncWrite + Unpin + Send + 'static, { fn descriptor(&self) -> TransportDescriptor { - TransportDescriptor { - class: TransportClass::NativeVsock, - locality: Locality::GuestLocal, - packet_atomic: false, - supports_attachments: false, - } + self.descriptor } fn into_split(self: Box) -> (Box, Box) { let Self { stream, + descriptor: _, receive_header, receive_body, receive_declared, diff --git a/packages/d2b-session/src/admission.rs b/packages/d2b-session/src/admission.rs index c53e377b5e..372b4e3ef0 100644 --- a/packages/d2b-session/src/admission.rs +++ b/packages/d2b-session/src/admission.rs @@ -781,6 +781,17 @@ impl AuthenticatedTtrpcHandle { } result } + + /// Serve generated ttrpc services on this already authenticated session. + /// + /// The handle contains only the correlated transport plane; authorization + /// and resource policy remain bound to the generated service adapter. + pub async fn serve_ttrpc_services( + self, + services: std::collections::HashMap, + ) -> std::result::Result<(), crate::SessionServerError> { + crate::serve_ttrpc_services(Arc::new(self.driver), services).await + } } impl fmt::Debug for AuthenticatedTtrpcHandle { @@ -1199,6 +1210,14 @@ impl AuthenticatedComponentSession { } } +impl AuthenticatedComponentSession<()> { + /// Split the transport plane after a session admitted without a + /// registration capability, such as a Guest target's parent session. + pub fn into_ttrpc_handle(self) -> AuthenticatedTtrpcHandle { + self.ttrpc_handle() + } +} + impl fmt::Debug for AuthenticatedComponentSession { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter diff --git a/packages/d2b-session/src/client.rs b/packages/d2b-session/src/client.rs new file mode 100644 index 0000000000..f21972ee33 --- /dev/null +++ b/packages/d2b-session/src/client.rs @@ -0,0 +1,272 @@ +//! Client-side ttrpc bridge for an authenticated ComponentSession. +//! +//! Generated ttrpc clients expect an ordinary byte stream. ComponentSession +//! owns the authenticated transport and request registry, so this adapter +//! translates the two framing layers without exposing the session driver to +//! generated service code. + +use std::sync::Arc; + +use d2b_contracts_zone_session::v3::component_session::MAX_LOGICAL_MESSAGE_BYTES; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + task::JoinHandle, +}; +use ttrpc::{ + proto::{ + MESSAGE_HEADER_LENGTH, MESSAGE_TYPE_DATA, MESSAGE_TYPE_REQUEST, MESSAGE_TYPE_RESPONSE, + MessageHeader, + }, + r#async::{Client, transport::Socket}, +}; + +use crate::{Cancellation, ComponentSessionDriver, SessionError, ttrpc_request_id}; + +const CLIENT_BRIDGE_CAPACITY: usize = 256 * 1024; + +/// A generated-ttrpc client bound to one authenticated ComponentSession. +/// +/// The returned [`Client`] clone is suitable for generated service clients. +/// Keeping this value alive keeps the bridge alive and ensures the session +/// request registry is cleaned up when the client is dropped. +pub struct SessionTtrpcClient { + client: Client, + bridge: Option>, + cancellation: Cancellation, +} + +impl std::fmt::Debug for SessionTtrpcClient { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("SessionTtrpcClient()") + } +} + +impl SessionTtrpcClient { + /// Create a generated-ttrpc client over an established session driver. + pub fn new(driver: Arc) -> Self { + let (client_transport, bridge_transport) = tokio::io::duplex(CLIENT_BRIDGE_CAPACITY); + let client = Client::new(Socket::new(client_transport)); + let cancellation = Cancellation::new(); + let bridge_cancellation = cancellation.clone(); + let bridge = tokio::spawn(async move { + let (reader, writer) = tokio::io::split(bridge_transport); + let outgoing = forward_client_frames( + reader, + Arc::clone(&driver), + bridge_cancellation.clone(), + ); + let incoming = forward_session_frames( + writer, + Arc::clone(&driver), + bridge_cancellation.clone(), + ); + tokio::select! { + _ = outgoing => {} + _ = incoming => {} + } + bridge_cancellation.cancel(); + }); + Self { + client, + bridge: Some(bridge), + cancellation, + } + } + + /// Clone the underlying generated-ttrpc client. + pub fn client(&self) -> Client { + self.client.clone() + } +} + +impl Drop for SessionTtrpcClient { + fn drop(&mut self) { + self.cancellation.cancel(); + if let Some(bridge) = self.bridge.take() { + bridge.abort(); + } + } +} + +async fn forward_client_frames( + mut reader: R, + driver: Arc, + cancellation: Cancellation, +) -> Result<(), SessionClientBridgeError> +where + R: AsyncRead + Unpin, +{ + loop { + let frame = read_frame(&mut reader, &cancellation).await?; + match frame_type(&frame)? { + MESSAGE_TYPE_REQUEST => { + let request_id = ttrpc_request_id(driver.generation(), &frame) + .map_err(|_| SessionClientBridgeError::Frame)?; + driver + .start_ttrpc(request_id, frame) + .await + .map_err(SessionClientBridgeError::Session)?; + } + MESSAGE_TYPE_DATA => { + driver + .send_ttrpc(frame) + .await + .map_err(SessionClientBridgeError::Session)?; + } + _ => return Err(SessionClientBridgeError::Frame), + } + } +} + +async fn forward_session_frames( + mut writer: W, + driver: Arc, + cancellation: Cancellation, +) -> Result<(), SessionClientBridgeError> +where + W: AsyncWrite + Unpin, +{ + loop { + let frame = tokio::select! { + _ = cancellation.cancelled() => return Ok(()), + result = driver.receive_ttrpc() => result.map_err(SessionClientBridgeError::Session)?, + }; + let kind = frame_type(&frame)?; + if !matches!(kind, MESSAGE_TYPE_DATA | MESSAGE_TYPE_RESPONSE) { + return Err(SessionClientBridgeError::Frame); + } + writer + .write_all(&frame) + .await + .map_err(|_| SessionClientBridgeError::Transport)?; + writer + .flush() + .await + .map_err(|_| SessionClientBridgeError::Transport)?; + if kind == MESSAGE_TYPE_RESPONSE { + let request_id = ttrpc_request_id(driver.generation(), &frame) + .map_err(|_| SessionClientBridgeError::Frame)?; + driver + .complete_ttrpc(request_id) + .await + .map_err(SessionClientBridgeError::Session)?; + } + } +} + +async fn read_frame( + reader: &mut R, + cancellation: &Cancellation, +) -> Result, SessionClientBridgeError> +where + R: AsyncRead + Unpin, +{ + let mut header_bytes = [0_u8; MESSAGE_HEADER_LENGTH]; + tokio::select! { + _ = cancellation.cancelled() => return Err(SessionClientBridgeError::Cancelled), + result = reader.read_exact(&mut header_bytes) => { + result.map_err(|_| SessionClientBridgeError::Transport)?; + } + } + let header = MessageHeader::from(header_bytes); + let body_len = usize::try_from(header.length).map_err(|_| SessionClientBridgeError::Frame)?; + if body_len > MAX_LOGICAL_MESSAGE_BYTES as usize { + return Err(SessionClientBridgeError::Frame); + } + let mut frame = Vec::with_capacity( + MESSAGE_HEADER_LENGTH + .checked_add(body_len) + .ok_or(SessionClientBridgeError::Frame)?, + ); + frame.extend_from_slice(&header_bytes); + frame.resize(MESSAGE_HEADER_LENGTH + body_len, 0); + tokio::select! { + _ = cancellation.cancelled() => return Err(SessionClientBridgeError::Cancelled), + result = reader.read_exact(&mut frame[MESSAGE_HEADER_LENGTH..]) => { + result.map_err(|_| SessionClientBridgeError::Transport)?; + } + } + Ok(frame) +} + +fn frame_type(frame: &[u8]) -> Result { + let header_bytes: [u8; MESSAGE_HEADER_LENGTH] = frame + .get(..MESSAGE_HEADER_LENGTH) + .ok_or(SessionClientBridgeError::Frame)? + .try_into() + .map_err(|_| SessionClientBridgeError::Frame)?; + let header = MessageHeader::from(header_bytes); + let body_len = usize::try_from(header.length).map_err(|_| SessionClientBridgeError::Frame)?; + if body_len > MAX_LOGICAL_MESSAGE_BYTES as usize + || frame.len() != MESSAGE_HEADER_LENGTH.saturating_add(body_len) + { + return Err(SessionClientBridgeError::Frame); + } + Ok(header.type_) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SessionClientBridgeError { + Cancelled, + Frame, + Session(SessionError), + Transport, +} + +impl std::fmt::Display for SessionClientBridgeError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Cancelled => "component-session-client-cancelled", + Self::Frame => "component-session-client-frame-invalid", + Self::Session(_) => "component-session-client-session-failed", + Self::Transport => "component-session-client-transport-failed", + }) + } +} + +impl std::error::Error for SessionClientBridgeError {} + +#[cfg(test)] +mod tests { + use super::*; + use ttrpc::proto::MessageHeader; + + #[test] + fn frame_type_rejects_trailing_bytes_and_oversize_bodies() { + let mut frame = Vec::from(MessageHeader::new_response(7, 3)); + frame.extend_from_slice(b"abc"); + assert_eq!(frame_type(&frame), Ok(MESSAGE_TYPE_RESPONSE)); + frame.push(0); + assert_eq!(frame_type(&frame), Err(SessionClientBridgeError::Frame)); + + let header = MessageHeader { + length: (MAX_LOGICAL_MESSAGE_BYTES + 1), + stream_id: 7, + type_: MESSAGE_TYPE_RESPONSE, + flags: 0, + }; + assert_eq!( + frame_type(&Vec::from(header)), + Err(SessionClientBridgeError::Frame) + ); + } + + #[tokio::test] + async fn read_frame_preserves_exact_ttrpc_boundaries() { + let (mut writer, mut reader) = tokio::io::duplex(128); + let first = { + let mut frame = Vec::from(MessageHeader::new_response(1, 3)); + frame.extend_from_slice(b"one"); + frame + }; + let second = { + let mut frame = Vec::from(MessageHeader::new_response(3, 3)); + frame.extend_from_slice(b"two"); + frame + }; + writer.write_all(&[&first[..], &second[..]].concat()).await.unwrap(); + let cancellation = Cancellation::new(); + assert_eq!(read_frame(&mut reader, &cancellation).await.unwrap(), first); + assert_eq!(read_frame(&mut reader, &cancellation).await.unwrap(), second); + } +} diff --git a/packages/d2b-session/src/engine.rs b/packages/d2b-session/src/engine.rs index 0897eed10d..93c13380f8 100644 --- a/packages/d2b-session/src/engine.rs +++ b/packages/d2b-session/src/engine.rs @@ -175,7 +175,7 @@ impl SessionEngine { let timeout = Duration::from_millis(u64::from(identity.limits.handshake_deadline_ms)); let result = match tokio::time::timeout(timeout, async move { identity - .validate_local_generation_discovery() + .validate_generation_discovery() .map_err(SessionError::from)?; Self::establish_initiator_with_generation_discovery_inner( transport, diff --git a/packages/d2b-session/src/handshake.rs b/packages/d2b-session/src/handshake.rs index bcf10f2684..1ff5ab0b92 100644 --- a/packages/d2b-session/src/handshake.rs +++ b/packages/d2b-session/src/handshake.rs @@ -139,7 +139,7 @@ pub fn negotiate_offer( pub fn encode_generation_discovery_request(identity: &EndpointPolicyIdentity) -> Result> { identity - .validate_local_generation_discovery() + .validate_generation_discovery() .map_err(SessionError::from)?; let encoded = identity .encode_canonical() @@ -165,7 +165,7 @@ pub fn accept_generation_discovery_request( let identity = EndpointPolicyIdentity::decode_canonical(&bytes[GENERATION_QUERY_MAGIC.len()..]) .map_err(handshake_binary_error)?; identity - .validate_local_generation_discovery() + .validate_generation_discovery() .map_err(SessionError::from)?; identity .validate_exact(policy) diff --git a/packages/d2b-session/src/lib.rs b/packages/d2b-session/src/lib.rs index 6fbf12b030..6623710715 100644 --- a/packages/d2b-session/src/lib.rs +++ b/packages/d2b-session/src/lib.rs @@ -10,6 +10,7 @@ mod attachment; pub mod audit; mod bootstrap; mod cancellation; +mod client; mod deadline; mod driver; mod engine; @@ -27,6 +28,7 @@ mod transport; pub use bootstrap::{AdmittedBootstrapPsk, BootstrapAdmission, BootstrapPsk, Secret32}; pub use cancellation::{Cancellation, RequestRegistry}; +pub use client::SessionTtrpcClient; pub use deadline::DeadlineBudget; pub use driver::{ComponentSessionDriver, SessionDriverHandle}; pub use engine::{SessionEngine, SessionEvent}; diff --git a/packages/d2b-session/src/operation.rs b/packages/d2b-session/src/operation.rs index 5430af467e..a8c01da911 100644 --- a/packages/d2b-session/src/operation.rs +++ b/packages/d2b-session/src/operation.rs @@ -129,6 +129,15 @@ pub const GENERATED_OPERATION_CATALOG: &[OperationCatalogEntry] = &[ interaction("d2b.notification.v3", "NotificationService/Reconcile"), interaction("d2b.notification.v3", "NotificationService/Deliver"), interaction("d2b.notification.v3", "NotificationService/Drain"), + interaction( + "d2b.config-nixos.v3", + "ConfigNixosService/ReadGuestConfig", + ), + interaction("d2b.config-nixos.v3", "ConfigNixosService/Stage"), + interaction("d2b.config-nixos.v3", "ConfigNixosService/Diff"), + interaction("d2b.config-nixos.v3", "ConfigNixosService/Approve"), + interaction("d2b.config-nixos.v3", "ConfigNixosService/Reject"), + interaction("d2b.config-nixos.v3", "ConfigNixosService/Status"), ]; const fn resource( diff --git a/packages/d2b-session/tests/component_session.rs b/packages/d2b-session/tests/component_session.rs index 68a6a108a5..34538f7229 100644 --- a/packages/d2b-session/tests/component_session.rs +++ b/packages/d2b-session/tests/component_session.rs @@ -124,6 +124,25 @@ fn policy(offer: &HandshakeOffer) -> EndpointPolicy { } } +fn guest_generation_identity() -> EndpointPolicyIdentity { + let mut guest = offer(NoiseProfile::Nn25519ChaChaPolySha256); + guest.purpose = EndpointPurpose::ZoneLink; + guest.purpose_class = PurposeClass::Enrolled; + guest.initiator_role = EndpointRole::ZoneController; + guest.responder_role = EndpointRole::GuestAgent; + guest.noise_profile = NoiseProfile::Kk25519ChaChaPolySha256; + guest.limits = LimitProfile::remote_default(); + guest.transport_binding = TransportBinding { + transport: TransportClass::NativeVsock, + locality: Locality::GuestLocal, + channel_binding: [0x22; 32], + identity_evidence: IdentityEvidenceRequirement::EnrolledStaticKeys, + }; + guest.attachment_policy = AttachmentPolicy::disabled(); + let guest_policy = policy(&guest); + EndpointPolicyIdentity::from(&guest_policy) +} + fn negotiated(offer: &HandshakeOffer) -> d2b_session::NegotiatedOffer { let encoded = offer.encode_canonical().unwrap(); let preface = d2b_session::contract::ComponentSessionPreface::new(encoded.len()) @@ -1345,6 +1364,16 @@ async fn local_generation_discovery_rejects_endpoint_identity_mismatch() { ); } +#[test] +fn enrolled_guest_generation_discovery_is_allowed() { + let identity = guest_generation_identity(); + let request = + encode_generation_discovery_request(&identity).expect("exact enrolled Guest discovery"); + let policy = identity.with_generation(7).expect("discovery policy generation"); + accept_generation_discovery_request(&request, &policy) + .expect("exact enrolled Guest discovery accepted"); +} + #[test] fn discovered_generation_is_still_exactly_checked_by_the_authenticated_offer() { let server_policy = policy(&offer(NoiseProfile::Nn25519ChaChaPolySha256)); diff --git a/packages/d2b/BUILD.bazel b/packages/d2b/BUILD.bazel index b1295f620e..93b0f88a01 100644 --- a/packages/d2b/BUILD.bazel +++ b/packages/d2b/BUILD.bazel @@ -39,6 +39,7 @@ rust_library( "//packages/d2b-contracts-control:d2b_contracts_control", "//packages/d2b-core:d2b_core", "//packages/d2b-cutover:d2b_cutover", + "//packages/d2b-provider-config-nixos:d2b_provider_config_nixos", "//packages/d2b-realm-core:d2b_realm_core", "//packages/d2b-resource-client:d2b_resource_client", "//packages/d2b-zone-routing:d2b_zone_routing", diff --git a/packages/d2b/Cargo.toml b/packages/d2b/Cargo.toml index d5fbf5e695..20f5d02dfa 100644 --- a/packages/d2b/Cargo.toml +++ b/packages/d2b/Cargo.toml @@ -18,6 +18,7 @@ d2b-contracts-broker = { path = "../d2b-contracts-broker", version = "0.0.0-boot d2b-contracts-control = { path = "../d2b-contracts-control", version = "0.0.0-bootstrap" } d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } d2b-cutover = { path = "../d2b-cutover", version = "0.0.0-bootstrap" } +d2b-provider-config-nixos = { path = "../d2b-provider-config-nixos", version = "0.0.0-bootstrap" } d2b-resource-client = { path = "../d2b-resource-client", version = "0.0.0-bootstrap" } d2b-realm-core = { path = "../d2b-realm-core", version = "0.0.0-bootstrap" } d2b-zone-routing = { path = "../d2b-zone-routing", version = "0.0.0-bootstrap" } diff --git a/packages/d2b/src/activation.rs b/packages/d2b/src/activation.rs index bcd7c25feb..31e2c2e127 100644 --- a/packages/d2b/src/activation.rs +++ b/packages/d2b/src/activation.rs @@ -2,11 +2,17 @@ use clap::{Args, Subcommand}; use serde_json::json; +use sha2::{Digest, Sha256}; use crate::{ CliFailure, context::{OutputMode, RequestDeadline, ZoneContext, parse_resource_ref}, }; +use d2b_provider_config_nixos::{ + ConfigApproveRequest, ConfigApproveResponse, ConfigDiffRequest, ConfigDiffResponse, + ConfigRejectRequest, ConfigRejectResponse, ConfigStageRequest, ConfigStageResponse, ConfigStatusRequest, + ConfigStatusResponse, ConfigSyncResponse, GuestConfigDocument, GUEST_CONFIG_IDENTIFIER, +}; #[derive(Debug, Args, Clone)] pub(crate) struct ActivationArgs { @@ -280,60 +286,471 @@ fn config( context: &ZoneContext, args: &ActivationConfigArgs, mode: OutputMode, - _deadline: RequestDeadline, + deadline: RequestDeadline, ) -> Result { - let legacy = crate::LegacyContext::from_env()?; match &args.command { ActivationConfigCommand::Sync(args) => { let guest_ref = parse_guest_ref(context, &args.guest_ref, mode)?; - crate::cmd_config_sync( - &legacy, - &crate::ConfigSyncArgs { - vm: guest_ref.name().as_str().to_owned(), - guest_path: crate::DEFAULT_GUEST_CONFIG_PATH.to_owned(), - host: None, - user: None, - key: None, - known_hosts: None, - dry_run: args.dry_run, - json: mode.is_json(), - }, - ) + let vm = guest_ref.name().as_str(); + crate::legacy::config_validate_vm_name(vm)?; + let staging = crate::legacy::config_staging_path(vm); + if args.dry_run { + return context.emit( + &json!({ + "command": "config sync", + "mode": "dry-run", + "resourceRef": guest_ref.to_canonical_string(), + "identifier": GUEST_CONFIG_IDENTIFIER, + }), + mode, + ).map(|_| 0); + } + let value = context.invoke_service( + d2b_resource_client::ZoneServiceKind::ConfigNixos, + "ConfigNixosService/ReadGuestConfig", + "invoke", + json!({ + "guestRef": guest_ref, + "identifier": GUEST_CONFIG_IDENTIFIER, + }), + deadline, + mode, + )?; + let response: ConfigSyncResponse = + serde_json::from_value(strip_config_response_envelope(value)).map_err(|_| { + context.failure( + "config-document-encoding-failed", + "Zone returned an invalid config-nixos response", + mode, + 1, + ) + })?; + let document = response.document().map_err(|error| { + context.failure(error.code(), "config-nixos returned an invalid document", mode, 1) + })?; + if let Some(parent) = staging.parent() { + std::fs::create_dir_all(parent).map_err(|error| { + context.failure( + "config-stage-failed", + &format!("config sync: create staging dir: {error}"), + mode, + 1, + ) + })?; + } + crate::legacy::config_atomic_write(&staging, document.bytes())?; + invoke_config_stage(context, guest_ref.clone(), &document, deadline, mode)?; + context.emit( + &json!({ + "command": "config sync", + "transport": "component-session", + "resourceRef": guest_ref.to_canonical_string(), + "identifier": GUEST_CONFIG_IDENTIFIER, + "staging": staging.display().to_string(), + "bytes": document.len(), + "sha256": document.sha256(), + }), + mode, + )?; + Ok(0) } ActivationConfigCommand::Diff(args) => { let guest_ref = parse_guest_ref(context, &args.guest_ref, mode)?; - crate::cmd_config_diff(&crate::ConfigDiffArgs { - vm: guest_ref.name().as_str().to_owned(), - against: args.against.clone(), - json: mode.is_json(), - }) + crate::legacy::config_validate_vm_name(guest_ref.name().as_str())?; + let staging = crate::legacy::config_staging_path(guest_ref.name().as_str()); + if !staging.exists() { + return Err(context.failure( + "config-stage-missing", + "nothing staged for this Guest", + mode, + 1, + )); + } + let staged_document = read_staged_document(context, &staging, mode)?; + invoke_config_stage(context, guest_ref.clone(), &staged_document, deadline, mode)?; + let against_digest = config_view_identifier(&args.against, context, mode)?; + let service_diff = invoke_config_diff( + context, + guest_ref.clone(), + against_digest, + deadline, + mode, + )?; + let output = std::process::Command::new("diff") + .arg("-u") + .arg(&args.against) + .arg(&staging) + .output() + .map_err(|error| { + context.failure( + "config-diff-failed", + &format!("config diff: spawn diff: {error}"), + mode, + 1, + ) + })?; + let code = output.status.code().unwrap_or(-1); + if code > 1 { + return Err(context.failure( + "config-diff-failed", + "config diff failed", + mode, + 1, + )); + } + let differs = code == 1; + if service_diff.differs != differs { + return Err(context.failure( + "config-diff-failed", + "config-nixos and local config views disagree", + mode, + 1, + )); + } + if mode.is_json() { + context.emit( + &json!({ + "command": "config diff", + "resourceRef": guest_ref.to_canonical_string(), + "against": args.against.display().to_string(), + "differs": differs, + "diff": String::from_utf8_lossy(&output.stdout), + }), + mode, + )?; + } else if differs { + crate::print_stdout(&String::from_utf8_lossy(&output.stdout)); + } else { + crate::print_stdout(&format!( + "config diff: staged config for '{}' is identical to {}\n", + guest_ref.name().as_str(), + args.against.display() + )); + } + Ok(0) } ActivationConfigCommand::Approve(args) => { let guest_ref = parse_guest_ref(context, &args.guest_ref, mode)?; - crate::cmd_config_approve(&crate::ConfigApproveArgs { - vm: guest_ref.name().as_str().to_owned(), - to: args.destination.clone(), - json: mode.is_json(), - }) + crate::legacy::config_validate_vm_name(guest_ref.name().as_str())?; + let staging = crate::legacy::config_staging_path(guest_ref.name().as_str()); + let staged_document = read_staged_document(context, &staging, mode)?; + invoke_config_stage(context, guest_ref.clone(), &staged_document, deadline, mode)?; + let opaque_destination = config_destination_identifier(&args.destination); + let service_approval = invoke_config_approve( + context, + guest_ref.clone(), + opaque_destination, + deadline, + mode, + )?; + if service_approval.sha256 != staged_document.sha256() { + return Err(context.failure( + "config-approve-failed", + "config-nixos approved a different staged document", + mode, + 1, + )); + } + let bytes = crate::legacy::config_approve_core_with_digest( + &staging, + &args.destination, + Some(&service_approval.sha256), + )?; + if service_approval.bytes != bytes { + return Err(context.failure( + "config-approve-failed", + "config-nixos approved a different staged document", + mode, + 1, + )); + } + context.emit( + &json!({ + "command": "config approve", + "resourceRef": guest_ref.to_canonical_string(), + "destination": args.destination.display().to_string(), + "bytes": bytes, + }), + mode, + )?; + Ok(0) } ActivationConfigCommand::Reject(args) => { let guest_ref = parse_guest_ref(context, &args.guest_ref, mode)?; - crate::cmd_config_reject(&crate::ConfigRejectArgs { - vm: guest_ref.name().as_str().to_owned(), - json: mode.is_json(), - }) + crate::legacy::config_validate_vm_name(guest_ref.name().as_str())?; + let staging = crate::legacy::config_staging_path(guest_ref.name().as_str()); + let staged_document = if staging.exists() { + Some(read_staged_document(context, &staging, mode)?) + } else { + None + }; + if let Some(document) = staged_document.as_ref() { + invoke_config_stage(context, guest_ref.clone(), document, deadline, mode)?; + } + let removed = crate::legacy::config_reject_core(&staging)?; + let service_rejection = + invoke_config_reject(context, guest_ref.clone(), deadline, mode)?; + context.emit( + &json!({ + "command": "config reject", + "resourceRef": guest_ref.to_canonical_string(), + "removed": removed || service_rejection.removed, + }), + mode, + )?; + Ok(0) } ActivationConfigCommand::Status(args) => { let guest_ref = parse_guest_ref(context, &args.guest_ref, mode)?; - crate::cmd_config_status(&crate::ConfigStatusArgs { - vm: Some(guest_ref.name().as_str().to_owned()), - all: false, - json: mode.is_json(), - }) + crate::legacy::config_validate_vm_name(guest_ref.name().as_str())?; + let staging = crate::legacy::config_staging_path(guest_ref.name().as_str()); + if staging.exists() { + let staged_document = read_staged_document(context, &staging, mode)?; + invoke_config_stage(context, guest_ref.clone(), &staged_document, deadline, mode)?; + } + let status = invoke_config_status(context, guest_ref.clone(), deadline, mode)?; + context.emit( + &json!({ + "command": "config status", + "resourceRef": guest_ref.to_canonical_string(), + "pending": status.pending, + "bytes": status.bytes, + "sha256": status.sha256, + }), + mode, + )?; + Ok(0) } } } +fn strip_config_response_envelope(mut value: serde_json::Value) -> serde_json::Value { + if let serde_json::Value::Object(object) = &mut value { + for key in ["ok", "zoneRef", "schemaVersion"] { + object.remove(key); + } + } + value +} + +fn read_staged_document( + context: &ZoneContext, + staging: &std::path::Path, + mode: OutputMode, +) -> Result { + let bytes = std::fs::read(staging).map_err(|error| { + context.failure( + "config-stage-failed", + &format!("config staging read failed: {error}"), + mode, + 1, + ) + })?; + GuestConfigDocument::new(bytes).map_err(|error| { + context.failure( + error.code(), + "config staging document failed validation", + mode, + 1, + ) + }) +} + +fn invoke_config_stage( + context: &ZoneContext, + guest_ref: d2b_contracts_resource::v3::ResourceRef, + document: &GuestConfigDocument, + deadline: RequestDeadline, + mode: OutputMode, +) -> Result { + let request = ConfigStageRequest::new(guest_ref, document).map_err(|error| { + context.failure(error.code(), "config stage request is invalid", mode, 1) + })?; + let value = context.invoke_service( + d2b_resource_client::ZoneServiceKind::ConfigNixos, + "ConfigNixosService/Stage", + "invoke", + serde_json::to_value(request).map_err(|_| { + context.failure( + "config-stage-failed", + "config stage request encoding failed", + mode, + 1, + ) + })?, + deadline, + mode, + )?; + serde_json::from_value(strip_config_response_envelope(value)).map_err(|_| { + context.failure( + "config-stage-failed", + "Zone returned an invalid config stage response", + mode, + 1, + ) + }) +} + +fn invoke_config_diff( + context: &ZoneContext, + guest_ref: d2b_contracts_resource::v3::ResourceRef, + against: String, + deadline: RequestDeadline, + mode: OutputMode, +) -> Result { + let request = ConfigDiffRequest::new(guest_ref, against).map_err(|error| { + context.failure(error.code(), "config diff request is invalid", mode, 1) + })?; + let value = context.invoke_service( + d2b_resource_client::ZoneServiceKind::ConfigNixos, + "ConfigNixosService/Diff", + "invoke", + serde_json::to_value(request).map_err(|_| { + context.failure( + "config-diff-failed", + "config diff request encoding failed", + mode, + 1, + ) + })?, + deadline, + mode, + )?; + serde_json::from_value(strip_config_response_envelope(value)).map_err(|_| { + context.failure( + "config-diff-failed", + "Zone returned an invalid config diff response", + mode, + 1, + ) + }) +} + +fn invoke_config_approve( + context: &ZoneContext, + guest_ref: d2b_contracts_resource::v3::ResourceRef, + destination: String, + deadline: RequestDeadline, + mode: OutputMode, +) -> Result { + let request = ConfigApproveRequest::new(guest_ref, destination).map_err(|error| { + context.failure(error.code(), "config approve request is invalid", mode, 1) + })?; + let value = context.invoke_service( + d2b_resource_client::ZoneServiceKind::ConfigNixos, + "ConfigNixosService/Approve", + "invoke", + serde_json::to_value(request).map_err(|_| { + context.failure( + "config-approve-failed", + "config approve request encoding failed", + mode, + 1, + ) + })?, + deadline, + mode, + )?; + serde_json::from_value(strip_config_response_envelope(value)).map_err(|_| { + context.failure( + "config-approve-failed", + "Zone returned an invalid config approval response", + mode, + 1, + ) + }) +} + +fn invoke_config_reject( + context: &ZoneContext, + guest_ref: d2b_contracts_resource::v3::ResourceRef, + deadline: RequestDeadline, + mode: OutputMode, +) -> Result { + let request = ConfigRejectRequest::new(guest_ref).map_err(|error| { + context.failure(error.code(), "config reject request is invalid", mode, 1) + })?; + let value = context.invoke_service( + d2b_resource_client::ZoneServiceKind::ConfigNixos, + "ConfigNixosService/Reject", + "invoke", + serde_json::to_value(request).map_err(|_| { + context.failure( + "config-reject-failed", + "config reject request encoding failed", + mode, + 1, + ) + })?, + deadline, + mode, + )?; + serde_json::from_value(strip_config_response_envelope(value)).map_err(|_| { + context.failure( + "config-reject-failed", + "Zone returned an invalid config rejection response", + mode, + 1, + ) + }) +} + +fn invoke_config_status( + context: &ZoneContext, + guest_ref: d2b_contracts_resource::v3::ResourceRef, + deadline: RequestDeadline, + mode: OutputMode, +) -> Result { + let request = ConfigStatusRequest::new(guest_ref).map_err(|error| { + context.failure(error.code(), "config status request is invalid", mode, 1) + })?; + let value = context.invoke_service( + d2b_resource_client::ZoneServiceKind::ConfigNixos, + "ConfigNixosService/Status", + "invoke", + serde_json::to_value(request).map_err(|_| { + context.failure( + "config-status-failed", + "config status request encoding failed", + mode, + 1, + ) + })?, + deadline, + mode, + )?; + serde_json::from_value(strip_config_response_envelope(value)).map_err(|_| { + context.failure( + "config-status-failed", + "Zone returned an invalid config status response", + mode, + 1, + ) + }) +} + +fn config_view_identifier( + path: &std::path::Path, + context: &ZoneContext, + mode: OutputMode, +) -> Result { + let bytes = std::fs::read(path).map_err(|error| { + context.failure( + "config-diff-failed", + &format!("config diff: read comparison view: {error}"), + mode, + 1, + ) + })?; + let digest = Sha256::digest(bytes); + Ok(format!("sha256:{digest:x}")) +} + +fn config_destination_identifier(path: &std::path::Path) -> String { + let digest = Sha256::digest(path.to_string_lossy().as_bytes()); + format!("target-{digest:x}") +} + fn parse_guest_ref( context: &ZoneContext, value: &str, diff --git a/packages/d2b/src/context.rs b/packages/d2b/src/context.rs index 6ab440ee0c..30be445c91 100644 --- a/packages/d2b/src/context.rs +++ b/packages/d2b/src/context.rs @@ -1162,6 +1162,7 @@ impl CliZoneConnector { | ZoneServiceKind::Zone | ZoneServiceKind::Audit | ZoneServiceKind::Support + | ZoneServiceKind::ConfigNixos ) || target.service() != service || target.transport() != TransportKind::LocalUnix || target.owner().zone() != &self.zone_path @@ -1661,6 +1662,16 @@ fn validate_operation( | (ZoneServiceKind::Support, "SupportService/GenerateBundle", Some("support-bundle")) => { Ok(operation.to_owned()) } + ( + ZoneServiceKind::ConfigNixos, + "ConfigNixosService/ReadGuestConfig" + | "ConfigNixosService/Stage" + | "ConfigNixosService/Diff" + | "ConfigNixosService/Approve" + | "ConfigNixosService/Reject" + | "ConfigNixosService/Status", + Some("invoke"), + ) => Ok(operation.to_owned()), (ZoneServiceKind::Zone, "Attach" | "Create", Some("attach")) => Ok(operation.to_owned()), (ZoneServiceKind::Audit | ZoneServiceKind::Support, ..) => Err(ClientError::InvalidMethod), (_, _, Some(_)) => Err(ClientError::InvalidMethod), diff --git a/packages/d2b/src/legacy.rs b/packages/d2b/src/legacy.rs index 3ac23f2a27..610c13768f 100644 --- a/packages/d2b/src/legacy.rs +++ b/packages/d2b/src/legacy.rs @@ -2029,7 +2029,7 @@ pub(super) struct ConfigStatusArgs { /// (no privileged surface), from `XDG_STATE_HOME` (or `HOME`). Tests /// override it per-thread via `set_test_staging_base` rather than mutating /// process-global env. -pub(super) fn config_staging_base() -> PathBuf { +pub(crate) fn config_staging_base() -> PathBuf { #[cfg(test)] if let Some(base) = TEST_STAGING_BASE.with(|b| b.borrow().clone()) { return base; @@ -2038,7 +2038,7 @@ pub(super) fn config_staging_base() -> PathBuf { .map(PathBuf::from) .filter(|p| !p.as_os_str().is_empty()) .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/state"))) - .unwrap_or_else(|| PathBuf::from("/tmp/d2b-state")); + .unwrap_or_else(|| PathBuf::from(".d2b-state")); base.join("d2b/config-staging") } @@ -2056,17 +2056,17 @@ pub(super) fn set_test_staging_base(base: Option) { TEST_STAGING_BASE.with(|b| *b.borrow_mut() = base); } -pub(super) fn config_staging_path_in(base: &Path, vm: &str) -> PathBuf { +pub(crate) fn config_staging_path_in(base: &Path, vm: &str) -> PathBuf { base.join(format!("{vm}.guest.nix")) } -pub(super) fn config_staging_path(vm: &str) -> PathBuf { +pub(crate) fn config_staging_path(vm: &str) -> PathBuf { config_staging_path_in(&config_staging_base(), vm) } /// Reject VM names that are not the framework's `^[a-z][a-z0-9-]*$` /// shape, so a VM arg can never traverse out of the staging dir. -pub(super) fn config_validate_vm_name(vm: &str) -> Result<(), CliFailure> { +pub(crate) fn config_validate_vm_name(vm: &str) -> Result<(), CliFailure> { let ok = !vm.is_empty() && vm.chars().next().is_some_and(|c| c.is_ascii_lowercase()) && vm @@ -2129,7 +2129,15 @@ pub(super) fn config_validate_staging_bytes(bytes: &[u8]) -> Result<(), CliFailu /// Core (testable) approve: validate the staging file, atomically write /// it onto `target`, then remove the staging file. Returns the byte /// count written. -pub(super) fn config_approve_core(staging: &Path, target: &Path) -> Result { +pub(crate) fn config_approve_core(staging: &Path, target: &Path) -> Result { + config_approve_core_with_digest(staging, target, None) +} + +pub(crate) fn config_approve_core_with_digest( + staging: &Path, + target: &Path, + expected_sha256: Option<&str>, +) -> Result { if !staging.exists() { return Err(CliFailure::new( 1, @@ -2142,6 +2150,16 @@ pub(super) fn config_approve_core(staging: &Path, target: &Path) -> Result Result Result { +pub(crate) fn config_reject_core(staging: &Path) -> Result { if staging.exists() { std::fs::remove_file(staging) .map_err(|e| CliFailure::new(1, format!("config reject: {e}")))?; @@ -2217,7 +2235,7 @@ pub(super) fn warn_all_pending_staged_configs() { /// atomic on the same filesystem, so a crash never leaves a partially /// written file (and never a non-empty truncated one that `approve` /// might later accept). -pub(super) fn config_atomic_write(target: &Path, bytes: &[u8]) -> Result<(), CliFailure> { +pub(crate) fn config_atomic_write(target: &Path, bytes: &[u8]) -> Result<(), CliFailure> { use std::io::Write as _; let parent = target.parent().filter(|p| !p.as_os_str().is_empty()); let base = target @@ -16562,8 +16580,9 @@ mod config_cmd_tests { use std::sync::atomic::{AtomicUsize, Ordering}; use super::{ - config_approve_core, config_atomic_write, config_reject_core, config_staging_path_in, - config_validate_remote_path, config_validate_staging_bytes, config_validate_vm_name, + config_approve_core, config_approve_core_with_digest, config_atomic_write, + config_reject_core, config_staging_path_in, config_validate_remote_path, + config_validate_staging_bytes, config_validate_vm_name, }; static COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -16648,6 +16667,25 @@ mod config_cmd_tests { assert!(leftovers.is_empty(), "approve left a temp file behind"); } + #[test] + fn approve_digest_mismatch_does_not_publish_changed_staging() { + let dir = scratch("approve-digest-mismatch"); + let staging = config_staging_path_in(&dir, "work-aad"); + let target = dir.join("work.guest.nix"); + fs::write(&staging, b"{ changed = true; }\n").expect("write staging"); + fs::write(&target, b"{ keep = true; }\n").expect("seed target"); + + let err = config_approve_core_with_digest( + &staging, + &target, + Some("sha256:0000000000000000000000000000000000000000000000000000000000000000"), + ) + .expect_err("changed content must fail closed"); + assert!(err.message.contains("changed after service approval")); + assert_eq!(fs::read(&target).expect("read target"), b"{ keep = true; }\n"); + assert!(staging.exists()); + } + #[test] fn atomic_write_publishes_whole_value_under_concurrency() { // Prove rename-atomicity: many threads each publish a DISTINCT diff --git a/packages/d2b/src/status_read_model.rs b/packages/d2b/src/status_read_model.rs index a39ba8f9af..7d1981a027 100644 --- a/packages/d2b/src/status_read_model.rs +++ b/packages/d2b/src/status_read_model.rs @@ -844,6 +844,7 @@ fn process_role_name(role: &d2b_core::processes::ProcessRole) -> String { d2b_core::processes::ProcessRole::Usbip => "usbip", d2b_core::processes::ProcessRole::WaylandProxy => "wayland-proxy", d2b_core::processes::ProcessRole::SecurityKeyFrontend => "security-key-frontend", + d2b_core::processes::ProcessRole::ActivationNixosRunner => "activation-nixos-runner", } .to_owned() } diff --git a/packages/d2bd-runtime/src/guest_component_session.rs b/packages/d2bd-runtime/src/guest_component_session.rs new file mode 100644 index 0000000000..60f073937a --- /dev/null +++ b/packages/d2bd-runtime/src/guest_component_session.rs @@ -0,0 +1,400 @@ +//! Host-side initiator for the enrolled Guest ComponentSession. + +use std::{ + fs, + io::Read, + os::{ + fd::OwnedFd, + unix::fs::{MetadataExt, OpenOptionsExt}, + }, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; + +use d2b_contracts_resource::v3::{ + ResourceRef, ResourceUid, SchemaFingerprint, ZoneId, + identity::{ReconnectGeneration, SessionPurpose}, +}; +use d2b_contracts_zone_session::v3::component_session::EndpointPolicyIdentity; +use d2b_session::{HandshakeCredentials, Secret32, SessionEngine, SessionTtrpcClient}; +use d2b_session_unix::FramedVsockTransport; +use serde::{Deserialize, Serialize}; + +use crate::{ + guest_control_vsock::{ + GuestControlTransportProbeResult, connect_guest_control_vsock, + }, + guest_mode::{BootIdentity, GuestIdentity}, +}; + +/// Relative location of the host-published Guest session descriptor. +/// +/// The descriptor contains only non-secret identity and locator metadata. +/// Key material stays in separate files under the same state root. +pub const GUEST_COMPONENT_SESSION_DESCRIPTOR: &str = "component-session/guest.json"; +/// Relative location of the host-side ComponentSession private key. +pub const GUEST_COMPONENT_SESSION_PRIVATE_KEY: &str = "component-session/host.key"; +/// Relative location of the enrolled Guest public key. +pub const GUEST_COMPONENT_SESSION_GUEST_PUBLIC_KEY: &str = "component-session/guest.pub"; +/// Bounded ComponentSession connection attempt. +pub const COMPONENT_SESSION_ATTEMPT_CAP: Duration = Duration::from_secs(3); +/// Backoff between bounded ComponentSession readiness attempts. +pub const COMPONENT_SESSION_RETRY_BACKOFF: Duration = Duration::from_millis(250); + +/// Host-published, non-secret identity needed to reconnect to one Guest. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct GuestComponentSessionDescriptor { + pub guest_ref: String, + pub guest_uid: String, + pub zone: String, + pub boot_identity_digest: String, + pub purpose: String, + pub schema_fingerprint: String, + pub reconnect_generation: u64, + pub provider_generation: u64, + pub controller_generation: u64, + pub assignment_epoch: u64, +} + +impl GuestComponentSessionDescriptor { + /// Read and validate a host-published descriptor from one state root. + pub fn read_from_state_root(state_root: impl AsRef) -> Result { + let state_root = state_root.as_ref(); + let path = state_root.join(GUEST_COMPONENT_SESSION_DESCRIPTOR); + let metadata = fs::symlink_metadata(&path) + .map_err(|_| GuestComponentSessionError::DescriptorUnavailable)?; + if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.nlink() != 1 { + return Err(GuestComponentSessionError::DescriptorUnavailable); + } + let mut file = fs::OpenOptions::new() + .read(true) + .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) + .open(path) + .map_err(|_| GuestComponentSessionError::DescriptorUnavailable)?; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes) + .map_err(|_| GuestComponentSessionError::DescriptorUnavailable)?; + serde_json::from_slice(&bytes).map_err(|_| GuestComponentSessionError::DescriptorInvalid) + } + + /// Build the enrolled identity represented by this descriptor. + pub fn identity(&self) -> Result { + let guest_ref = ResourceRef::parse(&self.guest_ref) + .map_err(|_| GuestComponentSessionError::DescriptorInvalid)?; + let guest_uid = ResourceUid::parse(self.guest_uid.clone()) + .map_err(|_| GuestComponentSessionError::DescriptorInvalid)?; + let zone = + ZoneId::parse(&self.zone).map_err(|_| GuestComponentSessionError::DescriptorInvalid)?; + let boot_identity = BootIdentity::from_digest(&self.boot_identity_digest) + .map_err(|_| GuestComponentSessionError::DescriptorInvalid)?; + let purpose = SessionPurpose::parse(&self.purpose) + .map_err(|_| GuestComponentSessionError::DescriptorInvalid)?; + let schema_fingerprint = SchemaFingerprint::parse(&self.schema_fingerprint) + .map_err(|_| GuestComponentSessionError::DescriptorInvalid)?; + let reconnect_generation = ReconnectGeneration::new(self.reconnect_generation) + .map_err(|_| GuestComponentSessionError::DescriptorInvalid)?; + GuestIdentity::new( + guest_ref, + guest_uid, + zone, + boot_identity, + purpose, + schema_fingerprint, + reconnect_generation, + self.provider_generation, + self.controller_generation, + self.assignment_epoch, + ) + .map_err(|_| GuestComponentSessionError::DescriptorInvalid) + } +} + +/// Validated Cloud Hypervisor CONNECT endpoint for a Guest session. +#[derive(Debug, Clone)] +pub struct GuestComponentSessionEndpoint { + /// Cloud Hypervisor API Unix socket. + pub socket_path: PathBuf, + /// State root containing the socket and its ownership marker. + pub state_root: PathBuf, + /// Expected state-root owner UID. + pub expected_state_root_uid: u32, + /// Expected state-root owner GID. + pub expected_state_root_gid: u32, + /// Expected Cloud Hypervisor peer UID. + pub expected_peer_uid: u32, + /// Expected Cloud Hypervisor peer GID. + pub expected_peer_gid: u32, + /// Deadline for CONNECT and ACK. + pub setup_timeout: Duration, +} + +/// Fully resolved host-side ComponentSession connection material. +/// +/// The descriptor is non-secret; key bytes are loaded only from the +/// host-owned state root and are never serialized or included in diagnostics. +#[derive(Debug)] +pub struct GuestComponentSessionConfig { + pub identity: GuestIdentity, + pub endpoint: GuestComponentSessionEndpoint, + pub local_private: Secret32, + pub guest_public: [u8; 32], +} + +impl GuestComponentSessionConfig { + /// Resolve the descriptor and enrolled keys from one validated state root. + pub fn from_state_root( + state_root: impl AsRef, + endpoint: GuestComponentSessionEndpoint, + ) -> Result { + let state_root = state_root.as_ref(); + let descriptor = GuestComponentSessionDescriptor::read_from_state_root(state_root)?; + let identity = descriptor.identity()?; + let private_path = state_root.join(GUEST_COMPONENT_SESSION_PRIVATE_KEY); + let public_path = state_root.join(GUEST_COMPONENT_SESSION_GUEST_PUBLIC_KEY); + let local_private = read_private_key(&private_path)?; + let guest_public = read_public_key(&public_path)?; + Ok(Self { + identity, + endpoint, + local_private, + guest_public, + }) + } + + /// Establish the authenticated session represented by this configuration. + pub async fn connect( + self, + ) -> Result { + GuestComponentSessionClient::connect( + self.identity, + self.endpoint, + self.local_private, + self.guest_public, + ) + .await + } +} + +/// A generated-ttrpc client backed by one authenticated Guest session. +pub struct GuestComponentSessionClient { + identity: GuestIdentity, + session_generation: u64, + transport: SessionTtrpcClient, +} + +impl std::fmt::Debug for GuestComponentSessionClient { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("GuestComponentSessionClient()") + } +} + +impl GuestComponentSessionClient { + /// Connect using an already validated identity, endpoint, and keys. + /// Connect through the validated Cloud Hypervisor CONNECT endpoint. + pub async fn connect( + identity: GuestIdentity, + endpoint: GuestComponentSessionEndpoint, + local_private: Secret32, + guest_public: [u8; 32], + ) -> Result { + let connected = tokio::task::spawn_blocking(move || { + connect_guest_control_vsock( + &endpoint.socket_path, + &endpoint.state_root, + endpoint.expected_state_root_uid, + endpoint.expected_state_root_gid, + endpoint.expected_peer_uid, + endpoint.expected_peer_gid, + endpoint.setup_timeout, + ) + }) + .await + .map_err(|_| GuestComponentSessionClientError::Transport)?; + let connected = match connected { + GuestControlTransportProbeResult::Connected(stream) => stream, + GuestControlTransportProbeResult::Failed(_) => { + return Err(GuestComponentSessionClientError::Transport); + } + }; + let transport = connected_stream_to_transport(connected)?; + let policy = EndpointPolicyIdentity::from(&identity.endpoint_policy()); + let engine = SessionEngine::establish_initiator_with_generation_discovery( + transport, + policy, + HandshakeCredentials::Kk { + local_private, + remote_public: guest_public, + }, + std::time::Instant::now(), + ) + .await + .map_err(|_| GuestComponentSessionClientError::Session)?; + let generation = engine.generation(); + if generation < identity.reconnect_generation().get() { + return Err(GuestComponentSessionClientError::StaleSession); + } + + fn connected_stream_to_transport( + connected: crate::guest_control_vsock::GuestControlConnectedStream, + ) -> Result< + FramedVsockTransport, + GuestComponentSessionClientError, + > { + let socket = connected.into_socket(); + socket + .set_read_timeout(None) + .map_err(|_| GuestComponentSessionClientError::Transport)?; + socket + .set_write_timeout(None) + .map_err(|_| GuestComponentSessionClientError::Transport)?; + let fd: OwnedFd = socket.into(); + let stream = std::os::unix::net::UnixStream::from(fd); + stream + .set_nonblocking(true) + .map_err(|_| GuestComponentSessionClientError::Transport)?; + let stream = tokio::net::UnixStream::from_std(stream) + .map_err(|_| GuestComponentSessionClientError::Transport)?; + Ok(FramedVsockTransport::new(stream)) + } + let driver = engine.into_driver(); + Ok(Self { + identity, + session_generation: generation, + transport: SessionTtrpcClient::new(Arc::new(driver)), + }) + } + + /// The enrolled Guest identity bound into the session transcript. + pub fn identity(&self) -> &GuestIdentity { + &self.identity + } + + /// The reconnect generation authenticated by the session. + pub fn generation(&self) -> u64 { + self.session_generation + } + + /// Clone the generated-ttrpc client for a typed service adapter. + pub fn client(&self) -> ttrpc::r#async::Client { + self.transport.client() + } + + /// Return the generated ResourceService client carried by this + /// authenticated ComponentSession. + pub fn resource_service_client( + &self, + ) -> d2b_resource_api::generated::d2b_resource_v3_ttrpc::ResourceServiceClient { + d2b_resource_api::generated::d2b_resource_v3_ttrpc::ResourceServiceClient::new( + self.transport.client(), + ) + } +} + +/// Errors while loading host-published session metadata. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GuestComponentSessionError { + DescriptorUnavailable, + DescriptorInvalid, + KeyUnavailable, + KeyInvalid, +} + +impl std::fmt::Display for GuestComponentSessionError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::DescriptorUnavailable => "guest-component-session-descriptor-unavailable", + Self::DescriptorInvalid => "guest-component-session-descriptor-invalid", + Self::KeyUnavailable => "guest-component-session-key-unavailable", + Self::KeyInvalid => "guest-component-session-key-invalid", + }) + } +} + +impl std::error::Error for GuestComponentSessionError {} + +fn read_private_key(path: &Path) -> Result { + let bytes = read_key_bytes(path)?; + let bytes: [u8; 32] = bytes + .try_into() + .map_err(|_| GuestComponentSessionError::KeyInvalid)?; + Secret32::new(bytes).map_err(|_| GuestComponentSessionError::KeyInvalid) +} + +fn read_public_key(path: &Path) -> Result<[u8; 32], GuestComponentSessionError> { + let bytes = read_key_bytes(path)?; + let bytes: [u8; 32] = bytes + .try_into() + .map_err(|_| GuestComponentSessionError::KeyInvalid)?; + if bytes == [0_u8; 32] { + return Err(GuestComponentSessionError::KeyInvalid); + } + Ok(bytes) +} + +fn read_key_bytes(path: &Path) -> Result, GuestComponentSessionError> { + let metadata = + fs::symlink_metadata(path).map_err(|_| GuestComponentSessionError::KeyUnavailable)?; + if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.nlink() != 1 { + return Err(GuestComponentSessionError::KeyInvalid); + } + let bytes = fs::OpenOptions::new() + .read(true) + .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) + .open(path) + .map_err(|_| GuestComponentSessionError::KeyUnavailable) + .and_then(|mut file| { + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes) + .map_err(|_| GuestComponentSessionError::KeyUnavailable)?; + if bytes.len() != 32 { + return Err(GuestComponentSessionError::KeyInvalid); + } + Ok(bytes) + })?; + Ok(bytes) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GuestComponentSessionClientError { + Transport, + Session, + StaleSession, +} + +impl std::fmt::Display for GuestComponentSessionClientError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Transport => "guest-component-session-transport-unavailable", + Self::Session => "guest-component-session-authentication-failed", + Self::StaleSession => "guest-component-session-generation-stale", + }) + } +} + +impl std::error::Error for GuestComponentSessionClientError {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn descriptor_rejects_hardlinked_state_files() { + let root = std::env::current_dir() + .expect("test working directory") + .join(".scratch") + .join(format!("component-session-{}", std::process::id())); + let directory = root.join("component-session"); + fs::create_dir_all(&directory).expect("component-session directory"); + let source = root.join("descriptor-source"); + let descriptor = directory.join("guest.json"); + fs::write(&source, b"{}").expect("descriptor source"); + fs::hard_link(&source, &descriptor).expect("descriptor hardlink"); + assert_eq!( + GuestComponentSessionDescriptor::read_from_state_root(&root), + Err(GuestComponentSessionError::DescriptorUnavailable) + ); + fs::remove_dir_all(root).expect("cleanup"); + } +} diff --git a/packages/d2bd-runtime/src/guest_mode.rs b/packages/d2bd-runtime/src/guest_mode.rs index 6d0b994cd6..f35a915f72 100644 --- a/packages/d2bd-runtime/src/guest_mode.rs +++ b/packages/d2bd-runtime/src/guest_mode.rs @@ -33,6 +33,7 @@ use sha2::{Digest, Sha256}; use crate::{ broker_transport::{ModeBoundBrokerAdapter, ModeBoundBrokerError}, + guest_resource_runtime::{GuestResourceRuntime, GuestResourceRuntimeError}, target_runtime::{ AdmissionBudget, AdmissionError, AdmissionKind, AdmissionLimits, AssignmentLease, ControllerAssignmentKey, DaemonMode, DeploymentError, ProviderDeployment, @@ -91,6 +92,26 @@ impl BootIdentity { Self::from_kernel_boot_id(&value) } + /// Rehydrate a previously published boot-identity commitment. + /// + /// Hosts never need the raw kernel boot-id to bind a reconnect. They may + /// persist only this domain-separated digest and use it to reconstruct + /// the enrolled identity after a daemon restart. + pub fn from_digest(value: &str) -> Result { + let encoded = value + .strip_prefix("sha256:") + .ok_or(GuestModeError::BootIdentityInvalid)?; + if encoded.len() != 64 || !encoded.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(GuestModeError::BootIdentityInvalid); + } + let mut digest = [0_u8; 32]; + for (index, slot) in digest.iter_mut().enumerate() { + *slot = u8::from_str_radix(&encoded[index * 2..index * 2 + 2], 16) + .map_err(|_| GuestModeError::BootIdentityInvalid)?; + } + Ok(Self(digest)) + } + pub const fn digest(self) -> [u8; 32] { self.0 } @@ -327,10 +348,12 @@ pub struct GuestHandshakeAdmission { #[derive(Debug)] struct GuestRuntimeInner { identity: GuestIdentity, + resource_runtime: GuestResourceRuntime, deployment: ProviderDeployment, admission: AdmissionBudget, broker: ModeBoundBrokerAdapter, - active_generation: Mutex>, + active_generation: Arc>>, + last_generation: Mutex, active_session_permit: Mutex>, } @@ -342,24 +365,32 @@ pub struct GuestRuntime { } impl GuestRuntime { - pub fn new( + pub async fn new( identity: GuestIdentity, broker_socket: PathBuf, broker_uid: u32, limits: AdmissionLimits, + state_dir: impl AsRef, ) -> Result { let admission = AdmissionBudget::new(limits).map_err(GuestModeError::Admission)?; let deployment = ProviderDeployment::new(DaemonMode::Guest, limits) .map_err(GuestModeError::Admission)?; + let resource_runtime = GuestResourceRuntime::new(identity.clone(), state_dir) + .await + .map_err(GuestModeError::Resource)?; + let active_generation = resource_runtime.active_generation(); let broker = ModeBoundBrokerAdapter::guest(broker_socket, broker_uid); broker.validate_instance().map_err(GuestModeError::Broker)?; + let initial_generation = identity.reconnect_generation().get().saturating_sub(1); Ok(Self { inner: Arc::new(GuestRuntimeInner { identity, + resource_runtime, deployment, admission, broker, - active_generation: Mutex::new(None), + active_generation, + last_generation: Mutex::new(initial_generation), active_session_permit: Mutex::new(None), }), }) @@ -369,6 +400,15 @@ impl GuestRuntime { &self.inner.identity } + /// Return the target-local Resource API owned by this Guest runtime. + /// + /// Keeping this alongside the session admission state prevents callers + /// from accidentally constructing a second store with a mismatched + /// identity. + pub fn resource_runtime(&self) -> &GuestResourceRuntime { + &self.inner.resource_runtime + } + pub fn deployment(&self) -> &ProviderDeployment { &self.inner.deployment } @@ -377,6 +417,15 @@ impl GuestRuntime { &self.inner.broker } + fn next_generation(&self) -> Result { + self.inner + .last_generation + .lock() + .map_err(|_| GuestModeError::StateUnavailable)? + .checked_add(1) + .ok_or(GuestModeError::GenerationZero) + } + pub const fn component_session_port() -> u32 { GUEST_COMPONENT_SESSION_PORT } @@ -466,6 +515,14 @@ impl GuestRuntime { generation: u64, admission: GuestHandshakeAdmission, ) -> Result { + let mut last_generation = self + .inner + .last_generation + .lock() + .map_err(|_| GuestModeError::StateUnavailable)?; + if generation <= *last_generation { + return Err(GuestModeError::StaleSession); + } let mut active = self .inner .active_generation @@ -494,6 +551,7 @@ impl GuestRuntime { *active_permit = Some(admission.session_permit.clone()); } *active = Some(generation); + *last_generation = generation; Ok(GuestSessionLease { runtime: Arc::downgrade(&self.inner), generation, @@ -539,7 +597,8 @@ impl GuestRuntime { { let admission = self.admit_handshake()?; let identity = self.inner.identity.clone(); - let policy = identity.endpoint_policy(); + let minimum_generation = self.next_generation()?; + let policy = identity.endpoint_policy_for_generation(minimum_generation); let engine = SessionEngine::establish_responder_with_generation_floor( transport, policy.clone(), @@ -547,7 +606,7 @@ impl GuestRuntime { local_private, remote_public: parent_public, }, - identity.reconnect_generation().get(), + minimum_generation, Instant::now(), ) .await @@ -747,6 +806,7 @@ fn monotonic_tick() -> u64 { pub enum GuestModeError { Admission(AdmissionError), Deployment(DeploymentError), + Resource(GuestResourceRuntimeError), GuestIdentityWrongKind, BootIdentityInvalid, BootIdentityUnavailable, @@ -767,6 +827,7 @@ impl std::fmt::Display for GuestModeError { formatter.write_str(match self { Self::Admission(error) => return error.fmt(formatter), Self::Deployment(error) => return error.fmt(formatter), + Self::Resource(error) => return error.fmt(formatter), Self::GuestIdentityWrongKind => "guest-mode-identity-wrong-kind", Self::BootIdentityInvalid => "guest-mode-boot-identity-invalid", Self::BootIdentityUnavailable => "guest-mode-boot-identity-unavailable", @@ -832,15 +893,18 @@ mod tests { assert_ne!(policy.transport_binding.channel_binding, [0; 32]); } - #[test] - fn guest_runtime_exposes_no_host_authority_surfaces() { + #[tokio::test] + async fn guest_runtime_exposes_no_host_authority_surfaces() { let identity = identity(1); + let state_dir = tempfile::tempdir().expect("state directory"); let runtime = GuestRuntime::new( identity, PathBuf::from("/run/d2b/guest-broker.sock"), 997, AdmissionLimits::guest_default(), + state_dir.path(), ) + .await .expect("runtime"); assert!(!runtime.surfaces().local_zone_store); assert!(runtime.local_zone_store().is_err()); diff --git a/packages/d2bd-runtime/src/guest_resource_runtime.rs b/packages/d2bd-runtime/src/guest_resource_runtime.rs new file mode 100644 index 0000000000..64105cee8e --- /dev/null +++ b/packages/d2bd-runtime/src/guest_resource_runtime.rs @@ -0,0 +1,1097 @@ +//! Target-local Resource API for an authenticated Guest ComponentSession. +//! +//! Guest mode has no Zone store. Controller-created Process-family resources +//! therefore live in this bounded in-memory store and are exposed only through +//! the currently admitted parent-Zone ComponentSession. + +use std::{ + collections::BTreeMap, + fs::{self, File, OpenOptions}, + os::unix::fs::{MetadataExt, OpenOptionsExt}, + path::Path, + sync::{Arc, Mutex}, +}; + +use d2b_contracts_resource::v3::{ + ConfigurationGeneration, ControllerGeneration, ResourceEnvelope, ResourceGeneration, + ResourceRef, ResourceTypeName, ResourceUid, RetryClass, ZoneId, + ZoneRevision, + activation_nixos::NIXOS_GENERATION_RESOURCE_TYPE, + resource_schema::SCHEMA_DOMAIN_TAG, +}; +use d2b_resource_api::{ + ResourceApiClient, ResourceBusAdapter, ResourceService, ResourceStoreBackend, + authz::{ + ApiCatalog, AuthorizationState, BindingScope, BootstrapPhase, BoundSubject, CompiledRole, + CompiledRoleBinding, NativeAuthorizer, PolicyRule, PolicySet, RelayGrantAuthority, + ResourceVerb, SessionVerb, + }, + service::UnavailableUpgradeDispatcher, +}; +use d2b_resource_store::{ + ExpectedRevision, MutationSealBody, ResourceMutationKind, SealedMutation, StoreCommitResult, + StoreError, StoreErrorKind, StoreGetRequest, StoreInspectSchemaRequest, StoreListRequest, + StoreListResult, StoreResolveRequest, StoreResolvedIdentity, StoreWatchReceipt, + StoreWatchRequest, StoredResource, StoredSchema, + mutation_seal::MutationSealAcceptor, +}; +use d2b_resource_store_redb::{ + RedbResourceStore, StoreIdentity, write_provisioning_marker, +}; + +use crate::{ + guest_mode::GuestIdentity, + resource_runtime_support::{initial_policy_snapshot, store_identity}, +}; + +#[cfg(test)] +const STORE_SLOT: u32 = 0; +const STORE_FILE_NAME: &str = "resource-store.redb"; +const STORE_MARKER_NAME: &str = "resource-store.marker"; +const ROLE_REF: &str = "Role/guest-component-session"; +const WATCH_STREAM_PREFIX: &str = "guest-watch"; +const SCHEMA_BYTES: &[u8] = br#"{"apiVersion":"d2b-cjson/v1","resourceType":"target-local"}"#; +type CommitFence = Arc Result<(), StoreError> + Send + Sync>; + +/// Authenticated target-local resource runtime for Guest mode. +#[derive(Clone)] +pub struct GuestResourceRuntime { + identity: GuestIdentity, + store: Arc, + authorizer: Arc, + authorization_state: AuthorizationState, + active_generation: Arc>>, +} + +impl core::fmt::Debug for GuestResourceRuntime { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("GuestResourceRuntime()") + } +} + +impl GuestResourceRuntime { + /// Build the target-local Resource API with Guest-owned durable state. + pub async fn new( + identity: GuestIdentity, + state_dir: impl AsRef, + ) -> Result { + let zone = identity.zone().clone(); + let activation_type = ResourceTypeName::parse(NIXOS_GENERATION_RESOURCE_TYPE) + .map_err(|_| GuestResourceRuntimeError::Policy)?; + let catalog = ApiCatalog::with_extensions([activation_type.clone()]) + .map_err(|_| GuestResourceRuntimeError::Policy)?; + let resource_types = [ + "Process", + "EphemeralProcess", + "Endpoint", + NIXOS_GENERATION_RESOURCE_TYPE, + ] + .into_iter() + .map(ResourceTypeName::parse) + .collect::, _>>() + .map_err(|_| GuestResourceRuntimeError::Policy)?; + let resource_verbs = [ + ResourceVerb::Get, + ResourceVerb::List, + ResourceVerb::Watch, + ResourceVerb::Create, + ResourceVerb::UpdateSpec, + ResourceVerb::UpdateStatus, + ResourceVerb::UpdateMetadata, + ResourceVerb::UpdateFinalizers, + ResourceVerb::Delete, + ]; + let session_verbs = [ + SessionVerb::Connect, + SessionVerb::Invoke, + SessionVerb::OpenStream, + SessionVerb::Cancel, + SessionVerb::Observe, + ]; + let rules = resource_types + .chunks(16) + .map(|resource_types| { + PolicyRule::new( + &catalog, + resource_types.iter().cloned(), + resource_verbs, + session_verbs, + [], + [], + [zone.clone()], + [], + ) + .map_err(|_| GuestResourceRuntimeError::Policy) + }) + .collect::, _>>()?; + let role_ref = + ResourceRef::parse(ROLE_REF).map_err(|_| GuestResourceRuntimeError::Policy)?; + let role = + CompiledRole::new(role_ref.clone(), rules) + .map_err(|_| GuestResourceRuntimeError::Policy)?; + let binding_scope = BindingScope { + zones: [zone.clone()].into_iter().collect(), + ..BindingScope::default() + }; + let binding = CompiledRoleBinding::new( + role_ref, + [BoundSubject { + subject_ref: identity.guest_ref().clone(), + subject_uid: identity.guest_uid().clone(), + }], + binding_scope, + RelayGrantAuthority::None, + ) + .map_err(|_| GuestResourceRuntimeError::Policy)?; + let policy_revision = 1; + let policy = + PolicySet::new(&catalog, policy_revision, vec![role], vec![binding]) + .map_err(|_| GuestResourceRuntimeError::Policy)?; + let authorization_state = AuthorizationState { + snapshot: d2b_resource_store::PolicySnapshot { + policy_revision, + api_catalog_revision: 1, + active_configuration_revision: ConfigurationGeneration::new(1) + .map_err(|_| GuestResourceRuntimeError::Policy)?, + controller_generation: Some( + ControllerGeneration::new(identity.controller_generation()) + .map_err(|_| GuestResourceRuntimeError::Policy)?, + ), + }, + zone_policy_revision: ZoneRevision::new(1), + bootstrap_phase: BootstrapPhase::Disabled, + now_tick: 1, + }; + let authorizer = Arc::new( + NativeAuthorizer::new(catalog, Some(policy)) + .map_err(|_| GuestResourceRuntimeError::Policy)?, + ); + let store_identity = store_identity(&zone, &format!("guest-target:{}", identity.guest_uid())) + .map_err(|_| GuestResourceRuntimeError::Store)? + .with_revisions( + initial_policy_snapshot().map_err(|_| GuestResourceRuntimeError::Store)?, + ); + let acceptor = authorizer + .take_store_seal(store_identity.seal_identity()) + .map_err(|_| GuestResourceRuntimeError::Store)?; + let backend = Arc::new( + GuestResourceStore::open_durable( + zone, + identity.guest_ref().clone(), + state_dir.as_ref(), + store_identity, + acceptor, + ) + .await?, + ); + let active_generation = Arc::new(Mutex::new(None)); + Ok(Self { + identity, + store: backend, + authorizer, + authorization_state, + active_generation, + }) + } + + /// This runtime is intentionally not backed by a local Zone store. + pub const fn is_target_local(&self) -> bool { + true + } + + pub(crate) fn active_generation(&self) -> Arc>> { + Arc::clone(&self.active_generation) + } + + /// Bind the Resource API to one already authenticated session route. + pub fn bind_session( + &self, + route: &d2b_session::AuthenticatedSessionRouteBinding, + ) -> Result { + self.identity + .validate_route(route) + .map_err(|_| GuestResourceRuntimeError::SessionBinding)?; + let subject = self + .authorizer + .issue_authenticated_subject(route.context().clone(), self.authorization_state.clone()) + .map_err(|_| GuestResourceRuntimeError::Authorization)?; + let backend = Arc::new(SessionBoundStore { + store: Arc::clone(&self.store), + active_generation: Arc::clone(&self.active_generation), + generation: route.reconnect_generation().get(), + }); + let session_store = Arc::clone(&backend); + let service = Arc::new( + ResourceService::new(backend, Arc::clone(&self.authorizer)) + .map_err(|_| GuestResourceRuntimeError::Store)?, + ); + let adapter = ResourceBusAdapter::bind_component_session(service, subject) + .map_err(|_| GuestResourceRuntimeError::Authorization)?; + Ok(GuestResourceSession { + store: session_store, + adapter: Arc::new(adapter), + generation: route.reconnect_generation().get(), + }) + } +} + +/// Resource API capability bound to one Guest session generation. +pub struct GuestResourceSession { + store: Arc, + adapter: Arc>, + generation: u64, +} + +impl core::fmt::Debug for GuestResourceSession { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("GuestResourceSession") + .field("generation", &"") + .finish() + } +} + +impl GuestResourceSession { + /// Return the authenticated reconnect generation for diagnostics. + pub const fn generation(&self) -> u64 { + self.generation + } + + /// Build the generated ResourceService map for the session server. + pub fn ttrpc_services(&self) -> std::collections::HashMap { + Arc::clone(&self.adapter).ttrpc_services() + } + + /// Return the in-process client used by target-local controllers. + pub fn client(&self) -> ResourceApiClient { + self.adapter.client() + } + + /// Return the store backend fenced to this authenticated session. + /// + /// Target-local controllers use the same backend as the bus adapter so + /// relists and status mutations cannot outlive the session generation. + pub fn store_backend(&self) -> Arc { + Arc::clone(&self.store) + } +} + +/// Closed construction and binding failures. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GuestResourceRuntimeError { + Policy, + Store, + StoreQuarantined, + SessionBinding, + Authorization, +} + +impl core::fmt::Display for GuestResourceRuntimeError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(match self { + Self::Policy => "guest-resource-policy-unavailable", + Self::Store => "guest-resource-store-unavailable", + Self::StoreQuarantined => "guest-resource-store-quarantined", + Self::SessionBinding => "guest-resource-session-binding-invalid", + Self::Authorization => "guest-resource-authorization-denied", + }) + } +} + +impl std::error::Error for GuestResourceRuntimeError {} + +struct GuestStoreState { + revision: u64, + resources: BTreeMap, + next_watch: u64, +} + +enum GuestStoreBackend { + Durable(Arc), + Memory { + acceptor: MutationSealAcceptor, + state: Mutex, + }, +} + +/// Target-local store owned by one Guest. +pub struct GuestResourceStore { + zone: ZoneId, + target: Option, + backend: GuestStoreBackend, +} + +impl core::fmt::Debug for GuestResourceStore { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("GuestResourceStore") + .field("resource_count", &self.resource_count()) + .finish() + } +} + +impl GuestResourceStore { + fn new_in_memory(zone: ZoneId, acceptor: MutationSealAcceptor) -> Self { + Self { + zone, + target: None, + backend: GuestStoreBackend::Memory { + acceptor, + state: Mutex::new(GuestStoreState { + revision: 0, + resources: BTreeMap::new(), + next_watch: 0, + }), + }, + } + } + + async fn open_durable( + zone: ZoneId, + target: ResourceRef, + state_dir: &Path, + identity: StoreIdentity, + acceptor: MutationSealAcceptor, + ) -> Result { + let metadata = fs::symlink_metadata(state_dir) + .map_err(|_| GuestResourceRuntimeError::Store)?; + if !metadata.is_dir() || metadata.file_type().is_symlink() || metadata.mode() & 0o002 != 0 { + return Err(GuestResourceRuntimeError::Store); + } + let database_path = state_dir.join(STORE_FILE_NAME); + let marker_path = state_dir.join(STORE_MARKER_NAME); + let database_present = fs::symlink_metadata(&database_path) + .map(|metadata| !metadata.file_type().is_symlink()) + .unwrap_or(false); + let marker_present = fs::symlink_metadata(&marker_path) + .map(|metadata| !metadata.file_type().is_symlink()) + .unwrap_or(false); + if database_present != marker_present { + return Err(GuestResourceRuntimeError::StoreQuarantined); + } + let database = open_owned_file(&database_path)?; + let mut marker = open_owned_file(&marker_path)?; + let database_empty = database + .metadata() + .map_err(|_| GuestResourceRuntimeError::Store)? + .len() + == 0; + let marker_empty = marker + .metadata() + .map_err(|_| GuestResourceRuntimeError::Store)? + .len() + == 0; + let store = if database_empty && marker_empty { + write_provisioning_marker(&mut marker, &identity) + .map_err(|_| GuestResourceRuntimeError::Store)?; + RedbResourceStore::provision_owned(database, marker, identity, acceptor) + .await + .map_err(map_store_error)? + } else if database_empty || marker_empty { + return Err(GuestResourceRuntimeError::StoreQuarantined); + } else { + drop(marker); + RedbResourceStore::open_owned(database, identity, acceptor) + .await + .map_err(map_store_error)? + }; + Ok(Self { + zone, + target: Some(target), + backend: GuestStoreBackend::Durable(Arc::new(store)), + }) + } + + fn resource_count(&self) -> usize { + match &self.backend { + GuestStoreBackend::Durable(_) => 0, + GuestStoreBackend::Memory { state, .. } => state + .lock() + .map(|state| state.resources.len()) + .unwrap_or(0), + } + } + + fn is_target_local_type(resource_type: &ResourceTypeName) -> bool { + matches!( + resource_type.as_str(), + "Process" | "EphemeralProcess" | "Endpoint" | NIXOS_GENERATION_RESOURCE_TYPE + ) + } + + fn forbidden() -> StoreError { + StoreError::new( + StoreErrorKind::AuthorizationDenied, + None, + None, + RetryClass::Never, + "guest-target-resource-type-denied", + ) + } + + fn unavailable(reason: &'static str) -> StoreError { + StoreError::new( + StoreErrorKind::ResourcePlaneUnavailable, + None, + None, + RetryClass::AfterDelay, + reason, + ) + } + + fn invalid(reason: &'static str) -> StoreError { + StoreError::new( + StoreErrorKind::ResourceSchemaInvalid, + None, + None, + RetryClass::Never, + reason, + ) + } + + fn not_found() -> StoreError { + StoreError::new( + StoreErrorKind::ResourceNotFound, + None, + None, + RetryClass::Never, + "guest-target-resource-not-found", + ) + } + + fn conflict(revision: u64) -> StoreError { + StoreError::new( + StoreErrorKind::ResourceConflict, + Some(ZoneRevision::new(revision)), + None, + RetryClass::Reauthorize, + "guest-target-resource-revision-changed", + ) + } + + fn parse_resource( + &self, + target: &ResourceRef, + canonical: &[u8], + ) -> Result<(ResourceUid, ResourceGeneration), StoreError> { + let envelope = ResourceEnvelope::from_json(canonical).map_err(|_| Self::invalid( + "guest-target-resource-envelope-invalid", + ))?; + let envelope_ref = ResourceRef::new( + envelope.resource_type().clone(), + envelope.metadata().name().clone(), + ); + if &envelope_ref != target || envelope.metadata().zone() != &self.zone { + return Err(Self::invalid("guest-target-resource-identity-mismatch")); + } + if matches!( + target.resource_type().as_str(), + "Process" | "EphemeralProcess" | NIXOS_GENERATION_RESOURCE_TYPE + ) { + let execution_ref = envelope + .spec() + .base() + .get("executionRef") + .and_then(|value| match value { + d2b_contracts_resource::v3::CanonicalJsonValue::String(value) => { + ResourceRef::parse(value).ok() + } + _ => None, + }) + .ok_or_else(|| Self::invalid("guest-target-execution-ref-missing"))?; + if self.target.as_ref() != Some(&execution_ref) { + return Err(Self::invalid("guest-target-execution-ref-mismatch")); + } + } + Ok(( + envelope.metadata().uid().clone(), + envelope.metadata().generation(), + )) + } + + fn validate_mutation_body(&self, body: &MutationSealBody) -> Result<(), StoreError> { + if body.authorization.zone != self.zone { + return Err(Self::invalid("guest-target-authorization-zone-mismatch")); + } + for prepared in &body.mutations { + let mutation = prepared.mutation(); + if mutation.zone != self.zone { + return Err(Self::invalid("guest-target-resource-zone-mismatch")); + } + if !Self::is_target_local_type(mutation.target.resource_type()) { + return Err(Self::forbidden()); + } + if self.target.as_ref().is_some_and(|target| { + !body.authorization.targets.iter().any(|authorization| { + authorization.resource_type == *mutation.target.resource_type() + && authorization + .resource_name + .as_ref() + .is_some_and(|name| name == mutation.target.name()) + && authorization.execution_ref.as_ref() == Some(target) + }) + }) { + return Err(Self::invalid("guest-target-authorization-target-mismatch")); + } + if !matches!( + mutation.target.resource_type().as_str(), + "Process" | "EphemeralProcess" | NIXOS_GENERATION_RESOURCE_TYPE + ) { + continue; + } + if let Some(canonical) = mutation.canonical_resource.as_deref() { + self.parse_resource(&mutation.target, canonical)?; + } + } + Ok(()) + } + + async fn commit_verified_with_fence( + &self, + sealed: SealedMutation, + commit_fence: Option, + ) -> Result { + match &self.backend { + GuestStoreBackend::Durable(store) => { + if let Some(commit_fence) = commit_fence { + store + .commit_verified_with_fence( + sealed, + |body| self.validate_mutation_body(body), + move || commit_fence(), + ) + .await + } else { + store + .commit_verified_with(sealed, |body| self.validate_mutation_body(body)) + .await + } + } + GuestStoreBackend::Memory { acceptor, state } => { + let opened = acceptor.open(sealed)?; + self.validate_mutation_body(opened.body())?; + let body = opened.into_body(); + let mut state = state + .lock() + .map_err(|_| Self::unavailable("guest-target-store-poisoned"))?; + let mut resources = state.resources.clone(); + let next_revision = state + .revision + .checked_add(1) + .ok_or_else(|| Self::unavailable("guest-target-store-revision-exhausted"))?; + let mut changed = Vec::new(); + for prepared in body.mutations { + let mutation = prepared.mutation(); + if mutation.zone != self.zone { + return Err(Self::invalid("guest-target-resource-zone-mismatch")); + } + if !Self::is_target_local_type(mutation.target.resource_type()) { + return Err(Self::forbidden()); + } + let current = resources.get(&mutation.target).cloned(); + match mutation.expected { + ExpectedRevision::CreateAbsent if current.is_some() => { + return Err(StoreError::new( + StoreErrorKind::ResourceAlreadyExists, + Some(ZoneRevision::new(state.revision)), + None, + RetryClass::Never, + "guest-target-resource-already-exists", + )); + } + ExpectedRevision::Exact(expected) + if current + .as_ref() + .is_none_or(|resource| resource.revision != expected) => + { + return Err(Self::conflict(state.revision)); + } + _ => {} + } + if let Some(expected_uid) = mutation.expected_uid.as_ref() + && current + .as_ref() + .is_some_and(|resource| &resource.uid != expected_uid) + { + return Err(Self::conflict(state.revision)); + } + if mutation.kind == ResourceMutationKind::Delete { + let removed = + resources.remove(&mutation.target).ok_or_else(Self::not_found)?; + changed.push(removed); + continue; + } + let canonical = mutation + .canonical_resource + .clone() + .or_else(|| current.as_ref().map(|resource| resource.canonical_json.clone())) + .ok_or_else(|| Self::invalid("guest-target-resource-body-missing"))?; + let (envelope_uid, generation) = self.parse_resource(&mutation.target, &canonical)?; + let uid = prepared + .resource_uid() + .cloned() + .unwrap_or(envelope_uid); + if current + .as_ref() + .is_some_and(|resource| resource.uid != uid) + { + return Err(Self::conflict(state.revision)); + } + let payload_digest = prepared + .payload_digest() + .map(str::to_owned) + .unwrap_or_else(|| { + d2b_contracts_resource::v3::canonical_digest( + d2b_contracts_resource::v3::resource_schema::RESOURCE_ENVELOPE_DOMAIN_TAG, + &canonical, + ) + }); + let resource = StoredResource { + resource_ref: mutation.target.clone(), + zone: self.zone.clone(), + uid, + generation, + revision: ZoneRevision::new(next_revision), + canonical_json: canonical, + payload_digest, + }; + resources.insert(mutation.target.clone(), resource.clone()); + changed.push(resource); + } + state.revision = next_revision; + state.resources = resources; + Ok(StoreCommitResult { + resources: changed, + revision: ZoneRevision::new(next_revision), + }) + } + } + } +} + +impl ResourceStoreBackend for GuestResourceStore { + async fn get(&self, request: StoreGetRequest) -> Result { + if request.zone != self.zone { + return Err(Self::not_found()); + } + if !Self::is_target_local_type(request.target.resource_type()) { + return Err(Self::forbidden()); + } + match &self.backend { + GuestStoreBackend::Durable(store) => store.get(request).await, + GuestStoreBackend::Memory { state, .. } => { + let state = state + .lock() + .map_err(|_| Self::unavailable("guest-target-store-poisoned"))?; + let resource = state + .resources + .get(&request.target) + .ok_or_else(Self::not_found)?; + if request + .expected_uid + .as_ref() + .is_some_and(|uid| uid != &resource.uid) + { + return Err(Self::not_found()); + } + Ok(resource.clone()) + } + } + } + + async fn list(&self, request: StoreListRequest) -> Result { + if request.zone != self.zone { + return Err(Self::not_found()); + } + if request + .resource_types + .iter() + .any(|resource_type| !Self::is_target_local_type(resource_type)) + { + return Err(Self::forbidden()); + } + match &self.backend { + GuestStoreBackend::Durable(store) => store.list(request).await, + GuestStoreBackend::Memory { state, .. } => { + let state = state + .lock() + .map_err(|_| Self::unavailable("guest-target-store-poisoned"))?; + let mut resources = state + .resources + .values() + .filter(|resource| { + (request.resource_types.is_empty() + || request + .resource_types + .iter() + .any(|kind| kind == resource.resource_ref.resource_type())) + && (request.resource_names.is_empty() + || request + .resource_names + .iter() + .any(|name| name == resource.resource_ref.name())) + }) + .cloned() + .collect::>(); + let page_size = usize::try_from(request.page_size).unwrap_or(usize::MAX); + let truncated = resources.len() > page_size; + resources.truncate(page_size); + Ok(StoreListResult { + resources, + snapshot_revision: ZoneRevision::new(state.revision), + next_cursor: None, + truncated, + }) + } + } + } + + async fn watch(&self, request: StoreWatchRequest) -> Result { + if request.zone != self.zone { + return Err(Self::not_found()); + } + if request + .resource_types + .iter() + .any(|resource_type| !Self::is_target_local_type(resource_type)) + { + return Err(Self::forbidden()); + } + match &self.backend { + GuestStoreBackend::Durable(store) => store.watch(request).await, + GuestStoreBackend::Memory { state, .. } => { + let mut state = state + .lock() + .map_err(|_| Self::unavailable("guest-target-store-poisoned"))?; + state.next_watch = state.next_watch.saturating_add(1); + Ok(StoreWatchReceipt { + stream_name: format!("{WATCH_STREAM_PREFIX}-{}", state.next_watch), + snapshot_revision: ZoneRevision::new(state.revision), + }) + } + } + } + + async fn resolve_ref( + &self, + request: StoreResolveRequest, + ) -> Result { + let resource = self + .get(StoreGetRequest { + operation: request.operation, + zone: request.zone, + target: request.target, + expected_uid: request.expected_uid, + projection: d2b_resource_store::StoreProjection::MetadataOnly, + }) + .await?; + Ok(StoreResolvedIdentity { + zone: resource.zone, + resource_ref: resource.resource_ref, + uid: resource.uid, + generation: resource.generation, + revision: resource.revision, + }) + } + + async fn inspect_schema( + &self, + request: StoreInspectSchemaRequest, + ) -> Result { + if request.zone != self.zone { + return Err(Self::not_found()); + } + if !Self::is_target_local_type(&request.resource_type) { + return Err(Self::forbidden()); + } + match &self.backend { + GuestStoreBackend::Durable(store) => store.inspect_schema(request).await, + GuestStoreBackend::Memory { .. } => { + let canonical = d2b_contracts_resource::v3::CanonicalJsonValue::parse(SCHEMA_BYTES) + .map_err(|_| Self::invalid("guest-target-schema-invalid"))? + .to_canonical_bytes(); + Ok(StoredSchema { + resource_type: request.resource_type, + payload_digest: d2b_contracts_resource::v3::canonical_digest( + SCHEMA_DOMAIN_TAG, + &canonical, + ), + canonical_json: canonical, + }) + } + } + } + + async fn commit_verified( + &self, + sealed: SealedMutation, + ) -> Result { + self.commit_verified_with_fence(sealed, None).await + } + +} + +fn open_owned_file(path: &Path) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) + .open(path) + .map_err(|_| GuestResourceRuntimeError::Store)?; + let metadata = file + .metadata() + .map_err(|_| GuestResourceRuntimeError::Store)?; + if !metadata.is_file() || metadata.nlink() != 1 { + return Err(GuestResourceRuntimeError::Store); + } + Ok(file) +} + +fn map_store_error(error: StoreError) -> GuestResourceRuntimeError { + if error.kind() == StoreErrorKind::StoreQuarantined { + GuestResourceRuntimeError::StoreQuarantined + } else { + GuestResourceRuntimeError::Store + } +} + +pub struct SessionBoundStore { + store: Arc, + active_generation: Arc>>, + generation: u64, +} + +impl SessionBoundStore { + fn ensure_current(&self) -> Result<(), StoreError> { + let active = self + .active_generation + .lock() + .map_err(|_| GuestResourceStore::unavailable("guest-target-session-state-poisoned"))?; + if *active != Some(self.generation) { + return Err(GuestResourceStore::unavailable( + "guest-target-session-stale", + )); + } + Ok(()) + } +} + +impl ResourceStoreBackend for SessionBoundStore { + async fn get(&self, request: StoreGetRequest) -> Result { + self.ensure_current()?; + self.store.get(request).await + } + + async fn list(&self, request: StoreListRequest) -> Result { + self.ensure_current()?; + self.store.list(request).await + } + + async fn watch(&self, request: StoreWatchRequest) -> Result { + self.ensure_current()?; + self.store.watch(request).await + } + + async fn resolve_ref( + &self, + request: StoreResolveRequest, + ) -> Result { + self.ensure_current()?; + self.store.resolve_ref(request).await + } + + async fn inspect_schema( + &self, + request: StoreInspectSchemaRequest, + ) -> Result { + self.ensure_current()?; + self.store.inspect_schema(request).await + } + + async fn commit_verified( + &self, + mutation: SealedMutation, + ) -> Result { + self.ensure_current()?; + let active_generation = Arc::clone(&self.active_generation); + let generation = self.generation; + let commit_fence: CommitFence = Arc::new(move || { + let active = active_generation.lock().map_err(|_| { + GuestResourceStore::unavailable("guest-target-session-state-poisoned") + })?; + if *active == Some(generation) { + Ok(()) + } else { + Err(GuestResourceStore::unavailable("guest-target-session-stale")) + } + }); + self.store + .commit_verified_with_fence(mutation, Some(commit_fence)) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use d2b_resource_store::mutation_seal::StoreSealIdentity; + + fn test_identity() -> GuestIdentity { + GuestIdentity::new( + ResourceRef::parse("Guest/work").expect("guest ref"), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").expect("guest uid"), + ZoneId::parse("work").expect("zone"), + crate::guest_mode::BootIdentity::from_kernel_boot_id("u6-test-boot") + .expect("boot identity"), + d2b_contracts_resource::v3::identity::SessionPurpose::parse("zone-link") + .expect("purpose"), + d2b_contracts_resource::v3::SchemaFingerprint::parse( + format!("sha256:{}", "1".repeat(64)), + ) + .expect("schema"), + d2b_contracts_resource::v3::identity::ReconnectGeneration::new(1) + .expect("generation"), + 1, + 1, + 1, + ) + .expect("identity") + } + + #[tokio::test] + async fn target_local_store_is_reopened_from_guest_state() { + let directory = tempfile::tempdir().expect("state directory"); + let identity = test_identity(); + let first = GuestResourceRuntime::new(identity.clone(), directory.path()) + .await + .expect("initial target-local runtime"); + assert!(directory.path().join("resource-store.redb").is_file()); + assert!(directory.path().join("resource-store.marker").is_file()); + drop(first); + + let second = GuestResourceRuntime::new(identity, directory.path()) + .await + .expect("restarted target-local runtime"); + let listed = second + .store + .list(StoreListRequest { + operation: d2b_resource_store::StoreOperationContext { + operation_id: "u6-reopen-list".to_owned(), + idempotency_key: None, + correlation_id: "u6-reopen-list".to_owned(), + trace_id: None, + deadline_ms: 1_000, + }, + zone: ZoneId::parse("work").expect("zone"), + resource_types: Vec::new(), + resource_names: Vec::new(), + filters: Vec::new(), + page_size: 16, + cursor: None, + projection: d2b_resource_store::StoreProjection::MetadataOnly, + }) + .await + .expect("reopened store list"); + assert!(listed.resources.is_empty()); + } + + #[tokio::test] + async fn partial_target_local_store_is_quarantined_without_repair() { + let directory = tempfile::tempdir().expect("state directory"); + File::create(directory.path().join(STORE_FILE_NAME)).expect("database placeholder"); + let error = GuestResourceRuntime::new(test_identity(), directory.path()) + .await + .expect_err("partial store must fail closed"); + assert_eq!(error, GuestResourceRuntimeError::StoreQuarantined); + assert!(!directory.path().join(STORE_MARKER_NAME).exists()); + } + + #[test] + fn target_local_store_rejects_zone_authority_types() { + assert!(GuestResourceStore::is_target_local_type( + &ResourceTypeName::parse("Process").expect("Process type") + )); + assert!(GuestResourceStore::is_target_local_type( + &ResourceTypeName::parse("EphemeralProcess").expect("EphemeralProcess type") + )); + assert!(GuestResourceStore::is_target_local_type( + &ResourceTypeName::parse("Endpoint").expect("Endpoint type") + )); + assert!(GuestResourceStore::is_target_local_type( + &ResourceTypeName::parse(NIXOS_GENERATION_RESOURCE_TYPE) + .expect("NixOS generation type") + )); + assert!(!GuestResourceStore::is_target_local_type( + &ResourceTypeName::parse("Zone").expect("Zone type") + )); + assert!(!GuestResourceStore::is_target_local_type( + &ResourceTypeName::parse("Role").expect("Role type") + )); + } + + #[tokio::test] + async fn target_local_store_rejects_schema_reads_for_zone_types() { + let zone = ZoneId::parse("work").expect("zone"); + let uid = + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").expect("store UID"); + let store_identity = StoreSealIdentity::new( + d2b_resource_store::StoreSlot::new(STORE_SLOT).expect("store slot"), + zone.clone(), + uid, + ); + let (_, acceptor) = + d2b_resource_store::mutation_seal::mutation_seal_pair(store_identity); + let store = GuestResourceStore::new_in_memory(zone.clone(), acceptor); + let error = store + .inspect_schema(StoreInspectSchemaRequest { + operation: d2b_resource_store::StoreOperationContext { + operation_id: "schema".to_owned(), + idempotency_key: None, + correlation_id: "schema".to_owned(), + trace_id: None, + deadline_ms: 1, + }, + zone, + resource_type: ResourceTypeName::parse("Zone").expect("Zone type"), + }) + .await + .expect_err("Zone schema is not target-local"); + assert_eq!(error.kind(), StoreErrorKind::AuthorizationDenied); + } + + #[tokio::test] + async fn target_local_store_rejects_watches_for_zone_types() { + let zone = ZoneId::parse("work").expect("zone"); + let uid = + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").expect("store UID"); + let store_identity = StoreSealIdentity::new( + d2b_resource_store::StoreSlot::new(STORE_SLOT).expect("store slot"), + zone.clone(), + uid, + ); + let (_, acceptor) = + d2b_resource_store::mutation_seal::mutation_seal_pair(store_identity); + let store = GuestResourceStore::new_in_memory(zone.clone(), acceptor); + let error = store + .watch(StoreWatchRequest { + operation: d2b_resource_store::StoreOperationContext { + operation_id: "watch".to_owned(), + idempotency_key: None, + correlation_id: "watch".to_owned(), + trace_id: None, + deadline_ms: 1, + }, + zone, + resource_types: vec![ResourceTypeName::parse("Zone").expect("Zone type")], + resource_names: Vec::new(), + filters: Vec::new(), + after_revision: ZoneRevision::new(0), + initial_credits: 1, + projection: d2b_resource_store::StoreProjection::MetadataOnly, + }) + .await + .expect_err("Zone watch is not target-local"); + assert_eq!(error.kind(), StoreErrorKind::AuthorizationDenied); + } +} diff --git a/packages/d2bd-runtime/src/lib.rs b/packages/d2bd-runtime/src/lib.rs index f6919adaa0..e211e2cf42 100644 --- a/packages/d2bd-runtime/src/lib.rs +++ b/packages/d2bd-runtime/src/lib.rs @@ -26,7 +26,9 @@ pub mod guest_control_health; pub mod guest_control_bridge; pub mod guest_control_runtime; pub mod guest_control_vsock; +pub mod guest_component_session; pub mod guest_mode; +pub mod guest_resource_runtime; pub mod host_mode; pub mod json_io; pub mod kernel_module_check; diff --git a/packages/d2bd-runtime/src/readiness.rs b/packages/d2bd-runtime/src/readiness.rs index b20d991bf0..ed9b1270c9 100644 --- a/packages/d2bd-runtime/src/readiness.rs +++ b/packages/d2bd-runtime/src/readiness.rs @@ -25,7 +25,7 @@ pub fn readiness_predicate_ready(predicate: &ReadinessPredicate) -> Result Ok(tcp_port_ready(host, *port)), ReadinessPredicate::Command(command) => command_ready(command), ReadinessPredicate::ComponentSpecific(_) => Ok(true), - // The authenticated guest-control Health probe is evaluated through a + // The authenticated Guest ComponentSession readiness probe is evaluated through a // daemon-state-aware path (it needs the per-VM vsock socket, peer // credentials, and a broker-backed signer that this stateless helper // cannot reach). The live readiness path intercepts @@ -34,7 +34,7 @@ pub fn readiness_predicate_ready(predicate: &ReadinessPredicate) -> Result { - Err("guest-control-health-needs-state-aware-path".to_owned()) + Err("guest-component-session-needs-state-aware-path".to_owned()) } } } diff --git a/packages/d2bd-runtime/src/resource_api.rs b/packages/d2bd-runtime/src/resource_api.rs index a46983f363..a542363e6a 100644 --- a/packages/d2bd-runtime/src/resource_api.rs +++ b/packages/d2bd-runtime/src/resource_api.rs @@ -208,6 +208,12 @@ pub fn route_service_matches( .ok_or(ResourceRuntimeError::RequestInvalid)?; Ok(match method { "ZoneList" | "ZoneStatus" => service == "d2b.zone.v3", + "ConfigNixosService/ReadGuestConfig" + | "ConfigNixosService/Stage" + | "ConfigNixosService/Diff" + | "ConfigNixosService/Approve" + | "ConfigNixosService/Reject" + | "ConfigNixosService/Status" => service == "d2b.config-nixos.v3", _ => service == "d2b.resource.v3", }) } diff --git a/packages/d2bd-runtime/src/vm_start_support.rs b/packages/d2bd-runtime/src/vm_start_support.rs index 666c13dfcc..b11d65fb63 100644 --- a/packages/d2bd-runtime/src/vm_start_support.rs +++ b/packages/d2bd-runtime/src/vm_start_support.rs @@ -20,6 +20,10 @@ pub fn vm_start_node_mode(role: &ProcessRole) -> VmStartNodeMode { VmStartNodeMode::LongLived(RunnerRole::CloudHypervisor) } ProcessRole::QemuMediaRunner => VmStartNodeMode::LongLived(RunnerRole::QemuMedia), + // Activation runners are target-local EphemeralProcess resources. + // Their bundle row is used for resource-ticket resolution, but they + // are never started as part of VM boot. + ProcessRole::ActivationNixosRunner => VmStartNodeMode::ReadinessOnly, ProcessRole::Gpu | ProcessRole::GpuRenderNode => { VmStartNodeMode::LongLived(RunnerRole::Gpu) } diff --git a/packages/d2bd/BUILD.bazel b/packages/d2bd/BUILD.bazel index a73d2ffd26..1bbac24c76 100644 --- a/packages/d2bd/BUILD.bazel +++ b/packages/d2bd/BUILD.bazel @@ -38,6 +38,7 @@ d2bd_shared_deps = [ "//packages/d2b-process-conformance:d2b_process_conformance", "//packages/d2b-provider:d2b_provider", "//packages/d2b-provider-activation-nixos:d2b_provider_activation_nixos", + "//packages/d2b-provider-config-nixos:d2b_provider_config_nixos", "//packages/d2b-provider-audio-pipewire:d2b_provider_audio_pipewire", "//packages/d2b-provider-clipboard-wayland:d2b_provider_clipboard_wayland", "//packages/d2b-provider-device-security-key:d2b_provider_device_security_key", diff --git a/packages/d2bd/Cargo.toml b/packages/d2bd/Cargo.toml index e2fda6a08b..ca83232ad7 100644 --- a/packages/d2bd/Cargo.toml +++ b/packages/d2bd/Cargo.toml @@ -28,6 +28,7 @@ d2b-provider-system-minijail = { path = "../d2b-provider-system-minijail", versi d2b-provider-system-systemd = { path = "../d2b-provider-system-systemd", version = "0.0.0-bootstrap" } d2b-provider-supervisor = { path = "../d2b-provider-supervisor", version = "0.0.0-bootstrap" } d2b-provider-activation-nixos = { path = "../d2b-provider-activation-nixos", version = "0.0.0-bootstrap" } +d2b-provider-config-nixos = { path = "../d2b-provider-config-nixos", version = "0.0.0-bootstrap" } d2b-provider-volume-local = { path = "../d2b-provider-volume-local", version = "0.0.0-bootstrap" } d2b-process = { path = "../d2b-process", version = "0.0.0-bootstrap" } d2b-process-conformance = { path = "../d2b-process-conformance", version = "0.0.0-bootstrap" } diff --git a/packages/d2bd/src/activation_resource_runtime.rs b/packages/d2bd/src/activation_resource_runtime.rs index 6f16437097..faf8600adb 100644 --- a/packages/d2bd/src/activation_resource_runtime.rs +++ b/packages/d2bd/src/activation_resource_runtime.rs @@ -3,7 +3,7 @@ //! The activation Provider is a fixed daemon composition. This module owns //! only the Zone-scoped durable-resource adapter: it relists and watches //! generation rows, applies the pure activation policy, and routes target -//! effects through the existing broker and guest-control boundaries. +//! effects through the existing broker and Guest ComponentSession boundaries. use std::{ collections::{BTreeMap, BTreeSet}, @@ -18,9 +18,6 @@ use d2b_contracts_broker::host_generation::{ ApplyHostGenerationHandoff, HandoffCallerRole, HandoffState, HostGenerationHandoffIntent, SourceGenerationCompatibilityFloorV1, target_fingerprint, }; -use d2b_contracts_control::public_wire::{ - self, MutatingVerbOutcome, MutatingVerbResponse, MutationFlags, -}; use d2b_contracts_resource::resource_proto as wire; use d2b_contracts_resource::v3::{ ActivationDetail, @@ -32,11 +29,13 @@ use d2b_contracts_resource::v3::{ ResourcePhase, ResourceRef, ResourceTypeName, + ResourceUid, ZoneId, ZoneRevision, }; use d2b_provider_activation_nixos::{ - ActivationCaller, ActivationController, CallerRole, GenerationObservation, GenerationPhase, + activation_runner_ref, activation_runner_spec, ActivationCaller, ActivationController, + CallerRole, GenerationObservation, GenerationPhase, }; use d2b_resource_api::watch::ResourceWatch; use d2b_resource_api::{RedbBackend, ResourceApiClient, service::UnavailableUpgradeDispatcher}; @@ -46,10 +45,116 @@ use d2b_resource_store::{ use d2b_resource_store_redb::RedbResourceStore; use crate::{ - BrokerActivationMode, ServerState, dispatch_broker_activation_metadata_only, - dispatch_broker_request_as, dispatch_live_guest_activation, + ServerState, dispatch_broker_request_as, }; +#[async_trait::async_trait] +pub(crate) trait ActivationResourceClient: Send + Sync { + async fn create( + &self, + request: wire::CreateRequest, + ) -> Result; + + async fn update_status( + &self, + request: wire::UpdateStatusRequest, + ) -> Result; + + async fn update_finalizers( + &self, + request: wire::UpdateFinalizersRequest, + ) -> Result; + + async fn delete(&self, request: wire::DeleteRequest) -> Result; +} + +#[async_trait::async_trait] +impl ActivationResourceClient + for ResourceApiClient +{ + async fn create( + &self, + request: wire::CreateRequest, + ) -> Result { + Ok(ResourceApiClient::create(self, request).await) + } + + async fn update_status( + &self, + request: wire::UpdateStatusRequest, + ) -> Result { + Ok(ResourceApiClient::update_status(self, request).await) + } + + async fn update_finalizers( + &self, + request: wire::UpdateFinalizersRequest, + ) -> Result { + Ok(ResourceApiClient::update_finalizers(self, request).await) + } + + async fn delete(&self, request: wire::DeleteRequest) -> Result { + Ok(ResourceApiClient::delete(self, request).await) + } +} + +/// Resource API adapter for one authenticated Guest ComponentSession. +pub(crate) struct GuestActivationResourceClient { + session: Arc, +} + +impl GuestActivationResourceClient { + pub(crate) fn new( + session: Arc, + ) -> Self { + Self { session } + } +} + +#[async_trait::async_trait] +impl ActivationResourceClient for GuestActivationResourceClient { + async fn create( + &self, + request: wire::CreateRequest, + ) -> Result { + self.session + .resource_service_client() + .create(ttrpc::context::Context::default(), &request) + .await + .map_err(|_| ()) + } + + async fn update_status( + &self, + request: wire::UpdateStatusRequest, + ) -> Result { + self.session + .resource_service_client() + .update_status(ttrpc::context::Context::default(), &request) + .await + .map_err(|_| ()) + } + + async fn update_finalizers( + &self, + request: wire::UpdateFinalizersRequest, + ) -> Result { + self.session + .resource_service_client() + .update_finalizers(ttrpc::context::Context::default(), &request) + .await + .map_err(|_| ()) + } + + async fn delete(&self, request: wire::DeleteRequest) -> Result { + self.session + .resource_service_client() + .delete(ttrpc::context::Context::default(), &request) + .await + .map_err(|_| ()) + } +} + const ACTIVATION_TYPE: &str = "activation-nixos.d2bus.org.NixosGeneration"; const ACTIVATION_FINALIZER: &str = "activation-nixos.d2bus.org/cleanup"; const RETAINED_GENERATIONS: usize = 3; @@ -84,6 +189,12 @@ struct DesiredRecord { ordinal: u64, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RunnerObservation { + phase: ResourcePhase, + outcome: Option, +} + impl DesiredRecord { fn key(&self) -> ResourceRef { self.resource.resource_ref.clone() @@ -121,7 +232,8 @@ pub(crate) struct ActivationResourceRuntime { zone: ZoneId, controller: ActivationController, records: BTreeMap, - status_client: Option>>, + status_client: Option>, + guest_clients: BTreeMap>, } impl core::fmt::Debug for ActivationResourceRuntime { @@ -142,14 +254,40 @@ impl ActivationResourceRuntime { controller: ActivationController::new(RETAINED_GENERATIONS), records: BTreeMap::new(), status_client: None, + guest_clients: BTreeMap::new(), } } - pub(crate) fn set_status_client( + pub(crate) fn set_status_client(&mut self, status_client: Arc) + where + C: ActivationResourceClient + 'static, + { + self.status_client = Some(status_client); + } + + pub(crate) fn set_guest_client( &mut self, - status_client: Arc>, + guest: impl Into, + client: Arc, ) { - self.status_client = Some(status_client); + self.guest_clients.insert(guest.into(), client); + } + + pub(crate) fn clear_guest_clients(&mut self) { + self.guest_clients.clear(); + } + + fn runner_client( + &self, + execution_ref: &ResourceRef, + ) -> Option> { + if execution_ref.resource_type().as_str() == "Guest" { + self.guest_clients + .get(execution_ref.name().as_str()) + .cloned() + } else { + self.status_client.clone() + } } /// Reconcile a complete durable activation snapshot. @@ -157,6 +295,7 @@ impl ActivationResourceRuntime { &mut self, state: Arc, snapshot: Vec, + process_snapshot: Vec, ) -> Result<(), ActivationResourceRuntimeError> { let desired = decode_snapshot(&self.zone, snapshot)?; let desired_keys = desired.keys().cloned().collect::>(); @@ -192,6 +331,22 @@ impl ActivationResourceRuntime { } if record.deletion_requested() { + if let Some(child) = find_runner_resource(&record, &process_snapshot) { + if !matches!( + status_phase(child).unwrap_or(ResourcePhase::Pending), + ResourcePhase::Deleted + ) { + if metadata_value(child, "deletionRequestedAt") + .is_none_or(|value| matches!(value, CanonicalJsonValue::Null)) + { + if let Some(client) = self.runner_client(record.spec.execution_ref()) { + self.request_delete_stored(client.as_ref(), child).await?; + } + } + self.records.insert(key, record); + continue; + } + } record = self .publish_status( &record, @@ -248,25 +403,72 @@ impl ActivationResourceRuntime { continue; } - record = self - .publish_status( - &record, - ResourcePhase::Pending, - ActivationDetail::Staged, - None, - ) - .await?; - record = self - .publish_status( - &record, - ResourcePhase::Pending, - ActivationDetail::Applying, - None, - ) - .await?; - let request = planned.runner_requests()[0].clone(); - let outcome = self.execute_runner(&state, &record, &request, &prior).await; + if request.execution_ref.resource_type().as_str() == "Host" { + let outcome = self.execute_runner(&state, &record, &request, &prior).await; + let applied = self + .controller + .apply_runner_result(&record.spec, outcome, observed) + .map_err(|_| ActivationResourceRuntimeError::Policy)?; + let detail = + activation_detail(record.spec.activation_mode(), outcome, applied.phase()); + record = self + .publish_status( + &record, + applied.phase(), + detail, + applied.audit_codes().first().copied(), + ) + .await?; + self.records.insert(key, record); + continue; + } + let runner = find_runner_observation(&record, &process_snapshot); + if runner.is_none() { + record = self + .publish_status( + &record, + ResourcePhase::Pending, + ActivationDetail::Staged, + None, + ) + .await?; + self.create_runner(&record, &request).await?; + self.records.insert(key, record); + continue; + } + let runner = runner.expect("checked above"); + if runner.phase == ResourcePhase::Pending { + record = self + .publish_status( + &record, + ResourcePhase::Pending, + ActivationDetail::Staged, + None, + ) + .await?; + self.records.insert(key, record); + continue; + } + if runner.phase == ResourcePhase::Ready { + record = self + .publish_status( + &record, + ResourcePhase::Pending, + ActivationDetail::Applying, + None, + ) + .await?; + self.records.insert(key, record); + continue; + } + let outcome = runner.outcome.unwrap_or_else(|| { + if runner.phase == ResourcePhase::Succeeded { + ActivationOutcomeCode::Succeeded + } else { + ActivationOutcomeCode::HelperFailed + } + }); let applied = self .controller .apply_runner_result(&record.spec, outcome, observed) @@ -301,7 +503,6 @@ impl ActivationResourceRuntime { "Host" => self .execute_host_handoff(state, record, prior) .unwrap_or(ActivationOutcomeCode::HelperFailed), - "Guest" => self.execute_guest_activation(state, record, request), _ => ActivationOutcomeCode::TargetMismatch, } } @@ -361,68 +562,6 @@ impl ActivationResourceRuntime { } } - fn execute_guest_activation( - &self, - state: &ServerState, - record: &DesiredRecord, - request: &d2b_provider_activation_nixos::RunnerRequest, - ) -> ActivationOutcomeCode { - let mode = match record.spec.activation_mode() { - ActivationMode::Switch => BrokerActivationMode::Switch, - ActivationMode::Boot => BrokerActivationMode::Boot, - ActivationMode::Test => BrokerActivationMode::Test, - ActivationMode::Adopt => return ActivationOutcomeCode::Adopted, - }; - let verb = match mode { - BrokerActivationMode::Switch => "switch", - BrokerActivationMode::Boot => "boot", - BrokerActivationMode::Test => "test", - BrokerActivationMode::Rollback => "rollback", - }; - let activation_request = public_wire::ActivationRequest { - vm: record.spec.execution_ref().name().as_str().to_owned(), - flags: MutationFlags { - apply: true, - ..MutationFlags::default() - }, - }; - let response = if mode == BrokerActivationMode::Boot { - dispatch_broker_activation_metadata_only( - state, - activation_request, - verb, - mode, - BrokerCallerRole::AdminUid { - uid: state.daemon_uid, - }, - Some(request.system_artifact_id.clone()), - ) - } else { - dispatch_live_guest_activation( - state, - activation_request, - verb, - mode, - BrokerCallerRole::AdminUid { - uid: state.daemon_uid, - }, - Some(request.system_artifact_id.clone()), - ) - }; - match response { - Ok(value) => match serde_json::from_value::(value) { - Ok(response) if response.outcome == MutatingVerbOutcome::Applied => { - ActivationOutcomeCode::Succeeded - } - Ok(response) if response.outcome == MutatingVerbOutcome::InvalidRequest => { - ActivationOutcomeCode::TargetMismatch - } - _ => ActivationOutcomeCode::HelperFailed, - }, - Err(_) => ActivationOutcomeCode::HelperFailed, - } - } - async fn apply_retention(&self) -> Result<(), ActivationResourceRuntimeError> { let observations = self .records @@ -461,7 +600,7 @@ impl ActivationResourceRuntime { let Some(client) = &self.status_client else { return Ok(record.clone()); }; - update_status(client, record, phase, detail, outcome).await + update_status(client.as_ref(), record, phase, detail, outcome).await } async fn ensure_finalizer( @@ -471,7 +610,7 @@ impl ActivationResourceRuntime { let Some(client) = &self.status_client else { return Ok(record.clone()); }; - update_finalizers(client, record, true).await + update_finalizers(client.as_ref(), record, true).await } async fn remove_finalizer( @@ -484,7 +623,7 @@ impl ActivationResourceRuntime { if !record.has_finalizer() { return Ok(record.clone()); } - update_finalizers(client, record, false).await + update_finalizers(client.as_ref(), record, false).await } async fn request_delete( @@ -494,7 +633,119 @@ impl ActivationResourceRuntime { let Some(client) = &self.status_client else { return Ok(()); }; - delete_resource(client, record).await + delete_resource(client.as_ref(), record).await + } + + async fn create_runner( + &self, + record: &DesiredRecord, + request: &d2b_provider_activation_nixos::RunnerRequest, + ) -> Result<(), ActivationResourceRuntimeError> { + let Some(client) = self.runner_client(&request.execution_ref) else { + return Ok(()); + }; + let mut spec = serde_json::to_value(activation_runner_spec(request)) + .map_err(|_| ActivationResourceRuntimeError::InvalidResource)?; + let spec_object = spec + .as_object_mut() + .ok_or(ActivationResourceRuntimeError::InvalidResource)?; + spec_object.insert( + "providerRef".to_owned(), + serde_json::json!("Provider/system-minijail"), + ); + let child_ref = activation_runner_ref(&record.key()); + let payload = serde_json::json!({ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { + "configurationGeneration": record.resource.generation.get(), + "createdAt": "1970-01-01T00:00:00.000Z", + "deletionRequestedAt": null, + "finalizers": [], + "generation": 1, + "managedBy": "controller", + "name": child_ref.name().as_str(), + "ownerRef": record.key().to_canonical_string(), + "revision": 1, + "updatedAt": "1970-01-01T00:00:00.000Z", + "zone": self.zone.as_str() + }, + "spec": spec, + "status": { + "completedAt": null, + "conditions": [], + "lastReconciledAt": null, + "observedGeneration": 0, + "outcome": null, + "phase": "Pending", + "resource": {}, + "startedAt": null, + "update": { + "dependencies": {"count": 0, "refs": []}, + "disruption": "None", + "lastAssessedAt": null, + "operationId": null, + "owned": {"count": 0, "refs": []}, + "preserveState": true, + "reasons": [], + "state": "Unknown", + "targetGeneration": 1 + } + }, + "type": "EphemeralProcess" + }); + let canonical = CanonicalJsonValue::parse( + &serde_json::to_vec(&payload) + .map_err(|_| ActivationResourceRuntimeError::InvalidResource)?, + ) + .map_err(|_| ActivationResourceRuntimeError::InvalidResource)? + .to_canonical_bytes(); + let mut identity = wire::ResourceIdentity::new(); + identity.zone = self.zone.as_str().to_owned(); + identity.resource_type = "EphemeralProcess".to_owned(); + identity.name = child_ref.name().as_str().to_owned(); + let mut body = wire::ResourceEnvelopeBytes::new(); + body.identity = protobuf::MessageField::some(identity.clone()); + body.payload_digest = d2b_contracts_resource::v3::canonical_digest( + d2b_contracts_resource::v3::RESOURCE_ENVELOPE_DOMAIN_TAG, + &canonical, + ); + body.canonical_json = canonical; + let mut precondition = wire::Precondition::new(); + precondition.kind = + protobuf::EnumOrUnknown::new(wire::PreconditionKind::PRECONDITION_KIND_CREATE_ABSENT); + let mut mutation = wire::Mutation::new(); + mutation.kind = protobuf::EnumOrUnknown::new(wire::MutationKind::MUTATION_KIND_CREATE); + mutation.target = protobuf::MessageField::some(identity); + mutation.precondition = protobuf::MessageField::some(precondition); + mutation.resource = protobuf::MessageField::some(body); + let operation = format!( + "activation-runner-create-{}-{}", + child_ref.to_canonical_string(), + record.resource.uid.as_str() + ); + let mut request_wire = wire::CreateRequest::new(); + request_wire.meta = protobuf::MessageField::some(request_meta(&operation)); + request_wire.mutation = protobuf::MessageField::some(mutation); + let response = client + .create(request_wire) + .await + .map_err(|_| ActivationResourceRuntimeError::Store)?; + if let Some(error) = response.error.as_ref() { + if error.kind.enum_value().ok() + != Some(wire::ResourceErrorKind::RESOURCE_ERROR_KIND_RESOURCE_ALREADY_EXISTS) + { + return Err(ActivationResourceRuntimeError::Store); + } + } + Ok(()) + } + + async fn request_delete_stored( + &self, + client: &dyn ActivationResourceClient, + resource: &StoredResource, + ) -> Result<(), ActivationResourceRuntimeError> { + delete_stored_resource(client, resource).await } } @@ -584,6 +835,68 @@ fn status_phase(resource: &StoredResource) -> Option { } } +fn find_runner_resource<'a>( + record: &DesiredRecord, + process_snapshot: &'a [StoredResource], +) -> Option<&'a StoredResource> { + let expected = activation_runner_ref(&record.key()); + process_snapshot.iter().find(|resource| { + resource.zone == record.resource.zone + && resource.resource_ref == expected + && resource_execution_ref(resource).as_ref() == Some(record.spec.execution_ref()) + && metadata_value(resource, "ownerRef").is_some_and(|owner| { + matches!( + owner, + CanonicalJsonValue::String(value) + if value == record.key().to_canonical_string() + ) + }) + }) +} + +fn resource_execution_ref(resource: &StoredResource) -> Option { + let envelope = ResourceEnvelope::from_json(&resource.canonical_json).ok()?; + match envelope.spec().base().get("executionRef") { + Some(CanonicalJsonValue::String(value)) => ResourceRef::parse(value).ok(), + _ => None, + } +} + +fn find_runner_observation( + record: &DesiredRecord, + process_snapshot: &[StoredResource], +) -> Option { + let resource = find_runner_resource(record, process_snapshot)?; + Some(RunnerObservation { + phase: status_phase(resource).unwrap_or(ResourcePhase::Pending), + outcome: status_outcome(resource), + }) +} + +fn status_outcome(resource: &StoredResource) -> Option { + let value = CanonicalJsonValue::parse(&resource.canonical_json).ok()?; + let CanonicalJsonValue::Object(root) = value else { + return None; + }; + let CanonicalJsonValue::Object(status) = root.get("status")? else { + return None; + }; + let CanonicalJsonValue::Object(outcome) = status.get("outcome")? else { + return None; + }; + let CanonicalJsonValue::String(code) = outcome.get("code")? else { + return None; + }; + match code.as_str() { + "succeeded" | "process-exited" => Some(ActivationOutcomeCode::Succeeded), + "runtime-deadline" => Some(ActivationOutcomeCode::HelperFailed), + "adopted" => Some(ActivationOutcomeCode::Adopted), + "target-mismatch" => Some(ActivationOutcomeCode::TargetMismatch), + "helper-refused" => Some(ActivationOutcomeCode::HelperRefused), + _ => None, + } +} + fn ordinal_from_resource(resource: &StoredResource) -> u64 { resource .resource_ref @@ -706,7 +1019,7 @@ fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64) { } async fn update_status( - client: &ResourceApiClient, + client: &dyn ActivationResourceClient, record: &DesiredRecord, phase: ResourcePhase, detail: ActivationDetail, @@ -736,7 +1049,10 @@ async fn update_status( let mut request = wire::UpdateStatusRequest::new(); request.meta = protobuf::MessageField::some(request_meta(&operation)); request.mutation = protobuf::MessageField::some(mutation); - let response = client.update_status(request).await; + let response = client + .update_status(request) + .await + .map_err(|_| ActivationResourceRuntimeError::Store)?; if response.error.is_some() { return Err(ActivationResourceRuntimeError::Store); } @@ -838,7 +1154,7 @@ fn activation_outcome_message(outcome: ActivationOutcomeCode) -> &'static str { } async fn update_finalizers( - client: &ResourceApiClient, + client: &dyn ActivationResourceClient, record: &DesiredRecord, add: bool, ) -> Result { @@ -865,7 +1181,10 @@ async fn update_finalizers( let mut request = wire::UpdateFinalizersRequest::new(); request.meta = protobuf::MessageField::some(request_meta(&operation)); request.mutation = protobuf::MessageField::some(mutation); - let response = client.update_finalizers(request).await; + let response = client + .update_finalizers(request) + .await + .map_err(|_| ActivationResourceRuntimeError::Store)?; if response.error.is_some() { return Err(ActivationResourceRuntimeError::Store); } @@ -881,7 +1200,7 @@ async fn update_finalizers( } async fn delete_resource( - client: &ResourceApiClient, + client: &dyn ActivationResourceClient, record: &DesiredRecord, ) -> Result<(), ActivationResourceRuntimeError> { let mut mutation = wire::Mutation::new(); @@ -896,7 +1215,36 @@ async fn delete_resource( let mut request = wire::DeleteRequest::new(); request.meta = protobuf::MessageField::some(request_meta(&operation)); request.mutation = protobuf::MessageField::some(mutation); - let response = client.delete(request).await; + let response = client + .delete(request) + .await + .map_err(|_| ActivationResourceRuntimeError::Store)?; + if response.error.is_some() { + return Err(ActivationResourceRuntimeError::Store); + } + Ok(()) +} + +async fn delete_stored_resource( + client: &dyn ActivationResourceClient, + resource: &StoredResource, +) -> Result<(), ActivationResourceRuntimeError> { + let mut mutation = wire::Mutation::new(); + mutation.kind = protobuf::EnumOrUnknown::new(wire::MutationKind::MUTATION_KIND_DELETE); + mutation.target = protobuf::MessageField::some(stored_resource_identity(resource)); + mutation.precondition = protobuf::MessageField::some(stored_exact_precondition(resource)); + let operation = format!( + "activation-runner-delete-{}-{}", + resource.resource_ref.to_canonical_string(), + resource.revision.get() + ); + let mut request = wire::DeleteRequest::new(); + request.meta = protobuf::MessageField::some(request_meta(&operation)); + request.mutation = protobuf::MessageField::some(mutation); + let response = client + .delete(request) + .await + .map_err(|_| ActivationResourceRuntimeError::Store)?; if response.error.is_some() { return Err(ActivationResourceRuntimeError::Store); } @@ -927,6 +1275,48 @@ fn exact_precondition(record: &DesiredRecord) -> wire::Precondition { precondition } +fn stored_resource_identity(resource: &StoredResource) -> wire::ResourceIdentity { + let mut identity = wire::ResourceIdentity::new(); + identity.zone = resource.zone.to_canonical_string(); + identity.resource_type = resource.resource_ref.resource_type().to_canonical_string(); + identity.name = resource.resource_ref.name().to_canonical_string(); + identity.uid = Some(resource.uid.as_str().to_owned()); + identity.generation = Some(resource.generation.get()); + identity.revision = Some(resource.revision.get()); + identity +} + +pub(crate) fn stored_resource_from_wire( + resource: &wire::ResourceEnvelopeBytes, +) -> Option { + let identity = resource.identity.as_ref()?; + let uid = ResourceUid::parse(identity.uid.as_deref()?).ok()?; + let generation = d2b_contracts_resource::v3::ResourceGeneration::new(identity.generation?) + .ok()?; + let revision = ZoneRevision::new(identity.revision?); + let zone = ZoneId::parse(&identity.zone).ok()?; + let resource_ref_text = format!("{}/{}", identity.resource_type, identity.name); + let resource_ref = ResourceRef::parse(&resource_ref_text).ok()?; + Some(StoredResource { + resource_ref, + zone, + uid, + generation, + revision, + canonical_json: resource.canonical_json.clone(), + payload_digest: resource.payload_digest.clone(), + }) +} + +fn stored_exact_precondition(resource: &StoredResource) -> wire::Precondition { + let mut precondition = wire::Precondition::new(); + precondition.kind = + protobuf::EnumOrUnknown::new(wire::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION); + precondition.expected_revision = Some(resource.revision.get()); + precondition.expected_uid = Some(resource.uid.as_str().to_owned()); + precondition +} + fn request_meta(operation: &str) -> wire::RequestMeta { let mut meta = wire::RequestMeta::new(); meta.operation_id = operation.to_owned(); @@ -1027,6 +1417,11 @@ pub(crate) async fn run_activation_watch( let Ok(snapshot) = list_activation_snapshot(&store, &zone).await else { continue; }; + let Ok(process_snapshot) = + crate::process_resource_runtime::list_process_snapshot(&store, &zone).await + else { + continue; + }; let runtime = match registry.lock() { Ok(mut guard) => guard.take(), Err(_) => return, @@ -1034,7 +1429,30 @@ pub(crate) async fn run_activation_watch( let Some(mut runtime) = runtime else { continue; }; - let result = runtime.reconcile(Arc::clone(&state), snapshot).await; + runtime.clear_guest_clients(); + let mut process_snapshot = process_snapshot; + for guest in crate::resource_runtime::guest_activation_targets(&snapshot) { + let Ok(session) = crate::connect_guest_component_session(&state, &guest).await else { + continue; + }; + match crate::resource_runtime::list_guest_process_snapshot(&session, &zone, &guest) + .await + { + Ok(resources) => { + runtime.set_guest_client( + guest.clone(), + Arc::new(GuestActivationResourceClient::new(Arc::clone(&session))), + ); + process_snapshot.extend(resources); + } + Err(()) => { + crate::invalidate_guest_component_session(&state, &guest).await; + } + } + } + let result = runtime + .reconcile(Arc::clone(&state), snapshot, process_snapshot) + .await; if let Ok(mut guard) = registry.lock() { *guard = Some(runtime); } else { @@ -1099,4 +1517,25 @@ mod tests { ActivationDetail::Applied ); } + + #[test] + fn activation_runtime_deadline_is_not_success() { + let resource = StoredResource { + resource_ref: ResourceRef::parse("EphemeralProcess/activation").expect("ref"), + zone: ZoneId::parse("dev").expect("zone"), + uid: d2b_contracts_resource::v3::ResourceUid::parse( + "123e4567-e89b-42d3-a456-426614174000", + ) + .expect("uid"), + generation: d2b_contracts_resource::v3::ResourceGeneration::new(1) + .expect("generation"), + revision: ZoneRevision::new(1), + canonical_json: br#"{"status":{"outcome":{"code":"runtime-deadline"}}}"#.to_vec(), + payload_digest: "sha256:".to_owned(), + }; + assert_eq!( + status_outcome(&resource), + Some(ActivationOutcomeCode::HelperFailed) + ); + } } diff --git a/packages/d2bd/src/composition.rs b/packages/d2bd/src/composition.rs index 253c782704..218317d164 100644 --- a/packages/d2bd/src/composition.rs +++ b/packages/d2bd/src/composition.rs @@ -3,9 +3,12 @@ // hundreds of call sites; the size trade-off is intentional and tracked // in plan.md §D-typed-error-boxing. Suppressed until that refactor lands. use std::collections::{BTreeMap, BTreeSet, HashMap, hash_map::Entry}; +#[cfg(test)] use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; -use std::io::{self, Read, Write}; +use std::io::{self, Read}; +#[cfg(test)] +use std::io::Write; use std::os::fd::{AsFd, AsRawFd, OwnedFd, RawFd}; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::os::unix::net::UnixStream; @@ -86,6 +89,11 @@ use d2b_contracts_resource::v3::{ process::ProcessSpec, volume::{VolumeAttachment, VolumeSpec}, }; +#[cfg(not(test))] +use d2b_contracts_resource::v3::{ + ResourceName, + activation_nixos::NIXOS_GENERATION_RESOURCE_TYPE, +}; use d2b_contracts_resource::v3::identity::ReconnectGeneration; use d2b_core::bundle::Bundle; use d2b_core::bundle_resolver::{ @@ -131,6 +139,7 @@ use d2b_provider_runtime_azure_container_apps::gateway::{ use d2b_provider_transport_azure_relay::auth::{DEFAULT_SAS_TTL_SECS, RelayEndpoint}; use d2b_provider_volume_local::diagnostics::storage_lifecycle; use d2b_realm_core::TargetName; +use d2b_process_conformance::{ConfigurationDigest, GuestExecutionBinding}; pub(crate) use d2bd_runtime::broker_transport::{ broker_remaining_before_op, broker_response_kind, default_audit_join_context, dispatch_broker_request_to_socket, redact_broker_dispatch_failure_for_launcher, @@ -547,6 +556,22 @@ struct ServerState { /// reconciliation, force-stop generations, and activation staging all /// resolve through this index; no process-global lock carries that state. zone_coordinator: Arc>, + /// Daemon-owned typed Guest configuration staging state. + config_staging: + Arc>, + /// One shared authenticated ComponentSession per Guest target. Typed + /// health, activation, and Resource API callers must not race by opening + /// independent sessions with the same reconnect generation. + guest_component_sessions: Arc< + tokio::sync::Mutex< + HashMap< + String, + Arc, + >, + >, + >, + guest_component_session_locks: + Arc>>>>, /// Per-VM console session table (ring buffers and drainer tasks) for /// `d2b console `. Sessions are created on first Attach and persist /// until the daemon restarts or the VM stops. @@ -946,6 +971,9 @@ pub async fn serve(options: ServeOptions) -> Result<(), TypedError> { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -1467,6 +1495,8 @@ pub struct GuestServeOptions { pub broker_socket_path: PathBuf, pub broker_uid: u32, pub state_dir: PathBuf, + pub bundle_path: PathBuf, + pub guest_config_path: PathBuf, pub boot_id_path: PathBuf, pub local_private_key_path: Option, pub parent_public_key_path: Option, @@ -1538,21 +1568,36 @@ pub async fn serve_guest(options: GuestServeOptions) -> Result<(), TypedError> { .map_err(|error| TypedError::InternalConfig { detail: error.to_string(), })?; + if options.validate_only { + return Ok(()); + } + fs::create_dir_all(&options.state_dir).map_err(|_| TypedError::InternalConfig { + detail: "guest state root unavailable".to_owned(), + })?; let runtime = d2bd_runtime::guest_mode::GuestRuntime::new( - identity, - options.broker_socket_path, + identity.clone(), + options.broker_socket_path.clone(), options.broker_uid, d2bd_runtime::target_runtime::AdmissionLimits::guest_default(), + &options.state_dir, ) + .await .map_err(|error| TypedError::InternalConfig { detail: error.to_string(), })?; - if options.validate_only { - return Ok(()); - } - fs::create_dir_all(&options.state_dir).map_err(|_| TypedError::InternalConfig { - detail: "guest state root unavailable".to_owned(), + let bundle = BundleResolver::load(&options.bundle_path).map_err(|_| { + TypedError::InternalConfig { + detail: "guest process bundle unavailable".to_owned(), + } })?; + let process_providers = Arc::new(process_provider_runtime::ProductionProcessProviders::new_for_mode( + bundle, + options.broker_socket_path.clone(), + BrokerCallerRole::LauncherUid { + uid: options.broker_uid, + }, + d2bd_runtime::target_runtime::DaemonMode::Guest, + )); let local_private_path = options .local_private_key_path .ok_or_else(|| TypedError::InternalConfig { @@ -1577,43 +1622,242 @@ pub async fn serve_guest(options: GuestServeOptions) -> Result<(), TypedError> { context: "install Guest SIGINT handler".to_owned(), detail: "signal handler unavailable".to_owned(), })?; + let mut active: Option<( + tokio::task::JoinHandle>, + tokio::task::JoinHandle<()>, + d2bd_runtime::guest_mode::GuestSessionLease, + )> = None; loop { let local_private = read_secret32(&local_private_path)?; - let accepted = tokio::select! { - _ = sigterm.recv() => break, - _ = sigint.recv() => break, - result = runtime.accept_from_parent(&mut listener, local_private, parent_public) => result, - }; - let (mut session, lease) = match accepted { - Ok(session) => session, - Err(error) => { - if options.once { - return Err(TypedError::InternalConfig { - detail: error.to_string(), - }); - } - tracing::warn!(error = %error, "Guest ComponentSession handshake refused"); - continue; - } - }; - if options.once { - drop(lease); - return Ok(()); - } - let shutdown = loop { + if let Some((mut serving, processing, lease)) = active.take() { tokio::select! { - _ = sigterm.recv() => break true, - _ = sigint.recv() => break true, - result = session.receive_ttrpc() => { - if result.is_err() { - break false; + _ = sigterm.recv() => { + serving.abort(); + let _ = serving.await; + processing.abort(); + let _ = processing.await; + drop(lease); + break; + }, + _ = sigint.recv() => { + serving.abort(); + let _ = serving.await; + processing.abort(); + let _ = processing.await; + drop(lease); + break; + }, + accepted = runtime.accept_from_parent(&mut listener, local_private, parent_public) => { + let (session, new_lease) = match accepted { + Ok(session) => session, + Err(error) => { + tracing::warn!(error = %error, "Guest ComponentSession handshake refused"); + active = Some((serving, processing, lease)); + continue; + } + }; + serving.abort(); + let _ = serving.await; + processing.abort(); + let _ = processing.await; + drop(lease); + let route = session.route_binding(); + let resource_session = match runtime.resource_runtime().bind_session(&route) { + Ok(session) => session, + Err(error) => { + tracing::warn!(error = %error, "Guest Resource API session binding refused"); + drop(new_lease); + continue; + } + }; + let config_services = match guest_config_services(&identity, &route, &options.guest_config_path) { + Ok(services) => services, + Err(error) => { + tracing::warn!(error = %error, "Guest config service binding refused"); + drop(new_lease); + continue; + } + }; + let mut services = resource_session.ttrpc_services(); + services.extend(config_services); + let ttrpc = session.into_ttrpc_handle(); + let resource_client = Arc::new(resource_session.client()); + let mut process_runtime = + process_resource_runtime::ProcessResourceRuntime::new_for_target( + identity.zone().clone(), + Arc::clone(&process_providers), + Some(identity.guest_ref().clone()), + ); + if let Ok(generation) = + d2b_contracts_resource::v3::ControllerGeneration::new( + identity.controller_generation(), + ) + { + process_runtime.set_controller_generation(generation); } + process_runtime.set_guest_execution_binding( + guest_process_execution_binding( + &identity, + route.reconnect_generation(), + )?, + ); + let process_store = resource_session.store_backend(); + let process_task = tokio::spawn( + process_resource_runtime::run_guest_process_reconciliation( + process_runtime, + process_store, + resource_client, + identity.zone().clone(), + ), + ); + active = Some(( + tokio::spawn(async move { + ttrpc + .serve_ttrpc_services(services) + .await + }), + process_task, + new_lease, + )); + }, + result = &mut serving => { + if let Ok(Err(error)) = result { + tracing::debug!(error = %error, "Guest ComponentSession resource server stopped"); + } + processing.abort(); + let _ = processing.await; + drop(lease); + }, + } + } else { + let accepted = tokio::select! { + _ = sigterm.recv() => break, + _ = sigint.recv() => break, + result = runtime.accept_from_parent(&mut listener, local_private, parent_public) => result, + }; + let (session, lease) = match accepted { + Ok(session) => session, + Err(error) => { + if options.once { + return Err(TypedError::InternalConfig { + detail: error.to_string(), + }); + } + tracing::warn!(error = %error, "Guest ComponentSession handshake refused"); + continue; } + }; + if options.once { + drop(lease); + return Ok(()); } - }; - drop(lease); - if shutdown { - break; + let route = session.route_binding(); + let resource_session = match runtime.resource_runtime().bind_session(&route) { + Ok(session) => session, + Err(error) => { + tracing::warn!(error = %error, "Guest Resource API session binding refused"); + drop(lease); + continue; + } + }; + let config_services = match guest_config_services(&identity, &route, &options.guest_config_path) { + Ok(services) => services, + Err(error) => { + tracing::warn!(error = %error, "Guest config service binding refused"); + drop(lease); + continue; + } + }; + let mut services = resource_session.ttrpc_services(); + services.extend(config_services); + let ttrpc = session.into_ttrpc_handle(); + let resource_client = Arc::new(resource_session.client()); + let mut process_runtime = + process_resource_runtime::ProcessResourceRuntime::new_for_target( + identity.zone().clone(), + Arc::clone(&process_providers), + Some(identity.guest_ref().clone()), + ); + if let Ok(generation) = + d2b_contracts_resource::v3::ControllerGeneration::new( + identity.controller_generation(), + ) + { + process_runtime.set_controller_generation(generation); + } + process_runtime.set_guest_execution_binding( + guest_process_execution_binding(&identity, route.reconnect_generation())?, + ); + let process_store = resource_session.store_backend(); + let process_task = tokio::spawn( + process_resource_runtime::run_guest_process_reconciliation( + process_runtime, + process_store, + resource_client, + identity.zone().clone(), + ), + ); + active = Some(( + tokio::spawn(async move { + ttrpc + .serve_ttrpc_services(services) + .await + }), + process_task, + lease, + )); + } + + fn guest_config_services( + identity: &d2bd_runtime::guest_mode::GuestIdentity, + route: &d2b_session::AuthenticatedSessionRouteBinding, + path: &Path, + ) -> Result, String> { + identity + .validate_route(route) + .map_err(|error| error.to_string())?; + let boot_digest = identity + .boot_identity() + .digest() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let backend = d2b_provider_config_nixos::GuestConfigReader::new( + identity.guest_ref().clone(), + format!("sha256:{boot_digest}"), + route.reconnect_generation().get(), + path, + ) + .map_err(|error| error.code().to_owned())?; + Ok(d2b_provider_config_nixos::create_ttrpc_services( + std::sync::Arc::new(backend), + )) + } + + fn guest_process_execution_binding( + identity: &d2bd_runtime::guest_mode::GuestIdentity, + session_generation: ReconnectGeneration, + ) -> Result { + GuestExecutionBinding::new( + identity.guest_uid().clone(), + ConfigurationDigest::from_bytes(identity.boot_identity().digest()), + session_generation, + identity.assignment_epoch(), + ResourceGeneration::new(identity.provider_generation()).map_err(|_| { + TypedError::InternalConfig { + detail: "guest Provider generation is invalid".to_owned(), + } + })?, + d2b_contracts_resource::v3::ControllerGeneration::new( + identity.controller_generation(), + ) + .map_err(|_| TypedError::InternalConfig { + detail: "guest controller generation is invalid".to_owned(), + })?, + ) + .map_err(|_| TypedError::InternalConfig { + detail: "guest Process execution binding is invalid".to_owned(), + }) } } Ok(()) @@ -1704,6 +1948,7 @@ impl d2bd_runtime::supervisor::state::PidfdOpener for BrokerPidfdOpener<'_> { expected_start_time_ticks, resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: None, }), Duration::from_secs(10), @@ -2186,6 +2431,7 @@ impl d2bd_runtime::usbipd_perenv_autostart::PerEnvUsbipdSpawner for BrokerPerEnv execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, workload_identity: None, vm_id: VmId::new(spec.vm_id.clone()), role_id: RoleId::new(spec.role.role_id().to_owned()), @@ -2195,6 +2441,7 @@ impl d2bd_runtime::usbipd_perenv_autostart::PerEnvUsbipdSpawner for BrokerPerEnv provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, role: d2bd_runtime::usbipd_perenv_autostart::spawn_runner_role(spec), bundle_runner_intent_ref: BundleOpId::new(spec.intent_id()), @@ -2699,6 +2946,236 @@ fn dispatch_request_locked( } } +fn dispatch_config_nixos_service_request( + state: &ServerState, + peer: &PeerIdentity, + request: &Value, +) -> Result { + if !matches!(peer.role, PeerRole::Admin) { + return Err(TypedError::AuthzNotAdmin { + verb: "config-nixos".to_owned(), + }); + } + if request.get("sessionVerb").and_then(Value::as_str) != Some("invoke") { + return Err(TypedError::WireInvalidFrame { + detail: "config-nixos service requires the invoke session verb".to_owned(), + }); + } + let zone = request + .get("zoneRef") + .and_then(Value::as_str) + .and_then(|value| value.strip_prefix("Zone/")) + .and_then(|value| ZoneId::parse(value).ok()) + .ok_or_else(|| TypedError::WireInvalidFrame { + detail: "config-nixos request has an invalid Zone reference".to_owned(), + })?; + let method = request + .get("method") + .and_then(Value::as_str) + .ok_or_else(|| TypedError::WireInvalidFrame { + detail: "config-nixos request is missing method".to_owned(), + })?; + let operation = d2b_provider_config_nixos::ConfigOperation::ALL + .into_iter() + .find(|candidate| candidate.as_str() == method) + .ok_or_else(|| TypedError::WireInvalidFrame { + detail: "config-nixos service operation is not supported on the operator route" + .to_owned(), + })?; + let payload = config_nixos_payload(request)?; + let service = d2b_provider_config_nixos::ConfigService; + service + .validate_operation(operation, &payload) + .map_err(config_nixos_wire_error)?; + let guest_ref = payload + .get("guestRef") + .and_then(Value::as_str) + .and_then(|value| ResourceRef::parse(value).ok()) + .filter(|resource| resource.resource_type().as_str() == "Guest") + .ok_or_else(|| TypedError::WireInvalidFrame { + detail: "config-nixos request has an invalid Guest reference".to_owned(), + })?; + let plane = state + .resource_plane + .lock() + .map_err(|_| TypedError::InternalConfig { + detail: "resource plane unavailable".to_owned(), + })? + .clone() + .ok_or_else(|| TypedError::InternalConfig { + detail: "resource plane unavailable".to_owned(), + })?; + let runtime = plane + .zone(&zone) + .map_err(|_| TypedError::InternalConfig { + detail: "config-nixos Zone runtime unavailable".to_owned(), + })?; + let guest_lookup = json!({ + "zoneRef": format!("Zone/{}", zone.as_str()), + "service": "d2b.resource.v3", + "method": "Get", + "resourceRef": guest_ref.to_canonical_string(), + }); + let guest = block_on_future(runtime.dispatch_cli_request(&guest_lookup)).map_err(|_| { + TypedError::InternalConfig { + detail: "config-nixos Guest lookup failed".to_owned(), + } + })?; + if guest.get("kind").is_some() { + return Err(TypedError::AuthzNotAdmin { + verb: "config-nixos Guest is not in the requested Zone".to_owned(), + }); + } + + match operation { + d2b_provider_config_nixos::ConfigOperation::ReadGuestConfig => { + let sync: d2b_provider_config_nixos::ConfigSyncRequest = + serde_json::from_value(payload).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos read request is malformed".to_owned(), + })?; + let response = read_guest_config_typed( + state, + &zone, + sync.guest_ref.name().as_str(), + )?; + serde_json::to_value(response).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos response encoding failed".to_owned(), + }) + } + d2b_provider_config_nixos::ConfigOperation::Stage => { + let stage: d2b_provider_config_nixos::ConfigStageRequest = + serde_json::from_value(payload).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos stage request is malformed".to_owned(), + })?; + let response = state + .config_staging + .lock() + .map_err(|_| TypedError::InternalConfig { + detail: "config staging state unavailable".to_owned(), + })? + .stage( + d2b_provider_config_nixos::ConfigCaller::Admin, + &zone, + &stage, + ) + .map_err(config_nixos_wire_error)?; + serde_json::to_value(response).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos stage response encoding failed".to_owned(), + }) + } + d2b_provider_config_nixos::ConfigOperation::Diff => { + let diff: d2b_provider_config_nixos::ConfigDiffRequest = + serde_json::from_value(payload).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos diff request is malformed".to_owned(), + })?; + let response = state + .config_staging + .lock() + .map_err(|_| TypedError::InternalConfig { + detail: "config staging state unavailable".to_owned(), + })? + .diff( + d2b_provider_config_nixos::ConfigCaller::Admin, + &zone, + &diff, + ) + .map_err(config_nixos_wire_error)?; + serde_json::to_value(response).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos diff response encoding failed".to_owned(), + }) + } + d2b_provider_config_nixos::ConfigOperation::Approve => { + let approve: d2b_provider_config_nixos::ConfigApproveRequest = + serde_json::from_value(payload).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos approve request is malformed".to_owned(), + })?; + let response = state + .config_staging + .lock() + .map_err(|_| TypedError::InternalConfig { + detail: "config staging state unavailable".to_owned(), + })? + .approve( + d2b_provider_config_nixos::ConfigCaller::Admin, + &zone, + &approve, + ) + .map_err(config_nixos_wire_error)?; + serde_json::to_value(response).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos approve response encoding failed".to_owned(), + }) + } + d2b_provider_config_nixos::ConfigOperation::Reject => { + let reject: d2b_provider_config_nixos::ConfigRejectRequest = + serde_json::from_value(payload).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos reject request is malformed".to_owned(), + })?; + let response = state + .config_staging + .lock() + .map_err(|_| TypedError::InternalConfig { + detail: "config staging state unavailable".to_owned(), + })? + .reject( + d2b_provider_config_nixos::ConfigCaller::Admin, + &zone, + &reject, + ) + .map_err(config_nixos_wire_error)?; + serde_json::to_value(response).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos reject response encoding failed".to_owned(), + }) + } + d2b_provider_config_nixos::ConfigOperation::Status => { + let status: d2b_provider_config_nixos::ConfigStatusRequest = + serde_json::from_value(payload).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos status request is malformed".to_owned(), + })?; + let response = state + .config_staging + .lock() + .map_err(|_| TypedError::InternalConfig { + detail: "config staging state unavailable".to_owned(), + })? + .status( + d2b_provider_config_nixos::ConfigCaller::Admin, + &zone, + &status, + ) + .map_err(config_nixos_wire_error)?; + serde_json::to_value(response).map_err(|_| TypedError::WireInvalidFrame { + detail: "config-nixos status response encoding failed".to_owned(), + }) + } + } +} + +fn config_nixos_payload(request: &Value) -> Result { + let mut payload = request + .as_object() + .ok_or_else(|| TypedError::WireInvalidFrame { + detail: "config-nixos request must be an object".to_owned(), + })? + .clone(); + for key in [ + "type", + "method", + "zoneRef", + "schemaVersion", + "service", + "sessionVerb", + ] { + payload.remove(key); + } + Ok(Value::Object(payload)) +} + +fn config_nixos_wire_error(error: d2b_provider_config_nixos::ConfigError) -> TypedError { + TypedError::WireInvalidFrame { + detail: error.code().to_owned(), + } +} + fn dispatch_resource_request( state: &ServerState, peer: &PeerIdentity, @@ -2709,6 +3186,11 @@ fn dispatch_resource_request( resource_runtime::ResourceRuntimeError::AuthenticationUnavailable, )); } + if request.value().get("service").and_then(Value::as_str) + == Some(d2b_provider_config_nixos::SERVICE_PACKAGE) + { + return dispatch_config_nixos_service_request(state, peer, &request.value()); + } if request.method() == Some("HostCutover") { if !matches!(peer.role, PeerRole::Admin) { return Err(TypedError::AuthzNotAdmin { @@ -2728,6 +3210,7 @@ fn dispatch_resource_request( object.remove(key); } } + let cutover: public_wire::HostCutoverRequest = serde_json::from_value(value).map_err(|error| TypedError::WireInvalidFrame { detail: format!("hostCutover request malformed: {error}"), @@ -4912,6 +5395,9 @@ mod workload_observability_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), console_sessions: Arc::new(Mutex::new(console_session::ConsoleSessionTable::default())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), @@ -8902,93 +9388,376 @@ fn cloud_hypervisor_vsock_socket(argv: &[String]) -> Option { if pair[0] != "--vsock" { return None; } - pair[1] - .split(',') - .find_map(|field| field.strip_prefix("socket=").map(PathBuf::from)) + pair[1] + .split(',') + .find_map(|field| field.strip_prefix("socket=").map(PathBuf::from)) + }) +} + +/// Resolve the guest-control probe parameters for `vm` from the trusted +/// bundle: the per-VM vsock socket path + its parent state-root, the +/// cloud-hypervisor runner's peer credentials (principal +/// `d2b--runner`), and the canonical state-root owner. Fails CLOSED if +/// any identity is unresolvable. +fn resolve_guest_control_probe_params( + state: &ServerState, + resolver: &BundleResolver, + vm: &str, +) -> Result { + let dag = resolver + .find_process_vm(vm) + .ok_or_else(|| "guest-control-probe:no-process-dag".to_owned())?; + let ch = dag + .nodes + .iter() + .find(|node| node.role == ProcessRole::CloudHypervisorRunner) + .ok_or_else(|| "guest-control-probe:no-cloud-hypervisor-node".to_owned())?; + let socket_path = cloud_hypervisor_vsock_socket(&ch.argv) + .ok_or_else(|| "guest-control-probe:no-vsock-socket".to_owned())?; + let state_root = socket_path + .parent() + .map(Path::to_path_buf) + .ok_or_else(|| "guest-control-probe:vsock-socket-no-parent".to_owned())?; + let (expected_state_root_uid, expected_state_root_gid) = + resolve_guest_control_state_root_owner(state)?; + Ok(guest_control_bridge::ProbeParams { + vm_id: vm.to_owned(), + socket_path, + state_root, + expected_state_root_uid, + expected_state_root_gid, + expected_peer_uid: ch.profile.uid, + expected_peer_gid: ch.profile.gid, + }) +} + +/// Establish one authenticated Guest ComponentSession from host-published +/// descriptor and key material. The returned client is the sole host-side +/// route to Guest Resource and service APIs. +pub(crate) async fn connect_guest_component_session( + state: &ServerState, + vm: &str, +) -> Result< + Arc, + String, +> { + connect_guest_component_session_with_mode( + state, + vm, + GuestComponentSessionCacheMode::Reuse, + ) + .await +} + +async fn connect_guest_component_session_fresh( + state: &ServerState, + vm: &str, +) -> Result< + Arc, + String, +> { + connect_guest_component_session_with_mode( + state, + vm, + GuestComponentSessionCacheMode::Fresh, + ) + .await +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum GuestComponentSessionCacheMode { + Reuse, + Fresh, +} + +fn select_cached_guest_component_session<'a, T, F>( + mode: GuestComponentSessionCacheMode, + cached: Option<&'a T>, + identity: F, + expected_identity: &d2bd_runtime::guest_mode::GuestIdentity, +) -> Option<&'a T> +where + F: Fn(&T) -> &d2bd_runtime::guest_mode::GuestIdentity, +{ + if matches!(mode, GuestComponentSessionCacheMode::Reuse) + && let Some(cached) = cached + && identity(cached) == expected_identity + { + Some(cached) + } else { + None + } +} + +async fn connect_guest_component_session_with_mode( + state: &ServerState, + vm: &str, + mode: GuestComponentSessionCacheMode, +) -> Result< + Arc, + String, +> { + let key_lock = { + let mut locks = state.guest_component_session_locks.lock().await; + locks + .entry(vm.to_owned()) + .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))) + .clone() + }; + let _connect_lock = key_lock.lock().await; + let resolver = load_bundle_resolver(state).map_err(|_| "bundle-unavailable".to_owned())?; + let params = resolve_guest_control_probe_params(state, &resolver, vm)?; + let endpoint = d2bd_runtime::guest_component_session::GuestComponentSessionEndpoint { + socket_path: params.socket_path, + state_root: params.state_root.clone(), + expected_state_root_uid: params.expected_state_root_uid, + expected_state_root_gid: params.expected_state_root_gid, + expected_peer_uid: params.expected_peer_uid, + expected_peer_gid: params.expected_peer_gid, + setup_timeout: d2bd_runtime::guest_component_session::COMPONENT_SESSION_ATTEMPT_CAP, + }; + let config = + d2bd_runtime::guest_component_session::GuestComponentSessionConfig::from_state_root( + ¶ms.state_root, + endpoint, + ) + .map_err(|_| "session-material-unavailable".to_owned())?; + { + let sessions = state.guest_component_sessions.lock().await; + if let Some(session) = select_cached_guest_component_session( + mode, + sessions.get(vm), + |session| session.identity(), + &config.identity, + ) { + return Ok(Arc::clone(session)); + } + } + let expected_identity = config.identity.clone(); + let client = tokio::time::timeout( + d2bd_runtime::guest_component_session::COMPONENT_SESSION_ATTEMPT_CAP, + config.connect(), + ) + .await + .map_err(|_| "session-timeout".to_owned())? + .map_err(|_| "session-unavailable".to_owned())?; + if client.identity() != &expected_identity + || client.identity().guest_ref().resource_type().as_str() != "Guest" + || client.identity().guest_ref().name().as_str() != vm + { + return Err("session-identity-mismatch".to_owned()); + } + let client = Arc::new(client); + let mut sessions = state.guest_component_sessions.lock().await; + if let Some(session) = select_cached_guest_component_session( + mode, + sessions.get(vm), + |session| session.identity(), + &expected_identity, + ) { + return Ok(Arc::clone(session)); + } + sessions.insert(vm.to_owned(), Arc::clone(&client)); + Ok(client) +} + +pub(crate) async fn invalidate_guest_component_session(state: &ServerState, vm: &str) { + state.guest_component_sessions.lock().await.remove(vm); +} + +#[cfg(test)] +mod guest_component_session_cache_tests { + use super::*; + + struct CachedSession { + identity: d2bd_runtime::guest_mode::GuestIdentity, + generation: u64, + } + + fn identity() -> d2bd_runtime::guest_mode::GuestIdentity { + d2bd_runtime::guest_mode::GuestIdentity::new( + ResourceRef::parse("Guest/workload").expect("Guest ref"), + ResourceUid::parse("123e4567-e89b-42d3-a456-426614174000").expect("Guest UID"), + ZoneId::parse("work").expect("Zone"), + d2bd_runtime::guest_mode::BootIdentity::from_kernel_boot_id("guest-cache-test") + .expect("boot identity"), + d2b_contracts_resource::v3::identity::SessionPurpose::parse( + d2bd_runtime::guest_mode::GUEST_COMPONENT_SESSION_PURPOSE, + ) + .expect("purpose"), + SchemaFingerprint::parse( + "sha256:0000000000000000000000000000000000000000000000000000000000000001", + ) + .expect("schema"), + ReconnectGeneration::new(1).expect("reconnect generation"), + 1, + 1, + 1, + ) + .expect("Guest identity") + } + + #[test] + fn normal_mode_reuses_matching_cached_client() { + let expected = identity(); + let cached = CachedSession { + identity: expected.clone(), + generation: 5, + }; + + let selected = select_cached_guest_component_session( + GuestComponentSessionCacheMode::Reuse, + Some(&cached), + |session| &session.identity, + &expected, + ); + + assert_eq!(selected.map(|session| session.generation), Some(5)); + } + + #[test] + fn fresh_mode_uses_newly_authenticated_generation() { + let expected = identity(); + let stale = CachedSession { + identity: expected.clone(), + generation: 5, + }; + let authenticated = CachedSession { + identity: expected.clone(), + generation: 6, + }; + + let selected = select_cached_guest_component_session( + GuestComponentSessionCacheMode::Fresh, + Some(&stale), + |session| &session.identity, + &expected, + ) + .unwrap_or(&authenticated); + + assert_eq!(selected.generation, 6); + } +} + +fn read_guest_config_typed( + state: &ServerState, + zone: &ZoneId, + vm: &str, +) -> Result { + if d2bd_runtime::zone_authority::authoritative_zone_for_vm(&state.zone_coordinator, vm) + .ok() + .as_ref() + != Some(zone) + { + return Err(TypedError::GuestControlReadFailed { + kind: d2bd_runtime::typed_error::GuestControlReadErrorKind::AuthFailed, + }); + } + ensure_vm_runtime_capability( + state, + vm, + RuntimeCapabilityGate::ConfigSync, + "config sync", + )?; + let guest_ref_text = format!("Guest/{vm}"); + let guest_ref = ResourceRef::parse(&guest_ref_text).map_err(|_| { + TypedError::GuestControlReadFailed { + kind: d2bd_runtime::typed_error::GuestControlReadErrorKind::AuthFailed, + } + })?; + let sync = d2b_provider_config_nixos::ConfigSyncRequest::new(guest_ref).map_err(|_| { + TypedError::GuestControlReadFailed { + kind: d2bd_runtime::typed_error::GuestControlReadErrorKind::AuthFailed, + } + })?; + let response: d2b_provider_config_nixos::ConfigSyncResponse = block_on_future(async { + let session = connect_guest_component_session(state, vm) + .await + .map_err(|_| d2bd_runtime::typed_error::GuestControlReadErrorKind::Transport)?; + let client = d2b_provider_config_nixos::ConfigNixosClient::new(session.client()); + let result = client + .call( + ttrpc::context::Context::default(), + d2b_provider_config_nixos::ConfigOperation::ReadGuestConfig, + &sync, + ) + .await; + if result.is_err() { + invalidate_guest_component_session(state, vm).await; + } + result.map_err(config_read_error_kind) }) + .map_err(|kind| TypedError::GuestControlReadFailed { kind })?; + response + .document() + .map_err(|_| TypedError::GuestControlReadFailed { + kind: d2bd_runtime::typed_error::GuestControlReadErrorKind::Protocol, + })?; + Ok(response) } -/// Resolve the guest-control probe parameters for `vm` from the trusted -/// bundle: the per-VM vsock socket path + its parent state-root, the -/// cloud-hypervisor runner's peer credentials (principal -/// `d2b--runner`), and the canonical state-root owner. Fails CLOSED if -/// any identity is unresolvable. -fn resolve_guest_control_probe_params( - state: &ServerState, - resolver: &BundleResolver, - vm: &str, -) -> Result { - let dag = resolver - .find_process_vm(vm) - .ok_or_else(|| "guest-control-probe:no-process-dag".to_owned())?; - let ch = dag - .nodes - .iter() - .find(|node| node.role == ProcessRole::CloudHypervisorRunner) - .ok_or_else(|| "guest-control-probe:no-cloud-hypervisor-node".to_owned())?; - let socket_path = cloud_hypervisor_vsock_socket(&ch.argv) - .ok_or_else(|| "guest-control-probe:no-vsock-socket".to_owned())?; - let state_root = socket_path - .parent() - .map(Path::to_path_buf) - .ok_or_else(|| "guest-control-probe:vsock-socket-no-parent".to_owned())?; - let (expected_state_root_uid, expected_state_root_gid) = - resolve_guest_control_state_root_owner(state)?; - Ok(guest_control_bridge::ProbeParams { - vm_id: vm.to_owned(), - socket_path, - state_root, - expected_state_root_uid, - expected_state_root_gid, - expected_peer_uid: ch.profile.uid, - expected_peer_gid: ch.profile.gid, - }) +fn config_read_error_kind( + error: ttrpc::Error, +) -> d2bd_runtime::typed_error::GuestControlReadErrorKind { + use d2bd_runtime::typed_error::GuestControlReadErrorKind as Kind; + if matches!( + &error, + ttrpc::Error::RpcStatus(status) if status.code() == ttrpc::Code::DEADLINE_EXCEEDED + ) { + return Kind::Timeout; + } + let ttrpc::Error::RpcStatus(status) = error else { + return Kind::Transport; + }; + match status.code() { + ttrpc::Code::UNAVAILABLE => return Kind::Transport, + ttrpc::Code::RESOURCE_EXHAUSTED => return Kind::FileTooLarge, + ttrpc::Code::PERMISSION_DENIED => return Kind::ReadDenied, + ttrpc::Code::UNAUTHENTICATED => return Kind::AuthFailed, + _ => {} + } + match status.message() { + "config-document-too-large" => Kind::FileTooLarge, + "config-document-invalid-utf8" => Kind::Protocol, + "config-document-unavailable" => Kind::ReadDenied, + "config-session-stale" | "config-unauthorized" => Kind::AuthFailed, + "config-request-invalid" + | "config-document-empty" + | "config-document-encoding-failed" => Kind::Protocol, + _ => Kind::Transport, + } } /// ADMIN-ONLY public.sock verb: read the editable guest config working copy of -/// `vm` over the authenticated guest-control bridge and return it as a base64 +/// `vm` over the authenticated ComponentSession config service and return it as a base64 /// string. The admin authorization gate runs in `dispatch_request` BEFORE this -/// handler. The orchestration runs on a dedicated OS thread (the -/// synchronous-verb runtime boundary). The encoded payload is bounded so it fits both transport frames; +/// handler. The encoded payload is bounded so it fits both transport frames; /// any guest content is never echoed into an error. fn dispatch_read_guest_config( state: &ServerState, caller_role: BrokerCallerRole, request: public_wire::ReadGuestConfigRequest, ) -> Result { - ensure_vm_runtime_capability( - state, - &request.vm, - RuntimeCapabilityGate::ConfigSync, - "config sync", - )?; - ensure_vm_runtime_capability( - state, - &request.vm, - RuntimeCapabilityGate::GuestControl, - "read guest config", - )?; - let resolver = load_bundle_resolver(state)?; - let params = - resolve_guest_control_probe_params(state, &resolver, &request.vm).map_err(|detail| { - tracing::warn!( - kind = "critical", - subsystem = "guest-control-health", - error_kind = "transport-io", - "guest-control config read: probe params unresolved: {detail}" - ); - TypedError::GuestControlReadFailed { - kind: d2bd_runtime::typed_error::GuestControlReadErrorKind::Transport, - } - })?; - let broker_path = broker_socket_path(state); - let bytes = guest_control_bridge::run_config_read_on_dedicated_thread( - params, - broker_path, + if !matches!( caller_role, - guest_control_bridge::GUEST_CONTROL_CONFIG_READ_TIMEOUT, + BrokerCallerRole::AdminUid { .. } | BrokerCallerRole::RootUid { .. } + ) { + return Err(TypedError::AuthzNotAdmin { + verb: "read guest config".to_owned(), + }); + } + let zone = d2bd_runtime::zone_authority::authoritative_zone_for_vm( + &state.zone_coordinator, + &request.vm, ) - .map_err(guest_control_bridge::map_guest_file_read_error)?; + .map_err(|_| TypedError::GuestControlReadFailed { + kind: d2bd_runtime::typed_error::GuestControlReadErrorKind::AuthFailed, + })?; + let response = read_guest_config_typed(state, &zone, &request.vm)?; + let bytes = response.document().map_err(|_| TypedError::GuestControlReadFailed { + kind: d2bd_runtime::typed_error::GuestControlReadErrorKind::Protocol, + })?; + let bytes = bytes.bytes().to_vec(); let encoded = d2b_core::base64_codec::encode(&bytes); if !d2b_contracts_control::guest_wire::guest_config_encoded_within_frame_caps(encoded.len()) { return Err(TypedError::GuestControlReadFailed { @@ -12635,7 +13404,9 @@ impl VmStartRunner<'_> { provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, + guest_execution: None, role: runner_role, bundle_runner_intent_ref: BundleOpId::new(intent.intent_id.clone()), runtime_allocations: vec![], @@ -12833,56 +13604,49 @@ impl VmStartRunner<'_> { } } - /// State-aware readiness for a `GuestControlHealth` node. Resolves the - /// per-VM probe parameters from the trusted bundle and runs the - /// authenticated Health probe on a dedicated current-thread runtime - /// inside `spawn_blocking` (a strict runtime boundary: no `Handle::current`, - /// `block_in_place`, or nested runtime; nothing borrowed from - /// `ServerState` crosses the boundary). The retry loop is bounded by - /// `budget.readiness`; `guest_control_health_ready` decides ready. - async fn wait_for_guest_control_health( + /// State-aware readiness for the Guest session node. Readiness is the + /// authenticated ComponentSession; no Guest-local Endpoint projection, + /// feature health RPC, or legacy guest-control probe participates. + async fn wait_for_guest_component_session( &self, vm: &str, node: &ProcessNode, budget: d2bd_runtime::supervisor::dag::NodeBudget, ) -> Result<(), String> { - let params = resolve_guest_control_probe_params(self.state, self.resolver, vm)?; - let broker_path = broker_socket_path(self.state); - let caller_role = self.caller_role.clone(); - let deadline = budget.readiness; + let deadline = Instant::now() + budget.readiness; let node_id = node.id.0.clone(); - let run = tokio::task::spawn_blocking(move || { - let probe = guest_control_bridge::RealGuestControlProbe::with_caller_role( - broker_path, - caller_role, - ); - let clock = guest_control_bridge::RealProbeClock::new(); - guest_control_bridge::run_guest_control_readiness_loop( - &probe, - ¶ms, - deadline, - guest_control_bridge::GUEST_CONTROL_ATTEMPT_CAP, - guest_control_bridge::GUEST_CONTROL_RETRY_BACKOFF, - &clock, + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(format!("guest-component-session-not-ready:{node_id}")); + } + let attempt = tokio::time::timeout( + remaining.min( + d2bd_runtime::guest_component_session::COMPONENT_SESSION_ATTEMPT_CAP, + ), + connect_guest_component_session_fresh(self.state, vm), ) - }) - .await - .map_err(|error| format!("guest-control-readiness-join:{error}"))?; - - let ready = d2bd_runtime::guest_control_health::guest_control_health_ready(&run.outcome); - let obs = guest_control_bridge::ReadinessObservation::from_run(&run); - guest_control_bridge::emit_guest_control_readiness_event(&obs, ready); - - if ready { - tracing::info!( - vm = %vm, - node = %node_id, - role_id = %tracked_role_id(node), - "guest-control-health node ready" - ); - Ok(()) - } else { - Err(format!("guest-control-health-not-ready:{node_id}")) + .await; + match attempt { + Ok(Ok(_session)) => { + tracing::info!( + vm = %vm, + node = %node_id, + role_id = %tracked_role_id(node), + "Guest ComponentSession authenticated and ready" + ); + return Ok(()); + } + _ => { + invalidate_guest_component_session(self.state, vm).await; + } + } + let delay = d2bd_runtime::guest_component_session::COMPONENT_SESSION_RETRY_BACKOFF + .min(deadline.saturating_duration_since(Instant::now())); + if delay.is_zero() { + return Err(format!("guest-component-session-not-ready:{node_id}")); + } + tokio::time::sleep(delay).await; } } } @@ -12896,13 +13660,13 @@ impl d2bd_runtime::supervisor::dag::NodeRunner for VmStartRunner<'_> { readiness: &[ReadinessPredicate], budget: d2bd_runtime::supervisor::dag::NodeBudget, ) -> Result<(), String> { - // The authenticated guest-control Health node is readiness-only but + // The authenticated Guest ComponentSession node is readiness-only but // needs daemon state (per-VM vsock socket, peer credentials, the // broker-backed signer), so it cannot go through the stateless // `wait_for_readiness` path. Intercept it here; this also covers the // `spawn_and_check_process_alive` fall-through, which delegates here. if node.role == ProcessRole::GuestControlHealth { - return self.wait_for_guest_control_health(vm, node, budget).await; + return self.wait_for_guest_component_session(vm, node, budget).await; } if node.role == ProcessRole::SecurityKeyFrontend { // Security-key relay/frontend ownership belongs to the @@ -13159,6 +13923,7 @@ fn signal_unregistered_spawned_runner( expected_start_time_ticks: Some(response.start_time_ticks), resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: None, }); match dispatch_broker_request_as(state, request, caller_role) { @@ -13757,6 +14522,10 @@ fn infer_runner_role_for_vm_stop(role_id: &str) -> Option { || role_id.contains("otel_host_bridge") { Some(RunnerRole::OtelHostBridge) + } else if role_id == RunnerRole::ActivationNixos.as_str() + || role_id.contains("activation-nixos") + { + Some(RunnerRole::ActivationNixos) } else { None } @@ -13781,6 +14550,7 @@ fn vm_stop_role_priority(role: Option) -> u8 { Some(RunnerRole::Swtpm) => 6, Some(RunnerRole::Virtiofsd) => 7, Some(RunnerRole::SwtpmFlush) => 8, + Some(RunnerRole::ActivationNixos) => 9, None => 9, } } @@ -13868,6 +14638,7 @@ fn signal_via_broker( expected_start_time_ticks: registration.as_ref().map(|entry| entry.start_time_ticks), resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: None, }); match dispatch_broker_request_as(state, request, caller_role) { @@ -13934,6 +14705,7 @@ fn deregister_runner_pidfd_via_broker( .map(|entry| entry.start_time_ticks), resource_ref: None, resource_uid: None, + guest_execution: None, tracing_span_id: None, }); match dispatch_broker_request_as(state, request, caller_role) { @@ -17470,7 +18242,9 @@ fn dispatch_broker_run_migrate_as( } } +#[cfg(test)] const GUEST_SYSTEM_ACTIVATION_REJOIN_GRACE: Duration = Duration::from_secs(60); +#[cfg(test)] const GUEST_SYSTEM_ACTIVATION_START_DEADLINE: Duration = Duration::from_secs(15); #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -17570,6 +18344,7 @@ fn stage_configuration_ordinal( .stage_configuration_ordinal(zone, ordinal) } +#[cfg(test)] fn commit_configuration_ordinal( state: &ServerState, zone: &ZoneId, @@ -17581,6 +18356,7 @@ fn commit_configuration_ordinal( .commit_configuration_ordinal(zone) } +#[cfg(test)] fn abort_configuration_ordinal(state: &ServerState, zone: &ZoneId) -> Result<(), CoordinatorError> { state .zone_coordinator @@ -17597,6 +18373,7 @@ fn activation_marker_path(state: &ServerState, vm: &str) -> PathBuf { activation_marker_dir(state).join(format!("{vm}.json")) } +#[cfg(test)] fn now_unix_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -17604,6 +18381,7 @@ fn now_unix_secs() -> u64 { .as_secs() } +#[cfg(test)] fn generate_activation_id() -> Result { let mut bytes = [0u8; 16]; getrandom::getrandom(&mut bytes).map_err(|err| TypedError::InternalIo { @@ -17633,6 +18411,7 @@ fn generate_activation_id() -> Result { )) } +#[cfg(test)] fn switch_script_basename(path: &str) -> String { Path::new(path) .parent() @@ -17643,12 +18422,14 @@ fn switch_script_basename(path: &str) -> String { .to_owned() } +#[cfg(test)] fn switch_script_digest(path: &str) -> String { let mut hasher = Sha256::new(); hasher.update(path.as_bytes()); format!("{:x}", hasher.finalize()) } +#[cfg(test)] fn build_activation_marker( vm: &str, mode: BrokerActivationMode, @@ -17672,6 +18453,7 @@ fn build_activation_marker( } } +#[cfg(test)] fn write_activation_marker_blocking( path: &Path, marker: &HostActivationPendingMarker, @@ -17693,6 +18475,7 @@ fn write_activation_marker_blocking( Ok(()) } +#[cfg(test)] fn write_activation_marker( state: &ServerState, marker: &HostActivationPendingMarker, @@ -17711,6 +18494,7 @@ fn write_activation_marker( }) } +#[cfg(test)] fn clear_activation_marker(state: &ServerState, vm: &str) { let path = activation_marker_path(state, vm); let dir = activation_marker_dir(state); @@ -17848,6 +18632,7 @@ fn activation_mode_label(mode: BrokerActivationMode) -> &'static str { } } +#[cfg(test)] fn activation_guest_mode( mode: BrokerActivationMode, ) -> d2bd_runtime::guest_control_health::GuestSystemActivationMode { @@ -17864,6 +18649,7 @@ fn activation_guest_mode( } #[derive(Debug, Clone)] +#[cfg(test)] struct GuestActivationPlan { vm: String, mode: BrokerActivationMode, @@ -17872,6 +18658,7 @@ struct GuestActivationPlan { } #[derive(Debug, Clone)] +#[cfg(test)] enum GuestActivationTerminal { Succeeded, DefinitiveFailure { @@ -17882,6 +18669,7 @@ enum GuestActivationTerminal { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(test)] enum GuestActivationFailureRemediation { GuestJournal, UpgradeGuest, @@ -17912,6 +18700,7 @@ fn test_guest_activation_hook_installed() -> bool { false } +#[cfg(test)] fn guest_activation_error_summary( error: &d2bd_runtime::guest_control_health::GuestSystemActivationError, ) -> String { @@ -17936,6 +18725,7 @@ fn guest_activation_error_summary( } } +#[cfg(test)] fn guest_activation_failure_remediation( error: &d2bd_runtime::guest_control_health::GuestSystemActivationError, ) -> GuestActivationFailureRemediation { @@ -17947,6 +18737,7 @@ fn guest_activation_failure_remediation( } } +#[cfg(test)] fn guest_activation_error_is_indeterminate( error: &d2bd_runtime::guest_control_health::GuestSystemActivationError, ) -> bool { @@ -17964,6 +18755,7 @@ fn guest_activation_error_is_indeterminate( ) } +#[cfg(test)] fn guest_activation_error_is_not_found( error: &d2bd_runtime::guest_control_health::GuestSystemActivationError, ) -> bool { @@ -17975,6 +18767,7 @@ fn guest_activation_error_is_not_found( ) } +#[cfg(test)] fn run_guest_system_activation( state: &ServerState, params: Option, @@ -18060,6 +18853,7 @@ fn run_guest_system_activation( ) } +#[cfg(test)] fn rejoin_guest_activation_status( state: &ServerState, params: guest_control_bridge::ProbeParams, @@ -18113,6 +18907,7 @@ fn rejoin_guest_activation_status( } } +#[cfg(test)] fn guest_activation_state_to_terminal( status: d2bd_runtime::guest_control_health::GuestSystemActivationStatus, ) -> GuestActivationTerminal { @@ -18146,6 +18941,7 @@ fn guest_activation_state_to_terminal( } } +#[cfg(test)] fn activation_state_label(state: pb::GuestActivationState) -> &'static str { match state { pb::GuestActivationState::GUEST_ACTIVATION_STATE_RUNNING => "running", @@ -18188,9 +18984,272 @@ fn dispatch_broker_activation( ); } + #[cfg(not(test))] + { + return dispatch_live_guest_activation_resource(state, request, verb, mode); + } + #[cfg(test)] dispatch_live_guest_activation(state, request, verb, mode, caller_role, None) } +#[cfg(not(test))] +fn dispatch_live_guest_activation_resource( + state: &ServerState, + request: public_wire::ActivationRequest, + verb: &'static str, + mode: BrokerActivationMode, +) -> Result { + if mode != BrokerActivationMode::Rollback && request.to_generation.is_some() { + return Ok(invalid_request_response_with_summary( + verb, + "a target generation is valid only for rollback".to_owned(), + "omit --to-generation for switch, test, and boot activation".to_owned(), + )); + } + let guard = match try_acquire_activation_lock(state, &request.vm) { + Ok(guard) => guard, + Err(frame) => return Ok(frame), + }; + let zone = guard.zone().clone(); + let resolver = load_bundle_resolver(state)?; + let plane = state + .resource_plane + .lock() + .map_err(|_| TypedError::InternalConfig { + detail: "resource plane unavailable".to_owned(), + })? + .clone() + .ok_or_else(|| TypedError::InternalConfig { + detail: "resource plane unavailable".to_owned(), + })?; + let runtime = plane + .zone(&zone) + .map_err(|_| TypedError::InternalConfig { + detail: "activation Zone runtime unavailable".to_owned(), + })?; + let guest_ref_text = format!("Guest/{}", request.vm); + let guest_ref = ResourceRef::parse(&guest_ref_text).map_err(|_| { + TypedError::InternalConfig { + detail: "activation Guest reference invalid".to_owned(), + } + })?; + let guest_ref_canonical = guest_ref.to_canonical_string(); + let get_guest = json!({ + "zoneRef": format!("Zone/{}", zone.as_str()), + "service": "d2b.resource.v3", + "method": "Get", + "resourceRef": guest_ref_canonical, + }); + let guest = block_on_future(runtime.dispatch_cli_request(&get_guest)).map_err(|_| { + TypedError::InternalConfig { + detail: "activation Guest resource unavailable".to_owned(), + } + })?; + if guest.get("kind").is_some() { + return Err(TypedError::InternalConfig { + detail: "activation Guest resource unavailable".to_owned(), + }); + } + let (ordinal, artifact) = if mode == BrokerActivationMode::Rollback { + let target_ordinal = request.to_generation.ok_or_else(|| { + TypedError::InternalConfig { + detail: "rollback target generation unavailable".to_owned(), + } + })?; + let list = json!({ + "zoneRef": format!("Zone/{}", zone.as_str()), + "service": "d2b.resource.v3", + "method": "List", + "resourceType": NIXOS_GENERATION_RESOURCE_TYPE, + "executionRef": guest_ref.to_canonical_string(), + "limit": 256, + }); + let resources = block_on_future(runtime.dispatch_cli_request(&list)).map_err(|_| { + TypedError::InternalConfig { + detail: "rollback generations unavailable".to_owned(), + } + })?; + let artifact = resources + .get("resources") + .and_then(Value::as_array) + .into_iter() + .flatten() + .find(|resource| { + resource + .get("spec") + .and_then(|spec| spec.get("executionRef")) + .and_then(Value::as_str) + == Some(guest_ref_canonical.as_str()) + && resource + .pointer("/metadata/name") + .and_then(Value::as_str) + .and_then(|name| name.rsplit('-').next()) + .and_then(|value| value.parse::().ok()) + == Some(target_ordinal) + }) + .and_then(|resource| { + resource + .pointer("/spec/systemArtifactId") + .and_then(Value::as_str) + }) + .ok_or_else(|| TypedError::InternalConfig { + detail: "rollback target generation is not retained".to_owned(), + })?; + (target_ordinal, artifact.to_owned()) + } else { + let activation = resolver + .find_activation_intent(&intent_id_activation(&request.vm)) + .ok_or_else(|| TypedError::InternalConfig { + detail: "activation intent unavailable".to_owned(), + })?; + let ordinal = activation + .generation_number + .filter(|generation| *generation > 0) + .ok_or_else(|| TypedError::InternalConfig { + detail: "activation generation unavailable".to_owned(), + })?; + let artifact = guest + .pointer("/spec/systemArtifactId") + .and_then(Value::as_str) + .ok_or_else(|| TypedError::InternalConfig { + detail: "activation system artifact unavailable".to_owned(), + })?; + (ordinal, artifact.to_owned()) + }; + let generation_name = activation_generation_name(&request.vm, ordinal, mode); + let generation_ref = format!("{NIXOS_GENERATION_RESOURCE_TYPE}/{generation_name}"); + let spec = json!({ + "providerRef": "Provider/activation-nixos", + "executionRef": guest_ref_canonical, + "systemArtifactId": artifact, + "activationMode": resource_activation_mode_label(mode), + }); + let create = json!({ + "zoneRef": format!("Zone/{}", zone.as_str()), + "service": "d2b.resource.v3", + "method": "Create", + "resourceType": NIXOS_GENERATION_RESOURCE_TYPE, + "resourceName": generation_name, + "spec": spec, + "waitForReconcile": false, + }); + let created = block_on_future(runtime.dispatch_cli_request(&create)).map_err(|_| { + TypedError::InternalConfig { + detail: "activation resource create failed".to_owned(), + } + })?; + if created + .get("kind") + .and_then(Value::as_str) + .is_some_and(|kind| kind != "resource-already-exists") + { + return Err(TypedError::InternalConfig { + detail: "activation resource create refused".to_owned(), + }); + } + let deadline = Instant::now() + live_activation_timeout_for(state, &request.vm); + loop { + if Instant::now() >= deadline { + return Ok(broker_failure_response( + verb, + format!("activation resource for vm '{}' timed out", request.vm), + format!("retry `d2b {verb} {} --apply` after the Guest session recovers", request.vm), + None, + )); + } + let get = json!({ + "zoneRef": format!("Zone/{}", zone.as_str()), + "service": "d2b.resource.v3", + "method": "Get", + "resourceRef": generation_ref, + }); + let current = block_on_future(runtime.dispatch_cli_request(&get)).map_err(|_| { + TypedError::InternalConfig { + detail: "activation resource status unavailable".to_owned(), + } + })?; + let current_spec = current.get("spec"); + if current_spec + .and_then(|spec| spec.get("executionRef")) + != spec.get("executionRef") + || current_spec + .and_then(|spec| spec.get("systemArtifactId")) + != spec.get("systemArtifactId") + || current_spec + .and_then(|spec| spec.get("activationMode")) + != spec.get("activationMode") + { + return Ok(invalid_request_response_with_summary( + verb, + format!( + "activation resource '{}' is bound to a different immutable target", + generation_ref + ), + "inspect the existing NixosGeneration resource and retry with its committed activation mode" + .to_owned(), + )); + } + match current.pointer("/status/phase").and_then(Value::as_str) { + Some("Ready") | Some("Succeeded") => { + return Ok(applied_response( + verb, + format!( + "d2b {verb} --apply reconciled activation resource for vm '{}' through the Resource API", + request.vm + ), + )); + } + Some("Failed") | Some("Degraded") => { + return Ok(broker_failure_response( + verb, + format!("activation resource for vm '{}' failed", request.vm), + "inspect the typed activation resource status before retrying".to_owned(), + None, + )); + } + _ => std::thread::sleep(Duration::from_millis(250)), + } + } +} + +#[cfg(not(test))] +fn activation_generation_name(vm: &str, ordinal: u64, mode: BrokerActivationMode) -> String { + let suffix = match mode { + BrokerActivationMode::Switch => "gen", + BrokerActivationMode::Boot => "boot", + BrokerActivationMode::Test => "test", + BrokerActivationMode::Rollback => "rollback", + }; + let readable = format!("{vm}--{suffix}-{ordinal}"); + if ResourceName::parse(readable.clone()).is_ok() { + return readable; + } + let mut digest = Sha256::new(); + digest.update(b"d2b-activation-generation-v1"); + digest.update([0]); + digest.update(vm.as_bytes()); + digest.update([0]); + digest.update(suffix.as_bytes()); + digest.update(ordinal.to_be_bytes()); + let suffix = digest + .finalize() + .iter() + .take(12) + .map(|byte| format!("{byte:02x}")) + .collect::(); + format!("activation-gen-{suffix}") +} + +#[cfg(not(test))] +const fn resource_activation_mode_label(mode: BrokerActivationMode) -> &'static str { + match mode { + BrokerActivationMode::Switch => "switch", + BrokerActivationMode::Boot => "boot", + BrokerActivationMode::Test => "test", + BrokerActivationMode::Rollback => "switch", + } +} + fn dispatch_run_activation_phase( state: &ServerState, request: &public_wire::ActivationRequest, @@ -18321,6 +19380,7 @@ fn dispatch_broker_activation_metadata_only( )) } +#[cfg(test)] fn dispatch_live_guest_activation( state: &ServerState, request: public_wire::ActivationRequest, @@ -19754,6 +20814,9 @@ mod public_status_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -22244,6 +23307,9 @@ mod detached_exec_routing_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -22991,6 +24057,9 @@ mod accept_loop_concurrency_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -23583,7 +24652,7 @@ mod accept_loop_concurrency_tests { #[cfg(test)] mod broker_dispatch_tests { - use std::collections::VecDeque; + use std::collections::{HashMap, VecDeque}; use std::fs::File; use std::io::{self, IoSlice, Read, Write}; use std::net::TcpListener; @@ -23736,6 +24805,9 @@ mod broker_dispatch_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -23785,6 +24857,9 @@ mod broker_dispatch_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -24929,6 +26004,7 @@ mod broker_dispatch_tests { "switch", d2bd_runtime::wire::Request::Switch(ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { dry_run: true, ..MutationFlags::default() @@ -24939,6 +26015,7 @@ mod broker_dispatch_tests { "boot", d2bd_runtime::wire::Request::Boot(ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { dry_run: true, ..MutationFlags::default() @@ -24949,6 +26026,7 @@ mod broker_dispatch_tests { "test", d2bd_runtime::wire::Request::Test(ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { dry_run: true, ..MutationFlags::default() @@ -24959,6 +26037,7 @@ mod broker_dispatch_tests { "rollback", d2bd_runtime::wire::Request::Rollback(ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { dry_run: true, ..MutationFlags::default() @@ -25350,6 +26429,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25366,6 +26446,7 @@ mod broker_dispatch_tests { &test_state_with_broker_socket(unreachable_broker_socket_path("boot-unreachable")), ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25388,6 +26469,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25410,6 +26492,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25454,6 +26537,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25511,6 +26595,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25551,6 +26636,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25624,6 +26710,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25663,6 +26750,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25694,6 +26782,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25736,6 +26825,7 @@ mod broker_dispatch_tests { &state, ActivationRequest { vm: "vm-a".to_owned(), + to_generation: None, flags: MutationFlags { apply: true, ..MutationFlags::default() @@ -25934,6 +27024,9 @@ mod broker_dispatch_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -25974,6 +27067,7 @@ mod broker_dispatch_tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, provider_identity: None, template_identity: None, generation: None, @@ -26181,6 +27275,9 @@ mod broker_dispatch_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -26268,6 +27365,7 @@ mod broker_dispatch_tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, provider_identity: None, template_identity: None, generation: None, @@ -26458,6 +27556,9 @@ mod broker_dispatch_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -27347,6 +28448,7 @@ mod broker_dispatch_tests { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, provider_identity: None, template_identity: None, generation: None, @@ -28380,6 +29482,9 @@ mod broker_dispatch_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -28526,7 +29631,7 @@ mod broker_dispatch_tests { let result = readiness_predicate_ready(&predicate); assert_eq!( result, - Err("guest-control-health-needs-state-aware-path".to_owned()), + Err("guest-component-session-needs-state-aware-path".to_owned()), "stateless guest-control readiness MUST be a loud Err so a routing regression cannot masquerade as a benign never-ready" ); } @@ -29357,7 +30462,7 @@ mod broker_dispatch_tests { fn read_guest_config_dispatch_denies_launcher_before_any_side_effect() { // The broker socket is unreachable. If the admin gate did NOT // short-circuit, dispatch_read_guest_config would load the bundle - // resolver, resolve probe params, BrokerSign, and read guest bytes - + // resolver, establish ComponentSession, and read guest bytes - // producing a transport / broker error, never AuthzNotAdmin. // Receiving AuthzNotAdmin proves the launcher was denied at the gate // BEFORE any bundle load / probe / sign / guest-byte read. @@ -29383,8 +30488,8 @@ mod broker_dispatch_tests { #[test] fn read_guest_config_dispatch_admin_clears_gate_and_reaches_handler() { // The admin peer clears the authz gate, so dispatch reaches the - // handler and fails LATER (the bundle vm has no guest-control node / - // the broker is unreachable) with a guest-control read or transport + // handler and fails LATER (the bundle vm has no config capability / + // the broker is unreachable) with a ComponentSession read or transport // error - never an authz error. This proves the gate is the only // thing denying the launcher above, not some unrelated failure. let state = test_state_with_broker_socket(unreachable_broker_socket_path( @@ -29622,6 +30727,9 @@ mod broker_dispatch_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new(Default::default())), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), @@ -29758,6 +30866,11 @@ mod broker_dispatch_tests { interaction_listeners: Arc::new(Mutex::new(None)), typed_shell_session_targets: d2bd_runtime::typed_shell_targets::new_cache(), zone_coordinator: d2bd_runtime::zone_authority::new_coordinator(), + config_staging: Arc::new(Mutex::new( + d2b_provider_config_nixos::ConfigStagingStore::default(), + )), + guest_component_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + guest_component_session_locks: Arc::new(tokio::sync::Mutex::new(HashMap::new())), security_key_sessions: Arc::new(parking_lot::Mutex::new( crate::security_key::SkSessionTable::default(), )), diff --git a/packages/d2bd/src/interaction_composition.rs b/packages/d2bd/src/interaction_composition.rs index 4ddeb99520..8fcd9e20f8 100644 --- a/packages/d2bd/src/interaction_composition.rs +++ b/packages/d2bd/src/interaction_composition.rs @@ -137,6 +137,10 @@ const INTERACTION_SERVICES: &[(&str, ServicePackage)] = &[ d2b_provider_notification_desktop::SERVICE_PACKAGE, ServicePackage::NotificationV3, ), + ( + d2b_provider_config_nixos::SERVICE_PACKAGE, + ServicePackage::ConfigNixosV3, + ), ]; /// Return the exact ComponentSession policy for one daemon-owned interaction @@ -3517,6 +3521,7 @@ impl BundleDisplayLaunchResolver { resource_uid: observed.intent.resource_uid.clone(), bundle_content_identity: observed.intent.bundle_content_identity.clone(), sandbox_plan: observed.intent.sandbox_plan.clone(), + guest_execution: observed.intent.guest_execution.clone(), pid: observed.pid, start_time_ticks: observed.start_time_ticks, cgroup_verified: observed.cgroup_verified, @@ -3558,6 +3563,8 @@ impl BrokerLaunchResolver for BundleDisplayLaunchResolver { provider_identity, template_identity, generation: ticket.resource_generation().get(), + activation_input: None, + guest_execution: None, resource_ref: ticket.process_ref().clone(), resource_uid: ticket.process_uid().clone(), bundle_content_identity: self.bundle.audit_bundle_hash().to_owned(), @@ -3618,6 +3625,8 @@ struct PersistedObservation { resource_uid: ResourceUid, bundle_content_identity: String, sandbox_plan: Option, + #[serde(default)] + guest_execution: Option, pid: i32, start_time_ticks: u64, cgroup_verified: bool, @@ -3649,6 +3658,8 @@ fn load_observations( provider_identity: record.provider_identity, template_identity: record.template_identity, generation: record.generation, + activation_input: None, + guest_execution: record.guest_execution, resource_ref: record.resource_ref, resource_uid: record.resource_uid, bundle_content_identity: record.bundle_content_identity, @@ -4797,6 +4808,7 @@ mod tests { resource_uid: ResourceUid::parse("22222222-2222-4222-8222-222222222222").unwrap(), bundle_content_identity: "sha256:test".to_owned(), sandbox_plan: None, + guest_execution: None, pid: 42, start_time_ticks: 99, cgroup_verified: true, diff --git a/packages/d2bd/src/main.rs b/packages/d2bd/src/main.rs index bafeac120a..9e428cb155 100644 --- a/packages/d2bd/src/main.rs +++ b/packages/d2bd/src/main.rs @@ -113,6 +113,10 @@ struct GuestArgs { broker_uid: u32, #[arg(long, default_value = "/var/lib/d2b/guest-state")] state_dir: PathBuf, + #[arg(long, hide = true, default_value = "/etc/d2b/guest-bundle.json")] + bundle_path: PathBuf, + #[arg(long, default_value = "/var/lib/d2b-guest/guest-config.nix")] + guest_config_path: PathBuf, #[arg(long, hide = true, default_value = "/proc/sys/kernel/random/boot_id")] boot_id_path: PathBuf, #[arg(long, hide = true)] @@ -172,6 +176,8 @@ async fn main() { broker_socket_path: args.broker_socket, broker_uid: args.broker_uid, state_dir: args.state_dir, + bundle_path: args.bundle_path, + guest_config_path: args.guest_config_path, boot_id_path: args.boot_id_path, local_private_key_path: args.local_private_key, parent_public_key_path: args.parent_public_key, diff --git a/packages/d2bd/src/process_provider_runtime.rs b/packages/d2bd/src/process_provider_runtime.rs index 4cdf7b57d8..771f4da8c5 100644 --- a/packages/d2bd/src/process_provider_runtime.rs +++ b/packages/d2bd/src/process_provider_runtime.rs @@ -25,8 +25,9 @@ use d2b_core::{ }; use d2b_process_conformance::{ AdoptionOutcome, CompiledDigests, ConfigurationDigest, IdentityBinding, LaunchTicket, - OperationBinding, ProcessConformanceError, ProcessIdentityDigest, ProcessLaunchEffectPort, - ProcessProvider, ProcessStatusReport, ReadinessExpectation, SandboxCompiler, StopClass, + GuestExecutionBinding, OperationBinding, ProcessConformanceError, ProcessIdentityDigest, + ProcessLaunchEffectPort, ProcessProvider, ProcessStatusReport, ReadinessExpectation, + SandboxCompiler, StopClass, }; use d2b_provider_supervisor::{ BrokerProcessBackend, BrokerSystemdEffectOwner, BundleBackedLaunchResolver, ProviderSupervisor, @@ -120,18 +121,19 @@ struct ManagedResource { fn resource_identity_matches( managed: &ManagedResource, - context: ProcessResourceContext<'_>, + context: &ProcessResourceContext<'_>, ) -> bool { managed.uid == *context.resource_uid && managed.generation == context.resource_generation } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub(crate) struct ProcessResourceContext<'a> { pub(crate) resource_ref: &'a ResourceRef, pub(crate) resource_uid: &'a ResourceUid, pub(crate) resource_generation: ResourceGeneration, pub(crate) provider_ref: &'a ResourceRef, pub(crate) controller_generation: ControllerGeneration, + pub(crate) guest_execution: Option, } impl<'a> ProcessResourceContext<'a> { @@ -148,8 +150,17 @@ impl<'a> ProcessResourceContext<'a> { resource_generation, provider_ref, controller_generation, + guest_execution: None, } } + + pub(crate) fn with_guest_execution( + mut self, + binding: Option<&GuestExecutionBinding>, + ) -> Self { + self.guest_execution = binding.cloned(); + self + } } /// Result of a Provider-backed launch, carrying only opaque process identity. @@ -335,6 +346,18 @@ impl ProductionProcessProviders { &self.fixed_effect } + fn validate_execution_target(&self, target: &ResourceRef) -> Result<(), String> { + let expected = match self.mode { + DaemonMode::Host => "Host", + DaemonMode::Guest => "Guest", + }; + if target.resource_type().as_str() == expected { + Ok(()) + } else { + Err("process-execution-target-not-owned-by-daemon".to_owned()) + } + } + /// Return whether this node is a daemon-owned Provider process. pub fn supports_node(node: &ProcessNode) -> bool { matches!( @@ -344,6 +367,7 @@ impl ProductionProcessProviders { | ProcessRole::Virtiofsd | ProcessRole::CloudHypervisorRunner | ProcessRole::QemuMediaRunner + | ProcessRole::ActivationNixosRunner | ProcessRole::Gpu | ProcessRole::GpuRenderNode | ProcessRole::Audio @@ -357,7 +381,10 @@ impl ProductionProcessProviders { /// Return whether this node remains supervised after its start step. pub fn is_long_lived(node: &ProcessNode) -> bool { - !matches!(node.role, ProcessRole::SwtpmPreStartFlush) && Self::supports_node(node) + !matches!( + node.role, + ProcessRole::SwtpmPreStartFlush | ProcessRole::ActivationNixosRunner + ) && Self::supports_node(node) } /// Return the stable role key used by the broker and daemon stop paths. @@ -438,6 +465,13 @@ impl ProductionProcessProviders { node: &ProcessNode, timeout: Duration, ) -> Result { + let target = node + .execution_ref + .clone() + .unwrap_or_else(|| d2b_core::bundle_resolver::default_execution_ref(vm, &node.role)); + self.validate_execution_target(&ResourceRef::parse(&target).map_err(|_| { + "process-execution-target-invalid".to_owned() + })?)?; let ticket = self.ticket_with_timeout(vm, node, timeout)?; let provider = self.provider_for(node); let report = match provider { @@ -464,11 +498,13 @@ impl ProductionProcessProviders { spec: &ProcessSpec, timeout: Duration, ) -> Result { + self.validate_execution_target(spec.execution().execution_ref())?; let provider = managed_provider_from_ref(context.provider_ref)?; let ticket = resource_ticket( &self.bundle, - context, + &context, spec.execution(), + None, &serde_json::to_vec(spec).map_err(|_| "provider-ticket:serialization".to_owned())?, provider, timeout, @@ -503,11 +539,13 @@ impl ProductionProcessProviders { spec: &EphemeralProcessSpec, timeout: Duration, ) -> Result { + self.validate_execution_target(spec.execution().execution_ref())?; let provider = managed_provider_from_ref(context.provider_ref)?; let ticket = resource_ticket( &self.bundle, - context, + &context, spec.execution(), + spec.activation_input(), &serde_json::to_vec(spec).map_err(|_| "provider-ticket:serialization".to_owned())?, provider, timeout, @@ -648,6 +686,7 @@ impl ProductionProcessProviders { self.adopt_resource_with_execution( context, spec.execution(), + None, &serde_json::to_vec(spec).map_err(|_| "provider-ticket:serialization".to_owned())?, ) .await @@ -683,6 +722,7 @@ impl ProductionProcessProviders { self.adopt_resource_with_execution( context, spec.execution(), + spec.activation_input(), &serde_json::to_vec(spec).map_err(|_| "provider-ticket:serialization".to_owned())?, ) .await @@ -696,8 +736,9 @@ impl ProductionProcessProviders { spec: &ProcessSpec, ) -> Result { self.probe_resource_with_execution( - context, + &context, spec.execution(), + None, &serde_json::to_vec(spec).map_err(|_| "provider-ticket:serialization".to_owned())?, ) .await @@ -711,8 +752,9 @@ impl ProductionProcessProviders { spec: &EphemeralProcessSpec, ) -> Result { self.probe_resource_with_execution( - context, + &context, spec.execution(), + spec.activation_input(), &serde_json::to_vec(spec).map_err(|_| "provider-ticket:serialization".to_owned())?, ) .await @@ -730,6 +772,7 @@ impl ProductionProcessProviders { self.stop_resource_with_execution( context, spec.execution(), + None, &serde_json::to_vec(spec).map_err(|_| "provider-ticket:serialization".to_owned())?, term_timeout, kill_timeout, @@ -749,6 +792,7 @@ impl ProductionProcessProviders { self.stop_resource_with_execution( context, spec.execution(), + spec.activation_input(), &serde_json::to_vec(spec).map_err(|_| "provider-ticket:serialization".to_owned())?, term_timeout, kill_timeout, @@ -770,7 +814,7 @@ impl ProductionProcessProviders { else { return Ok(()); }; - if !resource_identity_matches(&managed, context) { + if !resource_identity_matches(&managed, &context) { return Err("provider-process-identity-changed".to_owned()); } let result = match managed.provider { @@ -812,13 +856,16 @@ impl ProductionProcessProviders { &self, context: ProcessResourceContext<'_>, execution: &d2b_contracts_resource::v3::process::ExecutionSpec, + activation_input: Option<&d2b_contracts_resource::v3::ActivationRunnerInput>, spec_bytes: &[u8], ) -> Result { + self.validate_execution_target(execution.execution_ref())?; let provider = managed_provider_from_ref(context.provider_ref)?; let ticket = resource_ticket( &self.bundle, - context, + &context, execution, + activation_input, spec_bytes, provider, Duration::from_secs(30), @@ -852,15 +899,18 @@ impl ProductionProcessProviders { async fn probe_resource_with_execution( &self, - context: ProcessResourceContext<'_>, + context: &ProcessResourceContext<'_>, execution: &d2b_contracts_resource::v3::process::ExecutionSpec, + activation_input: Option<&d2b_contracts_resource::v3::ActivationRunnerInput>, spec_bytes: &[u8], ) -> Result { + self.validate_execution_target(execution.execution_ref())?; let provider = managed_provider_from_ref(context.provider_ref)?; let ticket = resource_ticket( &self.bundle, context, execution, + activation_input, spec_bytes, provider, Duration::from_secs(30), @@ -903,10 +953,12 @@ impl ProductionProcessProviders { &self, context: ProcessResourceContext<'_>, execution: &d2b_contracts_resource::v3::process::ExecutionSpec, + activation_input: Option<&d2b_contracts_resource::v3::ActivationRunnerInput>, spec_bytes: &[u8], term_timeout: Duration, kill_timeout: Duration, ) -> Result { + self.validate_execution_target(execution.execution_ref())?; let managed = self .managed_resources .lock() @@ -933,11 +985,16 @@ impl ProductionProcessProviders { let deadline = Instant::now() + term_timeout; loop { match self - .probe_resource_with_execution(context, execution, spec_bytes) + .probe_resource_with_execution( + &context, + execution, + activation_input, + spec_bytes, + ) .await? { ProviderLiveness::Exited => { - self.finalize_resource(context).await?; + self.finalize_resource(context.clone()).await?; return Ok(false); } ProviderLiveness::Alive => {} @@ -964,11 +1021,16 @@ impl ProductionProcessProviders { let kill_deadline = Instant::now() + kill_timeout; loop { match self - .probe_resource_with_execution(context, execution, spec_bytes) + .probe_resource_with_execution( + &context, + execution, + activation_input, + spec_bytes, + ) .await? { ProviderLiveness::Exited => { - self.finalize_resource(context).await?; + self.finalize_resource(context.clone()).await?; return Ok(true); } ProviderLiveness::Alive | ProviderLiveness::Unknown => {} @@ -1450,8 +1512,9 @@ fn configuration_digest(label: &str, value: &str) -> ConfigurationDigest { fn resource_ticket( bundle: &BundleResolver, - context: ProcessResourceContext<'_>, + context: &ProcessResourceContext<'_>, execution: &d2b_contracts_resource::v3::process::ExecutionSpec, + activation_input: Option<&d2b_contracts_resource::v3::ActivationRunnerInput>, spec_bytes: &[u8], provider: ManagedProvider, timeout: Duration, @@ -1503,6 +1566,24 @@ fn resource_ticket( required_identity(provider), ) .map_err(|error| format!("provider-ticket:{}", error.code()))?; + let ticket = match context.guest_execution.as_ref() { + Some(binding) if execution.execution_ref().resource_type().as_str() == "Guest" => ticket + .with_guest_execution_binding(binding.clone()) + .map_err(|error| format!("provider-ticket:{}", error.code()))?, + Some(_) => { + return Err("provider-ticket:guest-binding-for-host".to_owned()); + } + None if execution.execution_ref().resource_type().as_str() == "Guest" => { + return Err("provider-ticket:guest-binding-missing".to_owned()); + } + None => ticket, + }; + let ticket = match activation_input { + Some(input) => ticket + .with_activation_input(input.clone()) + .map_err(|error| format!("provider-ticket:{}", error.code()))?, + None => ticket, + }; let domain = execution.domain().unwrap_or(ExecutionDomain::System); let sandbox = if provider == ManagedProvider::Systemd { let spec = execution.sandbox(); @@ -1586,7 +1667,12 @@ fn build_ticket( let process_name = stable_token(&node.id.0); let process_ref = ResourceRef::parse(&format!("{process_type}/{process_name}")) .map_err(|_| ProcessConformanceError::InvalidTicket)?; - let execution_ref = ResourceRef::parse(&format!("Guest/{vm}")) + let execution_ref = ResourceRef::parse( + &node + .execution_ref + .clone() + .unwrap_or_else(|| d2b_core::bundle_resolver::default_execution_ref(vm, &node.role)), + ) .map_err(|_| ProcessConformanceError::InvalidTicket)?; let owner_provider = BoundedToken::parse(provider_name).map_err(|_| ProcessConformanceError::InvalidTicket)?; @@ -1849,7 +1935,7 @@ mod tests { &provider_ref, ControllerGeneration::new(1).expect("controller generation"), ); - assert!(resource_identity_matches(&managed, context)); + assert!(resource_identity_matches(&managed, &context)); let stale_context = ProcessResourceContext::new( &resource_ref, &uid, @@ -1857,7 +1943,7 @@ mod tests { &provider_ref, ControllerGeneration::new(1).expect("controller generation"), ); - assert!(!resource_identity_matches(&managed, stale_context)); + assert!(!resource_identity_matches(&managed, &stale_context)); } #[test] diff --git a/packages/d2bd/src/process_resource_runtime.rs b/packages/d2bd/src/process_resource_runtime.rs index fdd218cd19..c08b6ff0cb 100644 --- a/packages/d2bd/src/process_resource_runtime.rs +++ b/packages/d2bd/src/process_resource_runtime.rs @@ -23,8 +23,12 @@ use d2b_contracts_resource::v3::{ ZoneRevision, process::{EphemeralProcessSpec, ProcessSpec, RestartClass}, }; +use d2b_process_conformance::GuestExecutionBinding; use d2b_resource_api::watch::ResourceWatch; -use d2b_resource_api::{RedbBackend, ResourceApiClient, service::UnavailableUpgradeDispatcher}; +use d2b_resource_api::{ + ResourceApiClient, ResourceStoreBackend, + service::{UnavailableUpgradeDispatcher, UpgradeDispatcher}, +}; use d2b_resource_store::{ StoreListRequest, StoreOperationContext, StoreProjection, StoreWatchRequest, StoredResource, }; @@ -72,6 +76,46 @@ impl core::fmt::Display for ProcessResourceRuntimeError { impl std::error::Error for ProcessResourceRuntimeError {} +#[async_trait::async_trait] +pub(crate) trait ProcessResourceClient: Send + Sync { + async fn update_status( + &self, + request: wire::UpdateStatusRequest, + ) -> wire::UpdateStatusResponse; + + async fn update_finalizers( + &self, + request: wire::UpdateFinalizersRequest, + ) -> wire::UpdateFinalizersResponse; + + async fn delete(&self, request: wire::DeleteRequest) -> wire::DeleteResponse; +} + +#[async_trait::async_trait] +impl ProcessResourceClient for ResourceApiClient +where + S: ResourceStoreBackend + 'static, + U: UpgradeDispatcher + 'static, +{ + async fn update_status( + &self, + request: wire::UpdateStatusRequest, + ) -> wire::UpdateStatusResponse { + ResourceApiClient::update_status(self, request).await + } + + async fn update_finalizers( + &self, + request: wire::UpdateFinalizersRequest, + ) -> wire::UpdateFinalizersResponse { + ResourceApiClient::update_finalizers(self, request).await + } + + async fn delete(&self, request: wire::DeleteRequest) -> wire::DeleteResponse { + ResourceApiClient::delete(self, request).await + } +} + #[derive(Debug, Clone, PartialEq, Eq)] enum DesiredProcess { Process(ProcessSpec), @@ -130,6 +174,7 @@ impl DesiredRecord { /// Durable generic process registry for one Zone. pub(crate) struct ProcessResourceRuntime { zone: ZoneId, + target: Option, providers: Arc, records: BTreeMap, terminal: BTreeSet, @@ -139,7 +184,8 @@ pub(crate) struct ProcessResourceRuntime { completed_at: BTreeMap, next_restart_at: BTreeMap, controller_generation: ControllerGeneration, - status_client: Option>>, + guest_execution: Option, + status_client: Option>, } impl core::fmt::Debug for ProcessResourceRuntime { @@ -155,8 +201,17 @@ impl core::fmt::Debug for ProcessResourceRuntime { impl ProcessResourceRuntime { /// Construct a registry over the daemon-owned fixed Providers. pub(crate) fn new(zone: ZoneId, providers: Arc) -> Self { + Self::new_for_target(zone, providers, None) + } + + pub(crate) fn new_for_target( + zone: ZoneId, + providers: Arc, + target: Option, + ) -> Self { Self { zone, + target, providers, records: BTreeMap::new(), terminal: BTreeSet::new(), @@ -167,6 +222,7 @@ impl ProcessResourceRuntime { next_restart_at: BTreeMap::new(), controller_generation: ControllerGeneration::new(1) .expect("controller generation one is valid"), + guest_execution: None, status_client: None, } } @@ -175,10 +231,14 @@ impl ProcessResourceRuntime { self.controller_generation = generation; } - pub(crate) fn set_status_client( - &mut self, - status_client: Arc>, - ) { + pub(crate) fn set_guest_execution_binding(&mut self, binding: GuestExecutionBinding) { + self.guest_execution = Some(binding); + } + + pub(crate) fn set_status_client(&mut self, status_client: Arc) + where + C: ProcessResourceClient + 'static, + { self.status_client = Some(status_client); } @@ -190,6 +250,7 @@ impl ProcessResourceRuntime { &record.provider_ref, self.controller_generation, ) + .with_guest_execution(self.guest_execution.as_ref()) } /// Reconcile a complete durable Process/EphemeralProcess snapshot. @@ -197,7 +258,7 @@ impl ProcessResourceRuntime { &mut self, snapshot: Vec, ) -> Result<(), ProcessResourceRuntimeError> { - let desired = decode_snapshot(&self.zone, snapshot)?; + let desired = decode_snapshot(&self.zone, self.target.as_ref(), snapshot)?; let desired_keys = desired.keys().cloned().collect::>(); let removed = self .records @@ -606,7 +667,7 @@ impl ProcessResourceRuntime { return Ok(record.clone()); }; update_status( - client, + client.as_ref(), record, phase, self.restart_counts @@ -628,7 +689,7 @@ impl ProcessResourceRuntime { if record.has_runtime_finalizer() { return Ok(record.clone()); } - update_finalizers(client, record, true).await + update_finalizers(client.as_ref(), record, true).await } async fn remove_finalizer( @@ -641,7 +702,7 @@ impl ProcessResourceRuntime { if !record.has_runtime_finalizer() { return Ok(record.clone()); } - update_finalizers(client, record, false).await + update_finalizers(client.as_ref(), record, false).await } fn ephemeral_ttl_elapsed(&self, key: &ResourceRef, record: &DesiredRecord) -> bool { @@ -672,7 +733,7 @@ impl ProcessResourceRuntime { let Some(client) = &self.status_client else { return Ok(()); }; - delete_resource(client, record).await + delete_resource(client.as_ref(), record).await } async fn probe_record( @@ -932,7 +993,7 @@ fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64) { } async fn update_status( - client: &ResourceApiClient, + client: &dyn ProcessResourceClient, record: &DesiredRecord, phase: ResourcePhase, restart_count: u32, @@ -1080,7 +1141,7 @@ fn status_payload( } async fn update_finalizers( - client: &ResourceApiClient, + client: &dyn ProcessResourceClient, record: &DesiredRecord, add: bool, ) -> Result { @@ -1123,7 +1184,7 @@ async fn update_finalizers( } async fn delete_resource( - client: &ResourceApiClient, + client: &dyn ProcessResourceClient, record: &DesiredRecord, ) -> Result<(), ProcessResourceRuntimeError> { let mut mutation = wire::Mutation::new(); @@ -1194,6 +1255,7 @@ fn map_provider_error(error: String) -> ProcessResourceRuntimeError { fn decode_snapshot( zone: &ZoneId, + target: Option<&ResourceRef>, resources: Vec, ) -> Result, ProcessResourceRuntimeError> { let mut desired = BTreeMap::new(); @@ -1209,6 +1271,23 @@ fn decode_snapshot( .provider_ref() .cloned() .ok_or(ProcessResourceRuntimeError::InvalidResource)?; + let execution_ref = envelope + .spec() + .base() + .get("executionRef") + .and_then(|value| match value { + CanonicalJsonValue::String(value) => ResourceRef::parse(value).ok(), + _ => None, + }) + .ok_or(ProcessResourceRuntimeError::InvalidResource)?; + let target_matches = if let Some(target) = target { + execution_ref == *target + } else { + execution_ref.resource_type().as_str() == "Host" + }; + if !target_matches { + continue; + } if provider_ref.resource_type().as_str() != "Provider" || !matches!( provider_ref.name().as_str(), @@ -1307,6 +1386,54 @@ pub(crate) async fn list_process_snapshot( Ok(resources) } +/// Relist generic Process resources through a session-bound Resource API +/// backend. This mirrors the concrete Zone-store helper while preserving the +/// backend's reconnect fence. +pub(crate) async fn list_process_snapshot_backend( + store: &S, + zone: &ZoneId, +) -> Result, ProcessResourceRuntimeError> { + let mut request = process_list_request(zone); + let mut resources = Vec::new(); + loop { + let result = store + .list(request.clone()) + .await + .map_err(|_| ProcessResourceRuntimeError::Store)?; + resources.extend(result.resources); + let Some(cursor) = result.next_cursor else { + break; + }; + request.cursor = Some(cursor); + } + Ok(resources) +} + +/// Run Guest-local Process/EphemeralProcess reconciliation for one +/// authenticated ComponentSession. The session-bound store is intentionally +/// relisted instead of opening a second transport or watch implementation; +/// reconnect fencing makes the loop stop at the first stale-session error. +pub(crate) async fn run_guest_process_reconciliation( + mut runtime: ProcessResourceRuntime, + store: Arc, + client: Arc>, + zone: ZoneId, +) where + S: ResourceStoreBackend + 'static, +{ + runtime.set_status_client(client); + loop { + let snapshot = match list_process_snapshot_backend(store.as_ref(), &zone).await { + Ok(snapshot) => snapshot, + Err(_) => return, + }; + if let Err(error) = runtime.reconcile(snapshot).await { + tracing::warn!(zone = %zone.as_str(), error = %error, "Guest Process reconciliation degraded"); + } + tokio::time::sleep(Duration::from_millis(250)).await; + } +} + /// Run the relist/watch reconciliation loop for one Zone. pub(crate) async fn run_process_watch( mut watch: ResourceWatch, diff --git a/packages/d2bd/src/resource_runtime.rs b/packages/d2bd/src/resource_runtime.rs index 411b864275..7aea3c3b5e 100644 --- a/packages/d2bd/src/resource_runtime.rs +++ b/packages/d2bd/src/resource_runtime.rs @@ -19,8 +19,9 @@ use std::{ use std::time::{SystemTime, UNIX_EPOCH}; use crate::activation_resource_runtime::{ - ActivationResourceRuntime, ActivationResourceRuntimeError, activation_watch_request, - list_activation_snapshot, run_activation_watch, + ActivationResourceRuntime, ActivationResourceRuntimeError, GuestActivationResourceClient, + activation_watch_request, list_activation_snapshot, run_activation_watch, + stored_resource_from_wire, }; use crate::audio_resource_runtime::{ AudioBindingRuntimeStatus, AudioResourceRuntime, AudioResourceRuntimeError, @@ -41,8 +42,8 @@ use d2b_contracts_resource::resource_proto as wire; #[cfg(test)] use d2b_contracts_resource::v3::{ConfigurationGeneration, ControllerGeneration}; use d2b_contracts_resource::v3::{ - ResourceEnvelope, ResourceGeneration, ResourceName, ResourcePhase, ResourceRef, - ResourceTypeName, ResourceUid, ZoneId, ZoneRevision, + CanonicalJsonValue, NixosGenerationSpec, ResourceEnvelope, ResourceGeneration, ResourceName, + ResourcePhase, ResourceRef, ResourceTypeName, ResourceUid, ZoneId, ZoneRevision, }; use d2b_contracts_resource::v3::{ host::{HOST_PROVIDER_REF, HostSpec}, @@ -97,8 +98,9 @@ use d2bd_runtime::resource_operator_activation::{ use d2bd_runtime::resource_runtime_support::{ AssignmentRegistry, SystemCoreReconcileResult, compatibility_error_envelope, configuration_cleanup_pending, current_status_timestamp, encode_public_get_response, - encode_public_list_response, ensure_bootstrap_host_resource, handler_phase_to_zone_phase, - host_phase_for_resource_count, initial_policy_snapshot, map_startup_error, + encode_public_list_response, encode_public_resource, ensure_bootstrap_host_resource, + handler_phase_to_zone_phase, host_phase_for_resource_count, initial_policy_snapshot, + map_startup_error, materialize_zone_resource_bundle, new_assignment_registry, public_list_request, public_operation_id, public_request_meta, register_system_core_session, runtime_authorizer, runtime_policy, store_identity, watch_needs_restart, zone_runtime_metadata, @@ -1784,7 +1786,7 @@ impl ZoneResourceRuntime { /// Relist and reconcile durable NixOS generation resources owned by this /// Zone. Activation effects remain behind the typed broker or the - /// authenticated guest-control bridge. + /// authenticated Guest ComponentSession route. pub(crate) async fn reconcile_activation_resources( &self, state: Arc, @@ -1795,16 +1797,45 @@ impl ZoneResourceRuntime { let snapshot = list_activation_snapshot(&self.store, &self.zone) .await .map_err(map_activation_runtime_error)?; + let process_snapshot = crate::process_resource_runtime::list_process_snapshot( + &self.store, + &self.zone, + ) + .await + .map_err(map_process_runtime_error)?; let runtime = match self.activation_runtime.lock() { Ok(mut guard) => guard.take(), Err(_) => return Err(ResourceRuntimeError::CapabilityUnavailable), }; let mut runtime = runtime.unwrap_or_else(|| ActivationResourceRuntime::new(self.zone.clone())); + runtime.clear_guest_clients(); if let Some(status_client) = &self.process_status_client { runtime.set_status_client(Arc::clone(status_client)); } - let result = runtime.reconcile(Arc::clone(&state), snapshot).await; + let guest_targets = guest_activation_targets(&snapshot); + let mut process_snapshot = process_snapshot; + for guest in guest_targets { + let Ok(session) = crate::connect_guest_component_session(&state, &guest).await + else { + continue; + }; + match list_guest_process_snapshot(&session, &self.zone, &guest).await { + Ok(resources) => { + runtime.set_guest_client( + guest.clone(), + Arc::new(GuestActivationResourceClient::new(Arc::clone(&session))), + ); + process_snapshot.extend(resources); + } + Err(()) => { + crate::invalidate_guest_component_session(&state, &guest).await; + } + } + } + let result = runtime + .reconcile(Arc::clone(&state), snapshot, process_snapshot) + .await; match self.activation_runtime.lock() { Ok(mut guard) => *guard = Some(runtime), Err(_) => return Err(ResourceRuntimeError::CapabilityUnavailable), @@ -2109,6 +2140,34 @@ impl ZoneResourceRuntime { .await; encode_public_list_response(response) } + "Create" => { + let request_wire = public_create_request(self, request, &operation_id).await?; + let response = client.create(request_wire).await; + encode_public_create_response(response) + } + "UpdateSpec" => { + let request_wire = + public_update_spec_request(&client, self, request, &operation_id).await?; + let response = client.update_spec(request_wire).await; + encode_public_update_spec_response(response) + } + "UpdateStatus" => { + let request_wire = + public_update_status_request(&client, self, request, &operation_id).await?; + let response = client.update_status(request_wire).await; + encode_public_update_status_response(response) + } + "UpdateFinalizers" => { + let request_wire = public_update_finalizers_request(self, request, &operation_id)?; + let response = client.update_finalizers(request_wire).await; + encode_public_update_finalizers_response(response) + } + "Delete" => { + let request_wire = + public_delete_request(self, request, &operation_id).await?; + let response = client.delete(request_wire).await; + encode_public_delete_response(response) + } _ => Err(ResourceRuntimeError::CapabilityUnavailable), } } @@ -3375,6 +3434,85 @@ fn map_process_runtime_error(error: ProcessResourceRuntimeError) -> ResourceRunt } } +pub(crate) fn guest_activation_targets(resources: &[StoredResource]) -> BTreeSet { + resources + .iter() + .filter(|resource| { + resource.resource_ref.resource_type().as_str() + == d2b_contracts_resource::v3::activation_nixos::NIXOS_GENERATION_RESOURCE_TYPE + }) + .filter_map(|resource| { + let envelope = ResourceEnvelope::from_json(&resource.canonical_json).ok()?; + let spec = serde_json::from_slice::( + &envelope.spec().base().to_canonical_bytes(), + ) + .ok()?; + (spec.execution_ref().resource_type().as_str() == "Guest") + .then(|| spec.execution_ref().name().as_str().to_owned()) + }) + .collect() +} + +pub(crate) async fn list_guest_process_snapshot( + session: &d2bd_runtime::guest_component_session::GuestComponentSessionClient, + zone: &ZoneId, + guest: &str, +) -> Result, ()> { + let mut request = wire::ListRequest::new(); + let mut meta = wire::RequestMeta::new(); + meta.operation_id = "activation-guest-process-relist".to_owned(); + meta.idempotency_key = meta.operation_id.clone(); + meta.correlation_id = meta.operation_id.clone(); + meta.trace_id = meta.operation_id.clone(); + meta.deadline_ms = 10_000; + request.meta = protobuf::MessageField::some(meta); + request.resource_types = vec!["Process".to_owned(), "EphemeralProcess".to_owned()]; + request.page_size = 256; + let mut projection = wire::Projection::new(); + projection.kind = protobuf::EnumOrUnknown::new( + wire::ProjectionKind::PROJECTION_KIND_FULL, + ); + request.projection = protobuf::MessageField::some(projection); + + let client = session.resource_service_client(); + let mut resources = Vec::new(); + loop { + let response = client + .list(ttrpc::context::Context::default(), &request) + .await + .map_err(|_| ())?; + if response.error.is_some() { + return Err(()); + } + for resource in &response.resources { + let resource = stored_resource_from_wire(resource).ok_or(())?; + if resource.zone != *zone { + return Err(()); + } + let expected_target = ResourceRef::parse(&format!("Guest/{guest}")).map_err(|_| ())?; + let envelope = ResourceEnvelope::from_json(&resource.canonical_json).map_err(|_| ())?; + let execution_ref = envelope + .spec() + .base() + .get("executionRef") + .and_then(|value| match value { + CanonicalJsonValue::String(value) => ResourceRef::parse(value).ok(), + _ => None, + }) + .ok_or(())?; + if execution_ref != expected_target { + return Err(()); + } + resources.push(resource); + } + let Some(cursor) = response.next_cursor.as_ref().cloned() else { + break; + }; + request.cursor = protobuf::MessageField::some(cursor); + } + Ok(resources) +} + fn map_activation_runtime_error(error: ActivationResourceRuntimeError) -> ResourceRuntimeError { match error { ActivationResourceRuntimeError::Store => ResourceRuntimeError::StoreReadFailed, @@ -3383,6 +3521,674 @@ fn map_activation_runtime_error(error: ActivationResourceRuntimeError) -> Resour } } +async fn public_create_request( + runtime: &ZoneResourceRuntime, + request: &Value, + operation_id: &str, +) -> Result { + let resource_type = request + .get("resourceType") + .and_then(Value::as_str) + .ok_or(ResourceRuntimeError::RequestInvalid) + .and_then(|value| { + ResourceTypeName::parse(value.to_owned()) + .map_err(|_| ResourceRuntimeError::RequestInvalid) + })?; + let input = request + .get("resource") + .or_else(|| request.get("spec")) + .ok_or(ResourceRuntimeError::RequestInvalid)?; + let (name, spec) = if is_resource_envelope(input) { + let name = input + .pointer("/metadata/name") + .and_then(Value::as_str) + .ok_or(ResourceRuntimeError::RequestInvalid)?; + let spec = input + .get("spec") + .cloned() + .ok_or(ResourceRuntimeError::RequestInvalid)?; + (name.to_owned(), spec) + } else { + let name = request + .get("resourceName") + .and_then(Value::as_str) + .or_else(|| { + input + .get("metadata") + .and_then(|value| value.get("name")) + .and_then(Value::as_str) + }) + .ok_or(ResourceRuntimeError::RequestInvalid)?; + (name.to_owned(), input.clone()) + }; + let payload = public_create_payload( + runtime, + &resource_type, + &name, + &spec, + request.get("ownerRef").and_then(Value::as_str), + ) + .await?; + let identity = public_identity(runtime, &resource_type, &name, None, None, None); + let mut mutation = wire::Mutation::new(); + mutation.kind = protobuf::EnumOrUnknown::new(wire::MutationKind::MUTATION_KIND_CREATE); + mutation.target = protobuf::MessageField::some(identity.clone()); + mutation.precondition = protobuf::MessageField::some(create_precondition()); + mutation.resource = protobuf::MessageField::some(public_resource_body(identity, payload)?); + apply_public_mutation_options(&mut mutation, request)?; + let mut result = wire::CreateRequest::new(); + result.meta = protobuf::MessageField::some(public_request_meta(operation_id)); + result.mutation = protobuf::MessageField::some(mutation); + Ok(result) +} + +async fn public_update_spec_request( + client: &ResourceApiClient, + runtime: &ZoneResourceRuntime, + request: &Value, + operation_id: &str, +) -> Result { + let target = public_target_ref(request)?; + let current = public_get_resource(client, runtime, &target, operation_id).await?; + let spec = request + .get("spec") + .cloned() + .ok_or(ResourceRuntimeError::RequestInvalid)?; + let payload = replace_public_field(¤t, "spec", spec)?; + let current_uid = public_uid(¤t)?; + let current_revision = public_revision(¤t)?; + let expected_revision = public_expected_revision(request)?.unwrap_or(current_revision); + let identity = public_identity( + runtime, + target.resource_type(), + target.name().as_str(), + Some(¤t_uid), + Some(public_generation(¤t)?), + Some(expected_revision), + ); + let mut mutation = public_body_mutation( + wire::MutationKind::MUTATION_KIND_UPDATE_SPEC, + identity, + exact_public_precondition(expected_revision, ¤t_uid), + payload, + )?; + apply_public_mutation_options(&mut mutation, request)?; + let mut result = wire::UpdateSpecRequest::new(); + result.meta = protobuf::MessageField::some(public_request_meta(operation_id)); + result.mutation = protobuf::MessageField::some(mutation); + Ok(result) +} + +async fn public_update_status_request( + client: &ResourceApiClient, + runtime: &ZoneResourceRuntime, + request: &Value, + operation_id: &str, +) -> Result { + let target = public_target_ref(request)?; + let current = public_get_resource(client, runtime, &target, operation_id).await?; + let status = request + .get("status") + .cloned() + .or_else(|| request.get("resource").and_then(|value| value.get("status")).cloned()) + .ok_or(ResourceRuntimeError::RequestInvalid)?; + let payload = replace_public_field(¤t, "status", status)?; + let current_uid = public_uid(¤t)?; + let current_revision = public_revision(¤t)?; + let expected_revision = public_expected_revision(request)?.unwrap_or(current_revision); + let identity = public_identity( + runtime, + target.resource_type(), + target.name().as_str(), + Some(¤t_uid), + Some(public_generation(¤t)?), + Some(expected_revision), + ); + let mutation = public_body_mutation( + wire::MutationKind::MUTATION_KIND_UPDATE_STATUS, + identity, + exact_public_precondition(expected_revision, ¤t_uid), + payload, + )?; + let mut result = wire::UpdateStatusRequest::new(); + result.meta = protobuf::MessageField::some(public_request_meta(operation_id)); + result.mutation = protobuf::MessageField::some(mutation); + Ok(result) +} + +fn public_update_finalizers_request( + runtime: &ZoneResourceRuntime, + request: &Value, + operation_id: &str, +) -> Result { + let target = public_target_ref(request)?; + let uid = request + .get("uid") + .and_then(Value::as_str) + .map(|value| ResourceUid::parse(value.to_owned())) + .transpose() + .map_err(|_| ResourceRuntimeError::RequestInvalid)?; + let expected_revision = + public_expected_revision(request)?.ok_or(ResourceRuntimeError::RequestInvalid)?; + let uid = uid.ok_or(ResourceRuntimeError::RequestInvalid)?; + let identity = public_identity( + runtime, + target.resource_type(), + target.name().as_str(), + Some(&uid), + None, + Some(expected_revision), + ); + let mut mutation = wire::Mutation::new(); + mutation.kind = + protobuf::EnumOrUnknown::new(wire::MutationKind::MUTATION_KIND_UPDATE_FINALIZERS); + mutation.target = protobuf::MessageField::some(identity); + mutation.precondition = + protobuf::MessageField::some(exact_public_precondition(expected_revision, &uid)); + mutation.add_finalizers = public_string_array(request, "addFinalizers")?; + mutation.remove_finalizers = public_string_array(request, "removeFinalizers")?; + if mutation.add_finalizers.is_empty() && mutation.remove_finalizers.is_empty() { + return Err(ResourceRuntimeError::RequestInvalid); + } + let mut result = wire::UpdateFinalizersRequest::new(); + result.meta = protobuf::MessageField::some(public_request_meta(operation_id)); + result.mutation = protobuf::MessageField::some(mutation); + Ok(result) +} + +async fn public_delete_request( + runtime: &ZoneResourceRuntime, + request: &Value, + operation_id: &str, +) -> Result { + let target = public_target_ref(request)?; + let expected_revision = public_expected_revision(request)?; + let uid = request + .get("uid") + .and_then(Value::as_str) + .map(|value| ResourceUid::parse(value.to_owned())) + .transpose() + .map_err(|_| ResourceRuntimeError::RequestInvalid)?; + let identity = public_identity( + runtime, + target.resource_type(), + target.name().as_str(), + uid.as_ref(), + None, + expected_revision, + ); + let mut mutation = wire::Mutation::new(); + mutation.kind = protobuf::EnumOrUnknown::new(wire::MutationKind::MUTATION_KIND_DELETE); + mutation.target = protobuf::MessageField::some(identity.clone()); + let precondition = match expected_revision { + Some(revision) => { + let uid = uid.ok_or(ResourceRuntimeError::RequestInvalid)?; + exact_public_precondition(revision, &uid) + } + None => { + let mut precondition = wire::Precondition::new(); + precondition.kind = + protobuf::EnumOrUnknown::new(wire::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION); + precondition.expected_revision = Some(1); + precondition + } + }; + mutation.precondition = protobuf::MessageField::some(precondition); + apply_public_mutation_options(&mut mutation, request)?; + let mut result = wire::DeleteRequest::new(); + result.meta = protobuf::MessageField::some(public_request_meta(operation_id)); + result.mutation = protobuf::MessageField::some(mutation); + Ok(result) +} + +fn public_target_ref(request: &Value) -> Result { + request + .get("resourceRef") + .and_then(Value::as_str) + .ok_or(ResourceRuntimeError::RequestInvalid) + .and_then(|value| { + ResourceRef::parse(value).map_err(|_| ResourceRuntimeError::RequestInvalid) + }) +} + +fn public_expected_revision(request: &Value) -> Result, ResourceRuntimeError> { + let Some(value) = request.get("expectedRevision") else { + return Ok(None); + }; + if value.is_null() { + return Ok(None); + } + let value = value + .as_u64() + .or_else(|| value.as_str().and_then(|value| value.parse().ok())) + .filter(|value| *value > 0) + .ok_or(ResourceRuntimeError::RequestInvalid)?; + Ok(Some(value)) +} + +fn public_uid(resource: &Value) -> Result { + resource + .pointer("/metadata/uid") + .and_then(Value::as_str) + .ok_or(ResourceRuntimeError::ResponseInvalid) + .and_then(|value| { + ResourceUid::parse(value.to_owned()).map_err(|_| ResourceRuntimeError::ResponseInvalid) + }) +} + +fn public_revision(resource: &Value) -> Result { + resource + .pointer("/metadata/revision") + .and_then(Value::as_u64) + .filter(|value| *value > 0) + .ok_or(ResourceRuntimeError::ResponseInvalid) +} + +fn public_generation(resource: &Value) -> Result { + resource + .pointer("/metadata/generation") + .and_then(Value::as_u64) + .filter(|value| *value > 0) + .ok_or(ResourceRuntimeError::ResponseInvalid) +} + +async fn public_get_resource( + client: &ResourceApiClient, + runtime: &ZoneResourceRuntime, + target: &ResourceRef, + operation_id: &str, +) -> Result { + let mut meta = public_request_meta(operation_id); + meta.deadline_ms = 30_000; + let response = client + .get(wire::GetRequest { + meta: protobuf::MessageField::some(meta), + target: protobuf::MessageField::some(public_identity( + runtime, + target.resource_type(), + target.name().as_str(), + None, + None, + None, + )), + projection: { + let mut projection = wire::Projection::new(); + projection.kind = + protobuf::EnumOrUnknown::new(wire::ProjectionKind::PROJECTION_KIND_FULL); + protobuf::MessageField::some(projection) + }, + special_fields: protobuf::SpecialFields::new(), + }) + .await; + if response.error.is_some() { + return Err(ResourceRuntimeError::RequestInvalid); + } + let resource = response + .resource + .as_ref() + .ok_or(ResourceRuntimeError::ResponseInvalid)?; + encode_public_resource(resource) +} + +fn public_identity( + runtime: &ZoneResourceRuntime, + resource_type: &ResourceTypeName, + name: &str, + uid: Option<&ResourceUid>, + generation: Option, + revision: Option, +) -> wire::ResourceIdentity { + wire::ResourceIdentity { + zone: runtime.zone.to_canonical_string(), + resource_type: resource_type.to_canonical_string(), + name: name.to_owned(), + uid: uid.map(|value| value.as_str().to_owned()), + generation, + revision, + special_fields: protobuf::SpecialFields::new(), + } +} + +fn create_precondition() -> wire::Precondition { + let mut precondition = wire::Precondition::new(); + precondition.kind = + protobuf::EnumOrUnknown::new(wire::PreconditionKind::PRECONDITION_KIND_CREATE_ABSENT); + precondition +} + +fn exact_public_precondition(revision: u64, uid: &ResourceUid) -> wire::Precondition { + let mut precondition = wire::Precondition::new(); + precondition.kind = + protobuf::EnumOrUnknown::new(wire::PreconditionKind::PRECONDITION_KIND_EXACT_REVISION); + precondition.expected_revision = Some(revision); + precondition.expected_uid = Some(uid.as_str().to_owned()); + precondition +} + +fn public_body_mutation( + kind: wire::MutationKind, + identity: wire::ResourceIdentity, + precondition: wire::Precondition, + payload: Vec, +) -> Result { + let mut mutation = wire::Mutation::new(); + mutation.kind = protobuf::EnumOrUnknown::new(kind); + mutation.target = protobuf::MessageField::some(identity.clone()); + mutation.precondition = protobuf::MessageField::some(precondition); + mutation.resource = protobuf::MessageField::some(public_resource_body(identity, payload)?); + Ok(mutation) +} + +fn public_resource_body( + identity: wire::ResourceIdentity, + payload: Vec, +) -> Result { + let envelope = + ResourceEnvelope::from_json(&payload).map_err(|_| ResourceRuntimeError::RequestInvalid)?; + let digest = envelope + .digest() + .map_err(|_| ResourceRuntimeError::RequestInvalid)?; + let mut body = wire::ResourceEnvelopeBytes::new(); + body.identity = protobuf::MessageField::some(identity); + body.canonical_json = payload; + body.payload_digest = digest; + Ok(body) +} + +fn apply_public_mutation_options( + mutation: &mut wire::Mutation, + request: &Value, +) -> Result<(), ResourceRuntimeError> { + mutation.wait_for_reconcile = request + .get("waitForReconcile") + .and_then(Value::as_bool) + .unwrap_or(false); + mutation.reconcile_deadline_ms = request + .get("reconcileDeadlineMs") + .and_then(|value| { + value + .as_u64() + .or_else(|| value.as_str().and_then(|value| value.parse().ok())) + }) + .unwrap_or(0); + if !mutation.wait_for_reconcile && mutation.reconcile_deadline_ms != 0 { + return Err(ResourceRuntimeError::RequestInvalid); + } + Ok(()) +} + +fn public_string_array( + request: &Value, + field: &str, +) -> Result, ResourceRuntimeError> { + let Some(value) = request.get(field) else { + return Ok(Vec::new()); + }; + let values = value + .as_array() + .ok_or(ResourceRuntimeError::RequestInvalid)?; + values + .iter() + .map(|value| { + value + .as_str() + .map(str::to_owned) + .ok_or(ResourceRuntimeError::RequestInvalid) + }) + .collect() +} + +fn is_resource_envelope(value: &Value) -> bool { + value.get("metadata").is_some() + && value.get("spec").is_some() + && (value.get("type").is_some() || value.get("apiVersion").is_some()) +} + +async fn public_create_payload( + runtime: &ZoneResourceRuntime, + resource_type: &ResourceTypeName, + name: &str, + spec: &Value, + owner_ref: Option<&str>, +) -> Result, ResourceRuntimeError> { + let metadata = runtime + .store + .runtime_metadata() + .await + .map_err(|_| ResourceRuntimeError::StoreReadFailed)?; + let timestamp = current_status_timestamp(); + let value = json!({ + "apiVersion": "resources.d2bus.org/v3", + "type": resource_type.to_canonical_string(), + "metadata": { + "configurationGeneration": metadata.policy_snapshot.active_configuration_revision.get(), + "createdAt": timestamp, + "deletionRequestedAt": null, + "finalizers": [], + "generation": 1, + "managedBy": "api", + "name": name, + "ownerRef": owner_ref, + "revision": 1, + "updatedAt": timestamp, + "zone": runtime.zone.as_str() + }, + "spec": spec, + "status": { + "completedAt": null, + "conditions": [], + "lastReconciledAt": null, + "observedGeneration": 0, + "outcome": null, + "phase": "Pending", + "resource": {}, + "startedAt": null, + "update": { + "dependencies": {"count": 0, "refs": []}, + "disruption": "None", + "lastAssessedAt": 0, + "observedGeneration": 0, + "operationId": null, + "owned": {"count": 0, "refs": []}, + "preserveState": true, + "reasons": [], + "state": "Unknown", + "targetGeneration": 1 + } + } + }); + if value + .get("spec") + .and_then(Value::as_object) + .is_none() + { + return Err(ResourceRuntimeError::RequestInvalid); + } + let bytes = serde_json::to_vec(&value).map_err(|_| ResourceRuntimeError::RequestInvalid)?; + let canonical = CanonicalJsonValue::parse(&bytes) + .map_err(|_| ResourceRuntimeError::RequestInvalid)? + .to_canonical_bytes(); + ResourceEnvelope::from_json(&canonical) + .map_err(|_| ResourceRuntimeError::RequestInvalid)?; + Ok(canonical) +} + +fn replace_public_field( + current: &Value, + field: &str, + replacement: Value, +) -> Result, ResourceRuntimeError> { + let mut value = current.clone(); + value + .as_object_mut() + .and_then(|root| root.get_mut(field)) + .map(|field_value| *field_value = replacement) + .ok_or(ResourceRuntimeError::RequestInvalid)?; + let bytes = serde_json::to_vec(&value).map_err(|_| ResourceRuntimeError::RequestInvalid)?; + let canonical = CanonicalJsonValue::parse(&bytes) + .map_err(|_| ResourceRuntimeError::RequestInvalid)? + .to_canonical_bytes(); + ResourceEnvelope::from_json(&canonical) + .map_err(|_| ResourceRuntimeError::RequestInvalid)?; + Ok(canonical) +} + +fn encode_public_create_response( + response: wire::CreateResponse, +) -> Result { + encode_public_mutation_response( + response.error.as_ref(), + response.resource.as_ref(), + None, + response.revision, + Some(response.disposition.enum_value().unwrap_or( + wire::ReconcileDisposition::RECONCILE_DISPOSITION_UNSPECIFIED, + )), + Some(response.status_persistence.enum_value().unwrap_or( + wire::StatusPersistence::STATUS_PERSISTENCE_UNSPECIFIED, + )), + response.last_persisted_status_revision, + response.reconcile_projection.as_ref(), + ) +} + +fn encode_public_update_spec_response( + response: wire::UpdateSpecResponse, +) -> Result { + encode_public_mutation_response( + response.error.as_ref(), + response.resource.as_ref(), + None, + response.revision, + Some(response.disposition.enum_value().unwrap_or( + wire::ReconcileDisposition::RECONCILE_DISPOSITION_UNSPECIFIED, + )), + Some(response.status_persistence.enum_value().unwrap_or( + wire::StatusPersistence::STATUS_PERSISTENCE_UNSPECIFIED, + )), + response.last_persisted_status_revision, + response.reconcile_projection.as_ref(), + ) +} + +fn encode_public_update_status_response( + response: wire::UpdateStatusResponse, +) -> Result { + encode_public_mutation_response( + response.error.as_ref(), + response.resource.as_ref(), + None, + response.revision, + None, + None, + None, + None, + ) +} + +fn encode_public_update_finalizers_response( + response: wire::UpdateFinalizersResponse, +) -> Result { + encode_public_mutation_response( + response.error.as_ref(), + response.resource.as_ref(), + None, + response.revision, + None, + None, + None, + None, + ) +} + +fn encode_public_delete_response( + response: wire::DeleteResponse, +) -> Result { + encode_public_mutation_response( + response.error.as_ref(), + None, + response.resource.as_ref(), + response.revision, + Some(response.disposition.enum_value().unwrap_or( + wire::ReconcileDisposition::RECONCILE_DISPOSITION_UNSPECIFIED, + )), + None, + None, + None, + ) +} + +fn encode_public_mutation_response( + error: Option<&wire::ResourceError>, + resource: Option<&wire::ResourceEnvelopeBytes>, + identity: Option<&wire::ResourceIdentity>, + revision: u64, + disposition: Option, + status_persistence: Option, + last_persisted_status_revision: Option, + reconcile_projection: Option<&wire::ResourceEnvelopeBytes>, +) -> Result { + if let Some(error) = error { + return Ok(d2bd_runtime::resource_runtime_support::public_api_error(error)); + } + let mut body = serde_json::Map::new(); + if let Some(resource) = resource { + body.insert("resource".to_owned(), encode_public_resource(resource)?); + } + if let Some(identity) = identity { + body.insert( + "resourceRef".to_owned(), + Value::String(format!("{}/{}", identity.resource_type, identity.name)), + ); + } + body.insert("revision".to_owned(), Value::from(revision)); + if let Some(disposition) = disposition.filter(|value| { + *value != wire::ReconcileDisposition::RECONCILE_DISPOSITION_UNSPECIFIED + }) { + body.insert( + "disposition".to_owned(), + Value::String( + match disposition { + wire::ReconcileDisposition::RECONCILE_DISPOSITION_CONVERGED => "Converged", + wire::ReconcileDisposition::RECONCILE_DISPOSITION_PROGRESSING => "Progressing", + wire::ReconcileDisposition::RECONCILE_DISPOSITION_BLOCKED => "Blocked", + wire::ReconcileDisposition::RECONCILE_DISPOSITION_UPGRADE_REQUIRED => { + "UpgradeRequired" + } + wire::ReconcileDisposition::RECONCILE_DISPOSITION_FAILED => "Failed", + wire::ReconcileDisposition::RECONCILE_DISPOSITION_UNSPECIFIED => "Unspecified", + } + .to_owned(), + ), + ); + } + if let Some(status_persistence) = status_persistence.filter(|value| { + *value != wire::StatusPersistence::STATUS_PERSISTENCE_UNSPECIFIED + }) { + body.insert( + "statusPersistence".to_owned(), + Value::String( + match status_persistence { + wire::StatusPersistence::STATUS_PERSISTENCE_PENDING => "pending", + wire::StatusPersistence::STATUS_PERSISTENCE_COMMITTED => "committed", + wire::StatusPersistence::STATUS_PERSISTENCE_UNSPECIFIED => "unspecified", + } + .to_owned(), + ), + ); + } + if let Some(revision) = last_persisted_status_revision { + body.insert("lastPersistedStatusRevision".to_owned(), Value::from(revision)); + } + if let Some(projection) = reconcile_projection { + body.insert( + "reconcileProjection".to_owned(), + encode_public_resource(projection)?, + ); + } + Ok(Value::Object(body)) +} + /// All Zone runtimes owned by one daemon. #[derive(Default)] pub struct ResourcePlane { @@ -4133,8 +4939,10 @@ mod tests { 1000, ) .await - .unwrap_err(); - assert_eq!(direct_delete, ResourceRuntimeError::CapabilityUnavailable); + .unwrap(); + assert_eq!(direct_delete["type"], "error"); + assert_eq!(direct_delete["error"]["kind"], "resource-conflict"); + assert_eq!(direct_delete["error"]["retryClass"], "reauthorize"); runtime.shutdown().await.unwrap(); } } diff --git a/packages/d2bd/src/tpm_effect_port.rs b/packages/d2bd/src/tpm_effect_port.rs index 6cd9a1e96d..60e1e3cf0b 100644 --- a/packages/d2bd/src/tpm_effect_port.rs +++ b/packages/d2bd/src/tpm_effect_port.rs @@ -182,12 +182,14 @@ impl<'a> LiveTpmEffectExecutor<'a> { provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, role, bundle_runner_intent_ref: intent, execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, runtime_allocations: Vec::new(), tracing_span_id: None, workload_identity: None, diff --git a/packages/d2bd/src/usbip_production.rs b/packages/d2bd/src/usbip_production.rs index 2884e0eef5..9e5cf372d7 100644 --- a/packages/d2bd/src/usbip_production.rs +++ b/packages/d2bd/src/usbip_production.rs @@ -220,6 +220,7 @@ impl<'a, G> DaemonUsbipDispatcher<'a, G> { execution_ref: None, execution_domain: None, user_ref: None, + guest_execution: None, workload_identity: None, vm_id: VmId::new(self.context.vm_id.clone()), role_id: RoleId::new("usbip-attach".to_owned()), @@ -229,6 +230,7 @@ impl<'a, G> DaemonUsbipDispatcher<'a, G> { provider_identity: None, template_identity: None, generation: None, + activation_input: None, sandbox_plan: None, role: RunnerRole::Usbip, bundle_runner_intent_ref: BundleOpId::new(self.context.runner_intent_ref.clone()), diff --git a/packages/d2bd/tests/guest_mode_component_session.rs b/packages/d2bd/tests/guest_mode_component_session.rs index 3a3a822a60..a91fad47ff 100644 --- a/packages/d2bd/tests/guest_mode_component_session.rs +++ b/packages/d2bd/tests/guest_mode_component_session.rs @@ -9,6 +9,7 @@ use d2bd_runtime::{ BootIdentity, GUEST_COMPONENT_SESSION_PORT, GUEST_COMPONENT_SESSION_PURPOSE, GuestIdentity, GuestRuntime, reject_legacy_guest_control_prelude, }, + guest_resource_runtime::{GuestResourceRuntime, GuestResourceRuntimeError}, target_runtime::{AdmissionBudget, AdmissionKind, AdmissionLimits, ControllerAssignmentKey}, }; use std::time::Instant; @@ -32,14 +33,33 @@ fn identity(generation: u64) -> GuestIdentity { .expect("Guest identity") } -fn runtime() -> GuestRuntime { - GuestRuntime::new( +async fn runtime() -> (GuestRuntime, tempfile::TempDir) { + let state_dir = tempfile::tempdir().expect("state directory"); + let runtime = GuestRuntime::new( identity(1), "/run/d2b/guest-broker.sock".into(), 997, AdmissionLimits::guest_default(), + state_dir.path(), ) - .expect("Guest runtime") + .await + .expect("Guest runtime"); + (runtime, state_dir) +} + +#[tokio::test] +async fn guest_resource_runtime_is_target_local() { + let state_dir = tempfile::tempdir().expect("state directory"); + let runtime = GuestResourceRuntime::new(identity(1), state_dir.path()) + .await + .expect("Guest resource runtime"); + assert!(runtime.is_target_local()); +} + +#[tokio::test] +async fn guest_runtime_owns_one_target_local_resource_runtime() { + let (runtime, _state_dir) = runtime().await; + assert!(runtime.resource_runtime().is_target_local()); } #[test] @@ -92,9 +112,9 @@ fn old_guest_control_prelude_is_rejected() { assert!(reject_legacy_guest_control_prelude(b"D2BGC-old").is_err()); } -#[test] -fn reconnect_revokes_stale_assignments_and_drops_the_new_lease() { - let runtime = runtime(); +#[tokio::test] +async fn reconnect_revokes_stale_assignments_and_drops_the_new_lease() { + let (runtime, _state_dir) = runtime().await; let first = runtime .admit_generation_for_tests(1) .expect("first session"); @@ -126,8 +146,25 @@ fn reconnect_revokes_stale_assignments_and_drops_the_new_lease() { } #[tokio::test] -async fn newer_generation_completes_the_real_component_session_path() { - let runtime = runtime(); +async fn disconnected_generation_cannot_be_reused() { + let (runtime, _state_dir) = runtime().await; + let lease = runtime + .admit_generation_for_tests(1) + .expect("first session"); + drop(lease); + assert!( + runtime.admit_generation_for_tests(1).is_err(), + "disconnect must advance the reconnect high-water mark" + ); +} + +#[tokio::test] +async fn authenticated_guest_session_binds_readiness_and_stale_binding_fails_closed() { + let (runtime, _state_dir) = runtime().await; + let previous = runtime + .admit_generation_for_tests(1) + .expect("initial Guest session"); + drop(previous); let parent_private_bytes = [2_u8; 32]; let guest_private_bytes = [3_u8; 32]; let parent_public = x25519_public_key(&parent_private_bytes).expect("parent public key"); @@ -137,9 +174,9 @@ async fn newer_generation_completes_the_real_component_session_path() { let policy = identity(2).endpoint_policy(); let (left, right) = tokio::io::duplex(16 * 1024); let parent = tokio::spawn(async move { - SessionEngine::establish_initiator( + SessionEngine::establish_initiator_with_generation_discovery( FramedVsockTransport::new(left), - policy, + d2b_session::contract::EndpointPolicyIdentity::from(&policy), HandshakeCredentials::Kk { local_private: parent_private, remote_public: guest_public, @@ -148,7 +185,7 @@ async fn newer_generation_completes_the_real_component_session_path() { ) .await }); - let (_, lease) = runtime + let (session, lease) = runtime .establish_component_session( FramedVsockTransport::new(right), guest_private, @@ -159,4 +196,23 @@ async fn newer_generation_completes_the_real_component_session_path() { let parent = parent.await.expect("parent task").expect("parent session"); assert_eq!(lease.generation(), 2); assert_eq!(parent.generation(), 2); + let state_dir = tempfile::tempdir().expect("state directory"); + let resource_runtime = GuestResourceRuntime::new(identity(2), state_dir.path()) + .await + .expect("target-local resource runtime"); + let resource_session = resource_runtime + .bind_session(&session.route_binding()) + .expect("authenticated session binds Resource API"); + assert_eq!(resource_session.generation(), 2); + + let invalid_state_dir = tempfile::tempdir().expect("state directory"); + let invalid_runtime = GuestResourceRuntime::new(identity(3), invalid_state_dir.path()) + .await + .expect("target-local resource runtime"); + assert_eq!( + invalid_runtime + .bind_session(&session.route_binding()) + .expect_err("stale session binding must fail closed"), + GuestResourceRuntimeError::SessionBinding + ); } diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json index f6d9cdadc0..c3ffba9f61 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 832eed4fbf..13d8ba6812 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -172,7 +172,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 454, + "resolveNodeCount": 455, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json index 6346ff91c3..ff22ef7a90 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 832eed4fbf..13d8ba6812 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -172,7 +172,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 454, + "resolveNodeCount": 455, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index b4bd13f7ed..9697146841 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index 855ed6eb62..bff5cd07ca 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 454, + "resolveNodeCount": 455, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index 2fa5a7f620..5191c1ebc0 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index 855ed6eb62..bff5cd07ca 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 454, + "resolveNodeCount": 455, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 10fb75a888..a9b93a233a 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index 03c8bcf216..e526f773b8 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 454, + "resolveNodeCount": 455, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index 83469e79aa..c3ff7906ca 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index 03c8bcf216..e526f773b8 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 454, + "resolveNodeCount": 455, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json index e2051b3fa1..455ff63931 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json index 832eed4fbf..13d8ba6812 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -172,7 +172,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 454, + "resolveNodeCount": 455, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json index 69bfa89266..1245c000b7 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json index 832eed4fbf..13d8ba6812 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -172,7 +172,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 454, + "resolveNodeCount": 455, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock index 2cebf75db9..428e0eac87 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/Cargo.lock @@ -963,6 +963,7 @@ dependencies = [ "d2b-contracts-zone-session", "d2b-core", "d2b-cutover", + "d2b-provider-config-nixos", "d2b-realm-core", "d2b-resource-client", "d2b-zone-routing", @@ -1322,6 +1323,7 @@ dependencies = [ "d2b-contracts-zone-session", "serde", "serde_json", + "sha2", ] @@ -1368,6 +1370,25 @@ dependencies = [ ] +[[package]] +name = "d2b-provider-config-nixos" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "base64", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "libc", + "rustix 0.38.44", + "schemars", + "serde", + "serde_json", + "sha2", + "ttrpc", +] + + [[package]] name = "d2b-provider-credential-entra" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json index 96cf25d39c..2832f10a70 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/closure.json @@ -30,6 +30,7 @@ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-credential-entra", "d2b-provider-credential-managed-identity", "d2b-provider-credential-secret-service", @@ -72,7 +73,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -786,6 +787,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "name": "d2b-provider-config-nixos", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-provider-credential-entra@0.0.0-bootstrap#path", "name": "d2b-provider-credential-entra", @@ -5668,6 +5677,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-activation-nixos@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-audio-pipewire@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -5842,6 +5857,78 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "async-trait@0.1.92#registry+https://github.com/rust-lang/crates.io-index", + "kind": "proc-macro", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "base64@0.22.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "libc@0.2.189#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "schemars@0.8.22#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "ttrpc@0.9.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-credential-entra@0.0.0-bootstrap#path", "to": "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -7822,6 +7909,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b@0.0.0-bootstrap#path", + "to": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b@0.0.0-bootstrap#path", "to": "d2b-realm-core@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json index 7f2f5f00f0..f28e92a1df 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,8 +2,8 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", - "policyPackageCount": 420, + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", + "policyPackageCount": 421, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", "aead@0.5.2#registry+https://github.com/rust-lang/crates.io-index", @@ -94,6 +94,7 @@ "d2b-provider-activation-nixos@0.0.0-bootstrap#path", "d2b-provider-audio-pipewire@0.0.0-bootstrap#path", "d2b-provider-clipboard-wayland@0.0.0-bootstrap#path", + "d2b-provider-config-nixos@0.0.0-bootstrap#path", "d2b-provider-credential-entra@0.0.0-bootstrap#path", "d2b-provider-credential-managed-identity@0.0.0-bootstrap#path", "d2b-provider-credential-secret-service@0.0.0-bootstrap#path", @@ -426,8 +427,8 @@ "zvariant_derive@5.15.0#registry+https://github.com/rust-lang/crates.io-index", "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], - "productionPackageCount": 420, - "resolveNodeCount": 454, + "productionPackageCount": 421, + "resolveNodeCount": 455, "roots": [ "d2b", "d2b-audit", @@ -454,6 +455,7 @@ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-credential-entra", "d2b-provider-credential-managed-identity", "d2b-provider-credential-secret-service", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock index 2cebf75db9..428e0eac87 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/Cargo.lock @@ -963,6 +963,7 @@ dependencies = [ "d2b-contracts-zone-session", "d2b-core", "d2b-cutover", + "d2b-provider-config-nixos", "d2b-realm-core", "d2b-resource-client", "d2b-zone-routing", @@ -1322,6 +1323,7 @@ dependencies = [ "d2b-contracts-zone-session", "serde", "serde_json", + "sha2", ] @@ -1368,6 +1370,25 @@ dependencies = [ ] +[[package]] +name = "d2b-provider-config-nixos" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "base64", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "libc", + "rustix 0.38.44", + "schemars", + "serde", + "serde_json", + "sha2", + "ttrpc", +] + + [[package]] name = "d2b-provider-credential-entra" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json index 5a829e9ec1..23d284a388 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/closure.json @@ -30,6 +30,7 @@ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-credential-entra", "d2b-provider-credential-managed-identity", "d2b-provider-credential-secret-service", @@ -72,7 +73,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -786,6 +787,14 @@ "checksum": null, "target": "aarch64-unknown-linux-gnu" }, + { + "id": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "name": "d2b-provider-config-nixos", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "aarch64-unknown-linux-gnu" + }, { "id": "d2b-provider-credential-entra@0.0.0-bootstrap#path", "name": "d2b-provider-credential-entra", @@ -5404,6 +5413,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-activation-nixos@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-audio-pipewire@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -5572,6 +5587,78 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "async-trait@0.1.92#registry+https://github.com/rust-lang/crates.io-index", + "kind": "proc-macro", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "base64@0.22.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "libc@0.2.189#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "schemars@0.8.22#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "ttrpc@0.9.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-credential-entra@0.0.0-bootstrap#path", "to": "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -7366,6 +7453,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b@0.0.0-bootstrap#path", + "to": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b@0.0.0-bootstrap#path", "to": "d2b-realm-core@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json index 7f2f5f00f0..f28e92a1df 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,8 +2,8 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", - "policyPackageCount": 420, + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", + "policyPackageCount": 421, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", "aead@0.5.2#registry+https://github.com/rust-lang/crates.io-index", @@ -94,6 +94,7 @@ "d2b-provider-activation-nixos@0.0.0-bootstrap#path", "d2b-provider-audio-pipewire@0.0.0-bootstrap#path", "d2b-provider-clipboard-wayland@0.0.0-bootstrap#path", + "d2b-provider-config-nixos@0.0.0-bootstrap#path", "d2b-provider-credential-entra@0.0.0-bootstrap#path", "d2b-provider-credential-managed-identity@0.0.0-bootstrap#path", "d2b-provider-credential-secret-service@0.0.0-bootstrap#path", @@ -426,8 +427,8 @@ "zvariant_derive@5.15.0#registry+https://github.com/rust-lang/crates.io-index", "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], - "productionPackageCount": 420, - "resolveNodeCount": 454, + "productionPackageCount": 421, + "resolveNodeCount": 455, "roots": [ "d2b", "d2b-audit", @@ -454,6 +455,7 @@ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-credential-entra", "d2b-provider-credential-managed-identity", "d2b-provider-credential-secret-service", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index 770bb45995..5f869d8d96 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 4b93ff6d9e..042e531b70 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", @@ -148,7 +148,7 @@ "zmij@1.0.23#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 129, - "resolveNodeCount": 499, + "resolveNodeCount": 500, "roots": [ "d2b-guest-shell-runner" ], diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index abda343c6c..df03d1649e 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 4b93ff6d9e..042e531b70 100644 --- a/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/aarch64-linux/aarch64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", @@ -148,7 +148,7 @@ "zmij@1.0.23#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 129, - "resolveNodeCount": 499, + "resolveNodeCount": 500, "roots": [ "d2b-guest-shell-runner" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json index 0800c66e72..3ea9ad0fae 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json index 49faecdbd3..dbdeee1357 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -172,7 +172,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 456, + "resolveNodeCount": 457, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json index 1da9b8f0f1..f3d71859a9 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json index 49faecdbd3..dbdeee1357 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-default-tests/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -172,7 +172,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 456, + "resolveNodeCount": 457, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json index 7a2f5a2b3d..d062f6d330 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json index 82ee3fbe0d..53eabf8517 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 456, + "resolveNodeCount": 457, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json index 6e2d490248..ffb80d6d7c 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json index 82ee3fbe0d..53eabf8517 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-fake-backends-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "fake-backends" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 456, + "resolveNodeCount": 457, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json index 0654cc7bd6..f73a799f15 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json index 35c7429596..9ffb3b71f2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 456, + "resolveNodeCount": 457, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json index 8494ca69ad..d17d857d0c 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json index 35c7429596..9ffb3b71f2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-layer1-bootstrap-tests/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "layer1-bootstrap" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -174,7 +174,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 456, + "resolveNodeCount": 457, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json index f7aacb8b97..12ce33a9d2 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json index 49faecdbd3..dbdeee1357 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/policy/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -172,7 +172,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 456, + "resolveNodeCount": 457, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json index 53202f782e..6032353380 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/closure.json @@ -10,7 +10,7 @@ "features": [], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json index 49faecdbd3..dbdeee1357 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/broker-production/production/metadata.json @@ -2,7 +2,7 @@ "authority": "cargo-locked-metadata", "defaultFeatures": false, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 165, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -172,7 +172,7 @@ "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 134, - "resolveNodeCount": 456, + "resolveNodeCount": 457, "roots": [ "d2b-broker" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock index 2cebf75db9..428e0eac87 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/Cargo.lock @@ -963,6 +963,7 @@ dependencies = [ "d2b-contracts-zone-session", "d2b-core", "d2b-cutover", + "d2b-provider-config-nixos", "d2b-realm-core", "d2b-resource-client", "d2b-zone-routing", @@ -1322,6 +1323,7 @@ dependencies = [ "d2b-contracts-zone-session", "serde", "serde_json", + "sha2", ] @@ -1368,6 +1370,25 @@ dependencies = [ ] +[[package]] +name = "d2b-provider-config-nixos" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "base64", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "libc", + "rustix 0.38.44", + "schemars", + "serde", + "serde_json", + "sha2", + "ttrpc", +] + + [[package]] name = "d2b-provider-credential-entra" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json index b340cb24e6..1ce1ab46f5 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/closure.json @@ -30,6 +30,7 @@ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-credential-entra", "d2b-provider-credential-managed-identity", "d2b-provider-credential-secret-service", @@ -72,7 +73,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -802,6 +803,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "name": "d2b-provider-config-nixos", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-provider-credential-entra@0.0.0-bootstrap#path", "name": "d2b-provider-credential-entra", @@ -5720,6 +5729,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-activation-nixos@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-audio-pipewire@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -5894,6 +5909,78 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "async-trait@0.1.92#registry+https://github.com/rust-lang/crates.io-index", + "kind": "proc-macro", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "base64@0.22.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "libc@0.2.189#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "schemars@0.8.22#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "ttrpc@0.9.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-credential-entra@0.0.0-bootstrap#path", "to": "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -7874,6 +7961,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b@0.0.0-bootstrap#path", + "to": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b@0.0.0-bootstrap#path", "to": "d2b-realm-core@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json index f7ef9a0bf7..0e095cb874 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/policy/metadata.json @@ -2,8 +2,8 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", - "policyPackageCount": 422, + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", + "policyPackageCount": 423, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", "aead@0.5.2#registry+https://github.com/rust-lang/crates.io-index", @@ -96,6 +96,7 @@ "d2b-provider-activation-nixos@0.0.0-bootstrap#path", "d2b-provider-audio-pipewire@0.0.0-bootstrap#path", "d2b-provider-clipboard-wayland@0.0.0-bootstrap#path", + "d2b-provider-config-nixos@0.0.0-bootstrap#path", "d2b-provider-credential-entra@0.0.0-bootstrap#path", "d2b-provider-credential-managed-identity@0.0.0-bootstrap#path", "d2b-provider-credential-secret-service@0.0.0-bootstrap#path", @@ -428,8 +429,8 @@ "zvariant_derive@5.15.0#registry+https://github.com/rust-lang/crates.io-index", "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], - "productionPackageCount": 422, - "resolveNodeCount": 456, + "productionPackageCount": 423, + "resolveNodeCount": 457, "roots": [ "d2b", "d2b-audit", @@ -456,6 +457,7 @@ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-credential-entra", "d2b-provider-credential-managed-identity", "d2b-provider-credential-secret-service", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock index 2cebf75db9..428e0eac87 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/Cargo.lock @@ -963,6 +963,7 @@ dependencies = [ "d2b-contracts-zone-session", "d2b-core", "d2b-cutover", + "d2b-provider-config-nixos", "d2b-realm-core", "d2b-resource-client", "d2b-zone-routing", @@ -1322,6 +1323,7 @@ dependencies = [ "d2b-contracts-zone-session", "serde", "serde_json", + "sha2", ] @@ -1368,6 +1370,25 @@ dependencies = [ ] +[[package]] +name = "d2b-provider-config-nixos" +version = "0.0.0-bootstrap" +dependencies = [ + "async-trait", + "base64", + "d2b-contracts-control", + "d2b-contracts-resource", + "d2b-contracts-zone-session", + "libc", + "rustix 0.38.44", + "schemars", + "serde", + "serde_json", + "sha2", + "ttrpc", +] + + [[package]] name = "d2b-provider-credential-entra" version = "0.0.0-bootstrap" diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json index 31e8856aef..7973eb35ae 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/closure.json @@ -30,6 +30,7 @@ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-credential-entra", "d2b-provider-credential-managed-identity", "d2b-provider-credential-secret-service", @@ -72,7 +73,7 @@ "features": [], "default_features": true, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", @@ -802,6 +803,14 @@ "checksum": null, "target": "x86_64-unknown-linux-gnu" }, + { + "id": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "name": "d2b-provider-config-nixos", + "version": "0.0.0-bootstrap", + "source": null, + "checksum": null, + "target": "x86_64-unknown-linux-gnu" + }, { "id": "d2b-provider-credential-entra@0.0.0-bootstrap#path", "name": "d2b-provider-credential-entra", @@ -5456,6 +5465,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-activation-nixos@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-audio-pipewire@0.0.0-bootstrap#path", "to": "d2b-contracts-resource@0.0.0-bootstrap#path", @@ -5624,6 +5639,78 @@ "kind": "normal", "target": null }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "async-trait@0.1.92#registry+https://github.com/rust-lang/crates.io-index", + "kind": "proc-macro", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "base64@0.22.1#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-control@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-resource@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "d2b-contracts-zone-session@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "libc@0.2.189#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "rustix@0.38.44#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "schemars@0.8.22#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "serde@1.0.229#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "serde_json@1.0.151#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "sha2@0.10.9#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, + { + "from": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "to": "ttrpc@0.9.0#registry+https://github.com/rust-lang/crates.io-index", + "kind": "normal", + "target": null + }, { "from": "d2b-provider-credential-entra@0.0.0-bootstrap#path", "to": "d2b-contracts-provider@0.0.0-bootstrap#path", @@ -7418,6 +7505,12 @@ "kind": "normal", "target": null }, + { + "from": "d2b@0.0.0-bootstrap#path", + "to": "d2b-provider-config-nixos@0.0.0-bootstrap#path", + "kind": "normal", + "target": null + }, { "from": "d2b@0.0.0-bootstrap#path", "to": "d2b-realm-core@0.0.0-bootstrap#path", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json index f7ef9a0bf7..0e095cb874 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-gnu/main-product/production/metadata.json @@ -2,8 +2,8 @@ "authority": "cargo-locked-metadata", "defaultFeatures": true, "features": [], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", - "policyPackageCount": 422, + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", + "policyPackageCount": 423, "policyPackageIds": [ "adler2@2.0.1#registry+https://github.com/rust-lang/crates.io-index", "aead@0.5.2#registry+https://github.com/rust-lang/crates.io-index", @@ -96,6 +96,7 @@ "d2b-provider-activation-nixos@0.0.0-bootstrap#path", "d2b-provider-audio-pipewire@0.0.0-bootstrap#path", "d2b-provider-clipboard-wayland@0.0.0-bootstrap#path", + "d2b-provider-config-nixos@0.0.0-bootstrap#path", "d2b-provider-credential-entra@0.0.0-bootstrap#path", "d2b-provider-credential-managed-identity@0.0.0-bootstrap#path", "d2b-provider-credential-secret-service@0.0.0-bootstrap#path", @@ -428,8 +429,8 @@ "zvariant_derive@5.15.0#registry+https://github.com/rust-lang/crates.io-index", "zvariant_utils@4.2.0#registry+https://github.com/rust-lang/crates.io-index" ], - "productionPackageCount": 422, - "resolveNodeCount": 456, + "productionPackageCount": 423, + "resolveNodeCount": 457, "roots": [ "d2b", "d2b-audit", @@ -456,6 +457,7 @@ "d2b-provider-activation-nixos", "d2b-provider-audio-pipewire", "d2b-provider-clipboard-wayland", + "d2b-provider-config-nixos", "d2b-provider-credential-entra", "d2b-provider-credential-managed-identity", "d2b-provider-credential-secret-service", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json index f40ca32d01..0cda703eba 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json index 04fba978f8..2506adc901 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/policy/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", @@ -148,7 +148,7 @@ "zmij@1.0.23#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 129, - "resolveNodeCount": 501, + "resolveNodeCount": 502, "roots": [ "d2b-guest-shell-runner" ], diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json index c1d1ae7baa..8aa52b2ab4 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/closure.json @@ -12,7 +12,7 @@ ], "default_features": false, "source_authority": "Cargo.lock", - "lock_sha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lock_sha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "packages": [ { "id": "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", diff --git a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json index 04fba978f8..2506adc901 100644 --- a/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json +++ b/packages/policy-inputs/x86_64-linux/x86_64-unknown-linux-musl/guest-shell-runner-static/production/metadata.json @@ -4,7 +4,7 @@ "features": [ "real-libshpool" ], - "lockSha256": "28eeac68160fb5df809cdf34b92de5d9cdf12bbe17bd6729cf736d11df4c86c5", + "lockSha256": "53711dcbafa7bdc694d887661552dc89968cdfa3036cd2e39e1f543c2d5c459b", "policyPackageCount": 139, "policyPackageIds": [ "aho-corasick@1.1.5#registry+https://github.com/rust-lang/crates.io-index", @@ -148,7 +148,7 @@ "zmij@1.0.23#registry+https://github.com/rust-lang/crates.io-index" ], "productionPackageCount": 129, - "resolveNodeCount": 501, + "resolveNodeCount": 502, "roots": [ "d2b-guest-shell-runner" ], From 80dac63703e542ca995f0662804237d4498088c4 Mon Sep 17 00:00:00 2001 From: John Vicondoa Date: Sun, 23 Aug 2026 23:38:57 -0700 Subject: [PATCH 21/56] bazel: remove heavy-gate orchestration Run public validation lanes directly, compile Rust tests through BuildBuddy, keep test actions and Nix checks local, and retire obsolete source-policy and locality suites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .bazelrc | 2 + Makefile | 171 +- bazel/checks/BUILD.bazel | 16 - bazel/checks/policy/BUILD.bazel | 1 - bazel/checks/rust/BUILD.bazel | 19 - changelog.d/2026-08-21-v3-guest-u6.md | 15 + nixos-modules/host-activation.nix | 56 - nixos-modules/minijail-profiles.nix | 12 +- packages/d2b-bus/BUILD.bazel | 2 +- packages/d2b-host/BUILD.bazel | 2 +- packages/d2b-unsafe-local-helper/BUILD.bazel | 4 +- packages/d2b/BUILD.bazel | 3 +- packages/d2bd/BUILD.bazel | 9 +- packages/xtask/BUILD.bazel | 20 +- packages/xtask/Cargo.toml | 8 +- packages/xtask/src/heavy_gate.rs | 3593 ----------------- packages/xtask/src/main.rs | 4 +- packages/xtask/tests/buildbuddy_config.rs | 189 +- packages/xtask/tests/heavy_gate_reexec.rs | 267 -- .../containers/ubuntu-host-check.sh | 11 - .../distro-matrix/ubuntu-2404-tier1.sh | 11 - tests/integration/live/audio.sh | 10 - tests/integration/live/audit-forwarding.sh | 9 - tests/integration/live/cutover-real-host.sh | 11 +- tests/integration/live/d2b-store.sh | 9 - tests/integration/live/live-vm-smoke.sh | 10 - tests/integration/live/network-isolation.sh | 9 - .../live/swtpm-persistence-smoke.sh | 10 - .../live/usbip-guestd-lifecycle.sh | 9 - tests/test-integration.sh | 10 - tests/tools/heavy-gate-reexec.sh | 99 - tests/tools/no-bash-ast-walker/BUILD.bazel | 46 - tests/tools/no-bash-ast-walker/Cargo.lock | 45 - tests/tools/no-bash-ast-walker/Cargo.toml | 14 - tests/tools/no-bash-ast-walker/src/lib.rs | 87 - tests/tools/no-bash-ast-walker/src/main.rs | 28 - .../no-bash-ast-walker/tests/no_bash_ast.rs | 40 - tests/unit/gates/performance-budgets.sh | 12 - 38 files changed, 74 insertions(+), 4799 deletions(-) delete mode 100644 packages/xtask/src/heavy_gate.rs delete mode 100644 packages/xtask/tests/heavy_gate_reexec.rs delete mode 100644 tests/tools/heavy-gate-reexec.sh delete mode 100644 tests/tools/no-bash-ast-walker/BUILD.bazel delete mode 100644 tests/tools/no-bash-ast-walker/Cargo.lock delete mode 100644 tests/tools/no-bash-ast-walker/Cargo.toml delete mode 100644 tests/tools/no-bash-ast-walker/src/lib.rs delete mode 100644 tests/tools/no-bash-ast-walker/src/main.rs delete mode 100644 tests/tools/no-bash-ast-walker/tests/no_bash_ast.rs diff --git a/.bazelrc b/.bazelrc index 0de2f6eb3f..4c17a42109 100644 --- a/.bazelrc +++ b/.bazelrc @@ -37,6 +37,7 @@ build:remote --extra_execution_platforms=@toolchains_buildbuddy//platforms:linux build:remote --extra_toolchains=@toolchains_buildbuddy//toolchains/cc:ubuntu_gcc_x86_64 build:remote --action_env=PATH=/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin build:remote --test_env=PATH=/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +build:remote --strategy=TestRunner=local build:remote --remote_download_minimal build:remote --jobs=50 build:remote --remote_cache_compression @@ -56,6 +57,7 @@ build:trusted-seed --extra_execution_platforms=@toolchains_buildbuddy//platforms build:trusted-seed --extra_toolchains=@toolchains_buildbuddy//toolchains/cc:ubuntu_gcc_x86_64 build:trusted-seed --action_env=PATH=/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin build:trusted-seed --test_env=PATH=/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +build:trusted-seed --strategy=TestRunner=local build:trusted-seed --remote_upload_local_results=true build:trusted-seed --remote_cache_async=false build:trusted-seed --remote_download_minimal diff --git a/Makefile b/Makefile index aa49e95ba5..5f709ccab6 100644 --- a/Makefile +++ b/Makefile @@ -11,21 +11,18 @@ D2B_MAKE_BAZEL_TARGETS := \ check check-fast check-tier0 bazel-check test-unit \ test-lint test-rust test-rust-main test-rust-broker \ - test-rust-guest-shell-runner test-rust-local test-rust-no-bash-ast \ + test-rust-guest-shell-runner \ test-rust-schema test-rust-supply-chain test-rust-leaf-main-workspace \ test-rust-leaf-schema test-rust-leaf-fixture-contracts test-rust-leaf-broker \ - test-rust-leaf-guest-shell-runner test-rust-leaf-no-bash-ast \ + test-rust-leaf-guest-shell-runner \ test-rust-leaf-supply-chain test-fixture-contracts test-proofs test-flake \ test-flake-realized test-flake-aarch64 test-flake-x86 test-nix-unit \ test-performance-budgets test-drift test-policy test-changelog D2B_MAKE_LOCAL_TARGETS := \ check-ci test-integration test-host-integration perf \ - pre-tag smoke-lite heavy-check heavy-test-integration \ - heavy-test-host-integration heavy-flake-check heavy-lane-integration \ - heavy-lane-host-integration heavy-lane-perf heavy-lane-pre-tag \ - heavy-lane-smoke-lite heavy-lane-guard + pre-tag smoke-lite heavy-check heavy-flake-check # Meta helpers that invoke Bazel directly but are not Layer-1 test aliases. -D2B_MAKE_UTILITY_TARGETS := heavy-gate-build changelog-fold +D2B_MAKE_UTILITY_TARGETS := changelog-fold D2B_MAKE_GOALS := $(if $(strip $(MAKECMDGOALS)),$(MAKECMDGOALS),$(.DEFAULT_GOAL)) D2B_MAKE_CLASSIFIED_GOALS := $(filter \ @@ -79,23 +76,19 @@ SHELL := $(CURDIR)/tests/tools/scrub-shell-environment bazel-check \ test-unit \ test-lint test-rust test-rust-main \ - test-rust-broker test-rust-guest-shell-runner test-rust-local test-rust-no-bash-ast \ + test-rust-broker test-rust-guest-shell-runner \ test-rust-schema test-rust-supply-chain \ test-rust-leaf-main-workspace \ test-rust-leaf-schema \ test-rust-leaf-fixture-contracts test-rust-leaf-broker \ - test-rust-leaf-guest-shell-runner test-rust-leaf-no-bash-ast \ + test-rust-leaf-guest-shell-runner \ test-rust-leaf-supply-chain \ test-fixture-contracts test-proofs test-flake test-flake-realized \ test-flake-aarch64 test-flake-x86 test-nix-unit \ test-performance-budgets \ test-drift test-policy test-changelog \ test-integration test-host-integration perf \ - heavy-lane-guard heavy-lane-integration heavy-lane-host-integration \ - heavy-lane-perf \ - heavy-lane-pre-tag heavy-lane-smoke-lite \ - heavy-gate-build heavy-gate-provision heavy-check heavy-flake-check \ - heavy-test-integration heavy-test-host-integration \ + heavy-check heavy-flake-check \ clean # Current Nix system double, used to address per-system flake.checks attrs. @@ -112,8 +105,8 @@ SYSTEM ?= $(shell nix eval --extra-experimental-features 'nix-command flakes' \ # make test- focused Bazel suite. # make test-integration type-9 container integration; local host/manual pre-PR. # make test-host-integration type-10 runNixOSTest; local NixOS/KVM pre-PR. -# make heavy- the same lane, serialized through the two-slot -# per-uid heavy-gate semaphore (see "Heavy lanes"). +# make heavy-check full Layer-1 check. +# make heavy-flake-check full flake realization. # =========================================================================== ## check-ci - run the Layer-1 gate, then the conditional container lane. @@ -146,16 +139,8 @@ $(D2B_MAKE_BAZEL_TARGETS): # Sub-targets. Each target is a thin alias over one public Bazel suite. # =========================================================================== -## test-integration - L2 podman container integration tests. Public heavy lane: -## it acquires a heavy-gate slot, then runs the raw work behind the gate so it -## can never oversubscribe a concurrent lane, even when invoked directly or via -## `make check-ci`. -test-integration: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-integration - -## heavy-lane-integration - the raw L2 container work. Internal: reachable only -## from inside the gate (see heavy-lane-guard). -heavy-lane-integration: heavy-lane-guard +## test-integration - L2 podman container integration tests. +test-integration: bash tests/test-integration.sh # =========================================================================== @@ -170,12 +155,8 @@ heavy-lane-integration: heavy-lane-guard ## successor to the `D2B_LIVE`-against-the-real-host scripts. Needs KVM (a local ## NixOS host; TCG software emulation is the slow fallback when /dev/kvm is ## absent). x86_64-linux only (a same-system VM builder is required). -## Public heavy lane: acquires a slot, then runs the raw work behind the gate. ## Set D2B_VM_CHECK= to build one named vmChecks entry. -test-host-integration: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-host-integration - -heavy-lane-host-integration: heavy-lane-guard +test-host-integration: @set -eu; \ system="$$(nix eval --raw --impure --expr builtins.currentSystem)"; \ if [ "$$system" != "x86_64-linux" ]; then \ @@ -270,136 +251,30 @@ heavy-lane-host-integration: heavy-lane-guard echo "==> nix build $$*"; \ nix build --no-link --print-build-logs "$$@" -## Public heavy lanes: acquire a slot, then run the raw work behind the gate. -perf: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-perf - -heavy-lane-perf: heavy-lane-guard +## perf - run the advisory performance budget suite. +perf: $(BAZEL_RUN) //bazel/checks:test-performance-budgets -## heavy-lane-guard - fail closed when a heavy-lane internal target is invoked -## outside the gate. It does not trust the mere presence of D2B_HEAVY_GATE -## (any process can export that); instead it asks the wrapper to verify that -## this process genuinely holds a slot via its open file description lock. -## -## verify-slot reports its verdict purely through its exit status, so branch on -## the typed codes rather than collapsing every nonzero status into one: -## -## 0 a genuinely held slot -> proceed -## 3 no slot is held -> reacquire by running the PUBLIC lane -## (which acquires a slot and re-runs -## this lane through the gate). A shared -## prerequisite cannot exec that itself -## without double-running the parent -## recipe, so guide the operator to the -## acquiring entrypoint and fail closed -## with the typed unheld code. -## * the verifier itself malfunctioned -> propagate the exact code unchanged -## and fail closed -## -## Collapsing 3 and every malfunction into one "outside the semaphore" exit hid -## a broken gate behind a slot-bypass message; keeping the codes distinct lets a -## caller tell "ran the raw target directly" apart from "the verifier is broken". -heavy-lane-guard: heavy-gate-build - @rc=0; $(HEAVY_GATE_BIN) heavy-gate verify-slot || rc=$$?; \ - if [ "$$rc" -eq 0 ]; then \ - exit 0; \ - elif [ "$$rc" -eq 3 ]; then \ - echo "heavy lane invoked outside the heavy-gate semaphore (no slot held)." >&2; \ - echo "Run the public lane (e.g. 'make test-integration'), which acquires a slot" >&2; \ - echo "and re-runs this lane through the gate; do not run the internal target directly." >&2; \ - exit "$$rc"; \ - else \ - echo "heavy-gate verify-slot failed closed (exit $$rc); refusing to run heavy work unsynchronised." >&2; \ - exit "$$rc"; \ - fi - -# =========================================================================== -# Heavy lanes. -# -# Every Layer-2, host-integration, live, and perf-heavy command runs -# through ONE semaphore: `xtask heavy-gate`. It grants two slots per uid -# via open file description locks, so concurrent lanes cannot oversubscribe the -# shared Nix store or KVM device. Do not add a second -# lock file, sleep-and-retry loop, or per-crate heavy-lane guard. -# -# Run the heavy-* target instead of the bare target whenever another heavy lane -# might be running; the bare targets stay available for a serial console. -# =========================================================================== - BAZEL_BIN ?= $(if $(D2B_BAZEL_BIN),$(D2B_BAZEL_BIN),bazel) -HEAVY_GATE_BIN := $(CURDIR)/bazel-bin/packages/xtask/xtask -HEAVY_GATE = $(HEAVY_GATE_BIN) heavy-gate -- - -## heavy-gate-build - build the semaphore wrapper through its Bazel owner. -heavy-gate-build: - '$(BAZEL_BIN)' build --config=local //packages/xtask:xtask - -## heavy-gate-provision - create or repair the protected slot namespace for the -## current numeric uid without resolving a user name through NSS. This is the -## explicit post-login path for network-backed users and the developer setup -## path on hosts that do not consume the NixOS module. Because /run is a tmpfs, -## run it once per boot when the gate reports missing provisioning. It never -## creates a fallback under a user-owned root. -heavy-gate-provision: - @target_uid="$$(id -u)"; \ - sudo -- sh -eu -c '\ - target_uid="$$1"; root=/run/d2b-heavy-gates; \ - case "$$target_uid" in ""|*[!0-9]*) echo "heavy-gate provisioning: invalid target uid" >&2; exit 1;; esac; \ - if [ -L "$$root" ] || { [ -e "$$root" ] && [ ! -d "$$root" ]; }; then echo "heavy-gate provisioning: refusing an unsafe runtime root" >&2; exit 1; fi; \ - install -d -m 0755 -o root -g root "$$root"; \ - uid_dir="$$root/uid-$$target_uid"; \ - if [ -L "$$uid_dir" ] || { [ -e "$$uid_dir" ] && [ ! -d "$$uid_dir" ]; }; then echo "heavy-gate provisioning: refusing an unsafe per-user directory" >&2; exit 1; fi; \ - install -d -m 0755 -o root -g root "$$uid_dir"; \ - for index in 0 1; do \ - slot="$$uid_dir/slot-$$index"; \ - if [ -L "$$slot" ] || { [ -e "$$slot" ] && [ ! -f "$$slot" ]; }; then echo "heavy-gate provisioning: refusing an unsafe slot file" >&2; exit 1; fi; \ - if [ ! -e "$$slot" ]; then install -m 0600 -o "$$target_uid" -g root /dev/null "$$slot"; else chown "$$target_uid":root "$$slot"; chmod 0600 "$$slot"; fi; \ - done; \ - echo "heavy-gate provisioning: protected slots are ready for this boot"' \ - sh "$$target_uid" - -## heavy-check - the Layer-1 PR-equivalent gate under the heavy-lane semaphore. -heavy-check: heavy-gate-build - $(HEAVY_GATE) $(BAZEL_RUN) //bazel/checks:check - -## heavy-test-integration / -host-integration - explicit aliases for -## the public heavy lanes, kept for muscle memory and scripts. The public lanes -## now acquire the semaphore themselves; a redundant outer gate here is safe -## because the inner invocation verifies and reuses the inherited slot. -heavy-test-integration: test-integration -heavy-test-host-integration: test-host-integration +## heavy-check - the complete Layer-1 check. +heavy-check: + $(BAZEL_RUN) //bazel/checks:check -## heavy-flake-check - the building `nix flake check` under the semaphore. -## `make test-flake` is the cheap --no-build sibling. -heavy-flake-check: heavy-gate-build - $(HEAVY_GATE) $(NIX_FLAKE) flake check --print-build-logs +## heavy-flake-check - the building `nix flake check`; `make test-flake` is the +## cheap --no-build sibling. +heavy-flake-check: + $(NIX_FLAKE) flake check --print-build-logs # --- pre-existing maintainer targets --------------------------------------- ## pre-tag - run the full live-VM smoke gate before tagging a release. ## Requires: KVM, d2b active, both personal-dev and work-aad VMs declared. ## Exits non-zero on any probe failure. Updates $${TMPDIR:-/tmp}/d2b-smoke-run-log.txt. -## Public heavy lane: acquires a slot, then runs the raw live work behind -## the gate - the live smoke suite is the most destructive, stateful lane -## in the tree and must never bypass the sole-use semaphore. -pre-tag: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-pre-tag - -## heavy-lane-pre-tag - the raw full live-VM smoke work. Internal: reachable only -## from inside the gate (see heavy-lane-guard). -heavy-lane-pre-tag: heavy-lane-guard +pre-tag: bash tests/integration/live/live-vm-smoke.sh --full ## smoke-lite - run the single-VM lite smoke gate (≤5 min). -## Public heavy lane: acquires a slot, then runs the raw live work -## behind the gate. -smoke-lite: heavy-gate-build - $(HEAVY_GATE) $(MAKE) heavy-lane-smoke-lite - -## heavy-lane-smoke-lite - the raw lite live-VM smoke work. Internal: reachable -## only from inside the gate (see heavy-lane-guard). -heavy-lane-smoke-lite: heavy-lane-guard +smoke-lite: bash tests/integration/live/live-vm-smoke.sh --lite .PHONY: changelog-fold diff --git a/bazel/checks/BUILD.bazel b/bazel/checks/BUILD.bazel index 78e273afc6..fa4d69aae6 100644 --- a/bazel/checks/BUILD.bazel +++ b/bazel/checks/BUILD.bazel @@ -93,26 +93,15 @@ test_suite( tests = ["//bazel/checks/rust:portable_rust_guest"], ) -test_suite( - name = "test-rust-local", - tests = ["//bazel/checks/rust:portable_rust_local"], -) - test_suite( name = "test-rust", tests = [ ":test-rust-main", ":test-rust-broker", ":test-rust-guest-shell-runner", - ":test-rust-local", ], ) -test_suite( - name = "test-rust-no-bash-ast", - tests = ["//tests/tools/no-bash-ast-walker:no_bash_ast_test"], -) - test_suite( name = "test-rust-schema", tests = ["//packages/xtask:schema_reproducibility_test"], @@ -148,11 +137,6 @@ test_suite( tests = [":test-rust-guest-shell-runner"], ) -test_suite( - name = "test-rust-leaf-no-bash-ast", - tests = [":test-rust-no-bash-ast"], -) - test_suite( name = "test-rust-leaf-supply-chain", tests = [":test-rust-supply-chain"], diff --git a/bazel/checks/policy/BUILD.bazel b/bazel/checks/policy/BUILD.bazel index d814c04d72..34b42fa702 100644 --- a/bazel/checks/policy/BUILD.bazel +++ b/bazel/checks/policy/BUILD.bazel @@ -4,7 +4,6 @@ test_suite( name = "source_hygiene", tests = [ "//bazel/checks/meta:tier0", - "//tests/tools/no-bash-ast-walker:no_bash_ast_test", ], ) diff --git a/bazel/checks/rust/BUILD.bazel b/bazel/checks/rust/BUILD.bazel index e0e9648f24..6ec46dc7e9 100644 --- a/bazel/checks/rust/BUILD.bazel +++ b/bazel/checks/rust/BUILD.bazel @@ -38,25 +38,6 @@ test_suite( ], ) -test_suite( - name = "portable_rust_local", - tests = [ - "//packages/d2b:cli_json_contract", - "//packages/d2b:stub_no_socket", - "//packages/d2b-host:guest_control_token_materializer", - "//packages/d2b-unsafe-local-helper:d2b_unsafe_local_helper_test", - "//packages/d2b-unsafe-local-helper:shell_supervisor", - "//packages/d2bd:d2bd_lib_test", - "//packages/d2bd:daemon_socket_acl", - "//packages/d2bd:daemon_state_lock", - "//packages/d2bd:daemon_state_persistence", - "//packages/d2bd:stub_no_socket", - "//packages/d2b-bus:production_watch_rss", - "//packages/xtask:buildbuddy_config", - "//packages/xtask:heavy_gate_reexec", - ], -) - filegroup( name = "source_hygiene_sources", srcs = [ diff --git a/changelog.d/2026-08-21-v3-guest-u6.md b/changelog.d/2026-08-21-v3-guest-u6.md index b8775fe9e2..49241aa845 100644 --- a/changelog.d/2026-08-21-v3-guest-u6.md +++ b/changelog.d/2026-08-21-v3-guest-u6.md @@ -8,3 +8,18 @@ - Use typed Resource API services for Guest activation, configuration access, and readiness instead of legacy Guest control callers. +- Remove the standalone no-bash AST walker test and its executable Bazel and + Make wiring from the source-hygiene graph. +- Restore remote BuildBuddy compilation with local Bazel test execution until + the remote worker image supplies the upstream test wrapper runtime. +- Remove the heavy-gate semaphore, self-reexec guards, and host provisioning; + public integration, performance, smoke, and flake lanes now run their + existing work directly. +- Let BuildBuddy compile Rust tests while the remote profile runs their test + actions through the pinned local project shell. +- Remove obsolete per-target remote-execution exclusions from local-runtime + Rust suites. +- Remove the redundant local Rust suite; package suites now compile every Rust + test through BuildBuddy. +- Limit stable-principal collision checks to hash-derived UIDs so declared + Guest-root activation profiles can share UID 0. diff --git a/nixos-modules/host-activation.nix b/nixos-modules/host-activation.nix index 61fdfcd711..13e70ae20d 100644 --- a/nixos-modules/host-activation.nix +++ b/nixos-modules/host-activation.nix @@ -218,7 +218,6 @@ in # create or reconcile per-VM children without trusting a daemon-writable # parent. The per-VM leaves below remain d2bd:d2b for daemon-owned # sockets and guest-control artifacts. - (tmpfilesDir "/run/d2b-heavy-gates" "0755" "root" "root") (tmpfilesDir "/run/d2b/vms" "0750" "root" "d2b") (tmpfilesDir "/run/d2b/otel" "0750" "d2bd" "d2b") (tmpfilesDir "/run/d2b-gpu" "0750" "root" "d2b") @@ -238,61 +237,6 @@ in (runtimeAclMask "/run/d2b") ]; - # Numeric UIDs for normal users may be allocated only when the users - # activation step runs, so they cannot be embedded in static tmpfiles rules. - # Tmpfiles owns the fixed root; this activation step provisions slots for - # lifecycle users that NSS can resolve at activation time. Network-backed - # users may not resolve until networking and their identity provider are - # available, so they are deferred to the explicit post-login - # `make heavy-gate-provision` path. The gate remains fail-closed while their - # per-UID directory is absent. Existing regular slot inodes are repaired in - # place so a switch cannot replace an inode carrying a lock. - system.activationScripts.d2bHeavyGateProvision = lib.stringAfter [ "users" ] '' - set -eu - gate_root=/run/d2b-heavy-gates - - if [ -L "$gate_root" ] || { [ -e "$gate_root" ] && [ ! -d "$gate_root" ]; }; then - echo "d2b heavy-gate provisioning: refusing an unsafe runtime root" >&2 - exit 1 - fi - ${pkgs.coreutils}/bin/install -d -m 0755 -o root -g root "$gate_root" - - for user in ${lib.escapeShellArgs cfg.site.launcherUsers}; do - if ! passwd_record="$(${pkgs.getent}/bin/getent passwd "$user")"; then - echo "d2b heavy-gate provisioning: configured lifecycle user is unavailable during activation; skipping; run make heavy-gate-provision as that user after login" >&2 - continue - fi - uid="$(${pkgs.coreutils}/bin/printf '%s\n' "$passwd_record" | ${pkgs.coreutils}/bin/cut -d: -f3)" - case "$uid" in - ""|*[!0-9]*) - echo "d2b heavy-gate provisioning: lifecycle user has no usable uid during activation; skipping; run make heavy-gate-provision as that user after login" >&2 - continue - ;; - esac - - uid_dir="$gate_root/uid-$uid" - if [ -L "$uid_dir" ] || { [ -e "$uid_dir" ] && [ ! -d "$uid_dir" ]; }; then - echo "d2b heavy-gate provisioning: refusing an unsafe per-user directory" >&2 - exit 1 - fi - ${pkgs.coreutils}/bin/install -d -m 0755 -o root -g root "$uid_dir" - - for index in 0 1; do - slot="$uid_dir/slot-$index" - if [ -L "$slot" ] || { [ -e "$slot" ] && [ ! -f "$slot" ]; }; then - echo "d2b heavy-gate provisioning: refusing an unsafe slot file" >&2 - exit 1 - fi - if [ ! -e "$slot" ]; then - ${pkgs.coreutils}/bin/install -m 0600 -o "$uid" -g root /dev/null "$slot" - else - ${pkgs.coreutils}/bin/chown "$uid":root "$slot" - ${pkgs.coreutils}/bin/chmod 0600 "$slot" - fi - done - done - ''; - system.activationScripts.d2bGroupMigration = lib.stringAfter [ "users" ] '' target_gid="$(${pkgs.getent}/bin/getent group d2b | ${pkgs.coreutils}/bin/cut -d: -f3)" diff --git a/nixos-modules/minijail-profiles.nix b/nixos-modules/minijail-profiles.nix index f15d38a0b4..8a59a38645 100644 --- a/nixos-modules/minijail-profiles.nix +++ b/nixos-modules/minijail-profiles.nix @@ -873,11 +873,13 @@ let # container UID 0 maps to another role's host identity). # Walk every principal in fullProfileTable, group by UID, and # fail eval if any UID has more than one distinct principal. - principalUidPairs = lib.flatten (lib.mapAttrsToList - (profileId: data: [ - { principal = data.principal; uid = data.uid; profileId = profileId; } - ]) - fullProfileTable); + principalUidPairs = lib.filter + (pair: pair.uid == stablePrincipalId pair.principal) + (lib.flatten (lib.mapAttrsToList + (profileId: data: [ + { principal = data.principal; uid = data.uid; profileId = profileId; } + ]) + fullProfileTable)); principalUidByUid = lib.foldl' (acc: pair: let diff --git a/packages/d2b-bus/BUILD.bazel b/packages/d2b-bus/BUILD.bazel index e7ae40b5ea..4d256d4a82 100644 --- a/packages/d2b-bus/BUILD.bazel +++ b/packages/d2b-bus/BUILD.bazel @@ -114,7 +114,7 @@ rust_test( "//packages/d2b-resource-store:d2b_resource_store", "//packages/d2b-resource-store-redb:d2b_resource_store_redb", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], ) rust_test( diff --git a/packages/d2b-host/BUILD.bazel b/packages/d2b-host/BUILD.bazel index 4c56a299dc..5a6d25ca87 100644 --- a/packages/d2b-host/BUILD.bazel +++ b/packages/d2b-host/BUILD.bazel @@ -113,7 +113,7 @@ rust_test( }, env_inherit = ["PATH"], rustc_env = {"CARGO_BIN_EXE_d2b-activation-helper": "$(rootpath :d2b-activation-helper)"}, - tags = ["no-remote-cache", "no-remote-exec"], + tags = ["no-remote-cache"], visibility = ["//visibility:public"], deps = ["//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), diff --git a/packages/d2b-unsafe-local-helper/BUILD.bazel b/packages/d2b-unsafe-local-helper/BUILD.bazel index 20527ebedf..ac831e733e 100644 --- a/packages/d2b-unsafe-local-helper/BUILD.bazel +++ b/packages/d2b-unsafe-local-helper/BUILD.bazel @@ -40,7 +40,7 @@ rust_test( "CARGO_BIN_EXE_d2b-unsafe-local-helper": "$(rootpath :d2b-unsafe-local-helper)", }, rustc_env = {"CARGO_BIN_EXE_d2b-unsafe-local-helper": "$(rootpath :d2b-unsafe-local-helper)"}, - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], visibility = ["//visibility:public"], deps = [ ":d2b_unsafe_local_helper", @@ -55,7 +55,7 @@ rust_test( name = "d2b_unsafe_local_helper_test", compile_data = ["Cargo.toml"], crate = ":d2b_unsafe_local_helper", - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], data = [":d2b-unsafe-local-helper"], env = {"CARGO_BIN_EXE_d2b-unsafe-local-helper": "$(rootpath :d2b-unsafe-local-helper)"}, rustc_env = {"CARGO_BIN_EXE_d2b-unsafe-local-helper": "$(rootpath :d2b-unsafe-local-helper)"}, diff --git a/packages/d2b/BUILD.bazel b/packages/d2b/BUILD.bazel index 93b0f88a01..ee95a680d7 100644 --- a/packages/d2b/BUILD.bazel +++ b/packages/d2b/BUILD.bazel @@ -143,7 +143,6 @@ rust_test( "D2B_FIXTURES": "$(rootpath //bazel/checks/fixtures:fixture_smoke)", }, rustc_env = {"CARGO_BIN_EXE_d2b": "$(rootpath :d2b)"}, - tags = ["no-remote-exec"], visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-resource:d2b_contracts_resource", @@ -269,7 +268,7 @@ rust_test( env = {"CARGO_BIN_EXE_d2b": "$(rootpath :d2b)"}, env_inherit = ["PATH"], rustc_env = {"CARGO_BIN_EXE_d2b": "$(rootpath :d2b)"}, - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], visibility = ["//visibility:public"], deps = ["//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", ] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), diff --git a/packages/d2bd/BUILD.bazel b/packages/d2bd/BUILD.bazel index 1bbac24c76..d2f3afdb6e 100644 --- a/packages/d2bd/BUILD.bazel +++ b/packages/d2bd/BUILD.bazel @@ -231,7 +231,7 @@ rust_test( env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, env_inherit = ["PATH"], rustc_env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-provider:d2b_contracts_provider", @@ -266,7 +266,7 @@ rust_test( data = [":d2bd"], env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, rustc_env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-provider:d2b_contracts_provider", @@ -292,7 +292,7 @@ rust_test( "D2B_TEST_SCRUB_SHELL_ENVIRONMENT": "$(rootpath //:tests/tools/scrub-shell-environment)", }, rustc_env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], visibility = ["//visibility:public"], deps = [ "//packages/d2b-contracts-provider:d2b_contracts_provider", @@ -309,7 +309,7 @@ rust_test( env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, env_inherit = ["PATH"], rustc_env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], visibility = ["//visibility:public"], deps = ["//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-contracts-provider:d2b_contracts_provider",] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), @@ -419,7 +419,6 @@ rust_test( ], rustc_env = {"CARGO_BIN_EXE_d2bd": "$(rootpath :d2bd)"}, tags = [ - "local", "no-remote-cache", "no-sandbox", ], diff --git a/packages/xtask/BUILD.bazel b/packages/xtask/BUILD.bazel index 6874614691..81767b1c17 100644 --- a/packages/xtask/BUILD.bazel +++ b/packages/xtask/BUILD.bazel @@ -28,7 +28,6 @@ rust_binary( crate_root = "src/main.rs", compile_data = [ "Cargo.toml", - "src/heavy_gate.rs", ], visibility = ["//visibility:public"], deps = [ @@ -266,7 +265,7 @@ rust_test( env = {"CARGO_BIN_EXE_xtask": "$(rootpath :xtask)"}, env_inherit = ["D2B_REPO_ROOT", "PATH"], rustc_env = {"CARGO_BIN_EXE_xtask": "$(rootpath :xtask)"}, - tags = ["local", "no-remote-cache"], + tags = ["no-remote-cache"], visibility = ["//visibility:public"], deps = ["//packages/d2b-contracts-resource:d2b_contracts_resource", "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-contracts-provider:d2b_contracts_provider",] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), @@ -292,23 +291,6 @@ rust_test( "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-contracts-provider:d2b_contracts_provider",] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), ) -rust_test( - name = "heavy_gate_reexec", - srcs = ["tests/heavy_gate_reexec.rs"], - compile_data = ["Cargo.toml"], - data = [ - ":xtask", - "//bazel/checks/rust:source_hygiene_sources", - ], - env = {"CARGO_BIN_EXE_xtask": "$(rootpath :xtask)"}, - env_inherit = ["PATH"], - rustc_env = {"CARGO_BIN_EXE_xtask": "$(rootpath :xtask)"}, - tags = ["no-remote-cache", "no-remote-exec"], - visibility = ["//visibility:public"], - deps = ["//packages/d2b-contracts-resource:d2b_contracts_resource", - "//packages/d2b-contracts-zone-session:d2b_contracts_zone_session", "//packages/d2b-contracts-provider:d2b_contracts_provider",] + all_crate_deps(normal = True, normal_dev = True, cargo_only = True), -) - rust_test( name = "policy_changelog_gate", srcs = [ diff --git a/packages/xtask/Cargo.toml b/packages/xtask/Cargo.toml index 0dbeb3877f..71f93e9a0f 100644 --- a/packages/xtask/Cargo.toml +++ b/packages/xtask/Cargo.toml @@ -24,11 +24,9 @@ d2b-contracts = { path = "../d2b-contracts", version = "0.0.0-bootstrap" } d2b-contracts-broker = { path = "../d2b-contracts-broker", version = "0.0.0-bootstrap" } d2b-contracts-control = { path = "../d2b-contracts-control", version = "0.0.0-bootstrap" } d2b-contracts-resource = { path = "../d2b-contracts-resource", version = "0.0.0-bootstrap" } -# Safe wrappers for the syscalls the heavy-gate semaphore needs: F_OFD_SETLK -# open file description locks, FD_CLOEXEC control on the duplicated slot -# handle, signalfd, and process-group signalling. Already used by d2b, d2bd, -# and the broker at the same version, so this adds no new supply-chain edge. -nix = { version = "0.29", default-features = false, features = ["fs", "signal", "user"] } +# The delivery state writer uses nix for libc constants and effective-UID +# checks. Keep the feature surface limited to the user APIs it calls. +nix = { version = "0.29", default-features = false, features = ["user"] } # Directory-descriptor-anchored file operations for the delivery state # writer: `openat`/`openat2`/`mkdirat`/`renameat`/`unlinkat`/`fsync` relative # to a pinned, verified parent fd, so a temp create, rename, and directory diff --git a/packages/xtask/src/heavy_gate.rs b/packages/xtask/src/heavy_gate.rs deleted file mode 100644 index 1741712b09..0000000000 --- a/packages/xtask/src/heavy_gate.rs +++ /dev/null @@ -1,3593 +0,0 @@ -//! The sole heavy-lane semaphore for d2b validation. -//! -//! `cargo xtask heavy-gate -- [args...]` is the only place a -//! Layer-2, host-integration, hardware, live, or perf-heavy command may be -//! started. It is a two-slot, per-UID semaphore built on open file -//! description locks so concurrent heavy validation cannot oversubscribe the -//! shared Nix store, cargo target directory, or KVM device. -//! -//! Contract for the heavy validation lane. -//! section 11: -//! -//! * Exactly [`SLOT_COUNT`] slots, scoped to the invoking uid, living under the -//! system-provisioned `/run/d2b-heavy-gates/uid-/`. The root and per-uid -//! directory are root-owned and non-writable by unprivileged users, and the -//! two uid-owned slot files are provisioned in advance. No component that -//! names a slot can therefore be squatted, unlinked, or renamed by a peer or -//! by the invoking uid while another lane holds its inode. If this fixed -//! namespace is absent or malformed, the gate fails closed; it never falls -//! back to a user-owned runtime or temporary directory. -//! * Acquisition is nonblocking (`F_OFD_SETLK`), retried every -//! [`RETRY_INTERVAL`] for at most [`ACQUIRE_TIMEOUT`]. -//! * Fail-closed: if the platform or filesystem does not support open file -//! description locking, the gate refuses to run. There is deliberately no -//! `flock` fallback and no unsynchronized degraded mode, because a -//! fail-open gate would let two multi-hour lanes stampede the shared host -//! resources it exists to protect. -//! * The child receives a duplicated handle on the *same* open file -//! description, with `FD_CLOEXEC` cleared, so the slot stays held for the -//! child's whole life. An open file description lock is released only when -//! the last descriptor referring to that description is closed, so the -//! wrapper never releases the slot early. -//! * The wrapper owns the child's process group: it starts the child as a -//! process-group leader, forwards terminating signals to the whole group, -//! escalates to `SIGKILL` after [`TERMINATION_GRACE`], observes the leader's -//! exit *without reaping it*, unconditionally sweeps the group with -//! `SIGKILL` while the leader is still an unreaped zombie, and only then -//! reaps the leader. Sweeping before reaping keeps the leader's pid and pgid -//! pinned by the zombie, so the numeric pgid cannot be recycled onto an -//! unrelated process group between the exit and the `SIGKILL`. A `Ctrl-C`, -//! an external timeout, or a signal that races the leader's exit therefore -//! cannot orphan a descendant that still holds the slot, nor can the sweep -//! ever reach a stranger's group. -//! -//! The wrapper receives those signals through a `signalfd`, which requires -//! them to be blocked. A blocked signal mask survives `execve`, and a wrapped -//! command that inherited a blocked `SIGTERM` would ignore the wrapper's -//! graceful teardown entirely and only die at the `SIGKILL` escalation. The -//! wrapper therefore starts the command through a one-shot re-exec of itself -//! ([`EXEC_SHIM_FLAG`]) that clears the mask and immediately `execve`s the -//! real command in the same process. That keeps the pid, the process group, -//! and the inherited slot descriptor identical while guaranteeing the command -//! starts with a clean signal mask. -//! -//! "Sole use" also means: no crate, wave, or reviewer role may add a second -//! lock file, sleep-and-retry loop, or per-crate heavy-lane guard. Nested -//! invocations reuse the slot already held by the outer wrapper rather than -//! acquiring a second one, which would deadlock a two-slot semaphore against -//! itself. Nesting is never taken on trust: the mere presence of -//! [`GATE_ACTIVE_ENV`] proves nothing, because any process can export it. A -//! child is treated as nested only after the inherited slot descriptor -//! ([`SLOT_FD_ENV`]) is verified to be an open handle on the real, -//! currently-locked per-uid slot file it names ([`SLOT_INDEX_ENV`]); a forged, -//! stale, closed, or unlocked marker is ignored and a real slot is acquired -//! instead. - -use std::ffi::OsString; -use std::fmt; -use std::fs::File; -use std::os::fd::{AsFd, AsRawFd, RawFd}; -use std::os::unix::process::CommandExt; -use std::path::Path; -#[cfg(test)] -use std::path::PathBuf; -use std::process::{Command, ExitCode}; -use std::thread::sleep; -use std::time::{Duration, Instant}; - -use nix::errno::Errno; -use nix::fcntl::{FcntlArg, FdFlag, fcntl}; -use nix::libc; -use nix::sys::signal::{SigSet, Signal, killpg}; -use nix::sys::signalfd::{SfdFlags, SignalFd}; -use nix::sys::stat::fstat; -use nix::sys::wait::{Id, WaitPidFlag, WaitStatus, waitid}; -use nix::unistd::{Pid, getuid}; -use rustix::fs::{Mode, OFlags}; - -/// Number of concurrent heavy lanes allowed per uid. -pub const SLOT_COUNT: usize = 2; - -/// Delay between nonblocking acquisition attempts. -pub const RETRY_INTERVAL: Duration = Duration::from_millis(250); - -/// Ceiling on how long a lane waits for a slot before failing closed. -pub const ACQUIRE_TIMEOUT: Duration = Duration::from_secs(30 * 60); - -/// Grace period between forwarding a terminating signal to the child's -/// process group and escalating to `SIGKILL`. -pub const TERMINATION_GRACE: Duration = Duration::from_secs(10); - -/// How often the supervisor loop wakes to reap the child and drain signals. -const POLL_INTERVAL: Duration = Duration::from_millis(50); - -/// How often a waiting lane repeats its "still waiting" diagnostic. -const WAIT_NOTICE_INTERVAL: Duration = Duration::from_secs(60); - -/// The single, fixed, system-provisioned heavy-gate root. -/// -/// `/run/d2b-heavy-gates` is chosen as a root-owned runtime namespace, not as a -/// user-writable place where this process creates another directory. Its parent -/// (`/run`), the root itself, and every `uid-` slot directory must be -/// root-owned and non-writable by group or other. The two `slot-*` files are -/// pre-created, owned by the target uid, and mode `0600`. Consequently neither a -/// foreign uid nor the target uid can replace a name while a lock remains on the -/// old inode. -/// -/// The namespace deliberately has no fallback. `/run/user/` is not always -/// present and is owned by the uid, so selecting it conditionally would revive -/// the split-pool race and placing slot names inside it would let that uid rename -/// them. An absent provisioned root is therefore an environment error with a -/// bounded provisioning diagnostic, never permission to use a weaker location. -const CANONICAL_GATE_ROOT: &str = "/run/d2b-heavy-gates"; - -/// Semantic labels for the fixed heavy-gate namespace components. -/// -/// Every operator- and CI-facing diagnostic names the directory or file by its -/// ROLE, never by its absolute path or the caller's uid. The gate paths embed -/// both the root and the uid (`/run/d2b-heavy-gates/uid-`), and these -/// diagnostics reach stderr and CI logs verbatim, so interpolating the resolved -/// path or the raw uid would leak them. Naming the role instead keeps the -/// message actionable without disclosing either. The remediation text uses the -/// shell-style `$UID` placeholder rather than the literal number. -mod gate_label { - /// The single fixed, system-provisioned per-host root. - pub const ROOT: &str = "the heavy-gate root directory"; - /// The per-uid `uid-$UID` slot directory. - pub const SLOT_DIR: &str = "the per-uid heavy-gate slot directory"; - - /// The `slot-` file within the per-uid slot directory. - pub fn slot(index: usize) -> String { - format!("heavy-gate slot {index}") - } -} - -/// Set in the child environment so nested lanes reuse the held slot instead -/// of deadlocking against the same two slots. -pub const GATE_ACTIVE_ENV: &str = "D2B_HEAVY_GATE"; - -/// Slot index the child is running under. -pub const SLOT_INDEX_ENV: &str = "D2B_HEAVY_GATE_SLOT"; - -/// Descriptor number of the child's inherited handle on the locked slot. -pub const SLOT_FD_ENV: &str = "D2B_HEAVY_GATE_SLOT_FD"; - -/// Internal marker selecting the one-shot re-exec shim. Not an operator -/// surface: the wrapper passes it to itself so the wrapped command starts -/// with a cleared signal mask. -pub const EXEC_SHIM_FLAG: &str = "--exec-child"; - -/// Signals the wrapper forwards to the child's process group. -const FORWARDED_SIGNALS: [Signal; 4] = [ - Signal::SIGINT, - Signal::SIGTERM, - Signal::SIGHUP, - Signal::SIGQUIT, -]; - -pub type Result = std::result::Result; - -/// Failure classes, each with a distinct exit code drawn from the `sysexits` -/// range so a gate failure is never confused with the wrapped command's own -/// exit status. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum GateErrorKind { - /// The invocation itself was malformed. - Usage, - /// Open file description locking is unavailable. Always fails closed. - Unsupported, - /// No slot became free within [`ACQUIRE_TIMEOUT`]. - Timeout, - /// The gate directory or slot file could not be prepared safely. - Environment, - /// The wrapped command could not be started or supervised. - Spawn, -} - -impl GateErrorKind { - pub fn as_str(self) -> &'static str { - match self { - Self::Usage => "usage", - Self::Unsupported => "unsupported", - Self::Timeout => "timeout", - Self::Environment => "environment", - Self::Spawn => "spawn", - } - } - - /// Process exit code for this failure class. Never zero. - pub fn exit_code(self) -> u8 { - match self { - Self::Usage => 64, - Self::Unsupported => 69, - Self::Environment => 72, - Self::Spawn => 71, - Self::Timeout => 75, - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct GateError { - kind: GateErrorKind, - message: String, -} - -impl GateError { - pub fn of(kind: GateErrorKind, message: impl Into) -> Self { - Self { - kind, - message: message.into(), - } - } - - pub fn usage(message: impl Into) -> Self { - Self::of(GateErrorKind::Usage, message) - } - - pub fn environment(message: impl Into) -> Self { - Self::of(GateErrorKind::Environment, message) - } - - pub fn unsupported(message: impl Into) -> Self { - Self::of(GateErrorKind::Unsupported, message) - } - - pub fn kind(&self) -> GateErrorKind { - self.kind - } - - pub fn message(&self) -> &str { - &self.message - } -} - -const PROVISIONING_ERROR_CODE: &str = "heavy-gate-provisioning-required"; -const PROVISIONING_ACTION: &str = "make heavy-gate-provision"; - -/// Build the stable operator-facing diagnostic for an unavailable or malformed -/// protected namespace. -/// -/// The observed state and remediation are intentionally path- and uid-free. -/// Detailed errors may name only the semantic labels in [`gate_label`]. -fn provisioning_error(observed: &str, detail: impl fmt::Display) -> GateError { - GateError::environment(format!( - "code: {PROVISIONING_ERROR_CODE}; observed: {observed}; remediation: run \ - `{PROVISIONING_ACTION}`; detail: {detail}; no fallback namespace was used" - )) -} - -impl fmt::Display for GateError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - self.message.fmt(formatter) - } -} - -impl std::error::Error for GateError {} - -/// How a nonblocking lock attempt resolved. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum LockOutcome { - /// The slot is held by another lane; retry. - Busy, - /// Open file description locking is not available here; fail closed. - Unsupported, - /// Something else went wrong with the slot file. - Environment, -} - -/// The verdict of proving whether an inherited descriptor genuinely holds a -/// heavy-gate slot. -/// -/// This deliberately separates a legitimate "no slot is held" answer from a -/// verifier *malfunction*. A malfunction (a lock mechanism that is unsupported -/// here, or an environment error touching the slot file) is returned as an -/// [`Err`] so callers can fail closed instead of mistaking it for "unheld" and -/// re-execing forever against a broken environment. The two non-error verdicts -/// are: -/// -/// * [`SlotProof::Held`] - the inherited descriptor is proven to hold the slot -/// lock, so the caller may proceed under it; and -/// * [`SlotProof::NotHeld`] - there is no genuinely held slot to inherit (no -/// marker, a forged or stale marker, an absent slot file, a foreign owner, a -/// mismatched inode, or a slot another open file description holds), so the -/// caller should acquire a real slot. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum SlotProof { - /// The inherited descriptor is proven to hold the slot lock. - Held, - /// No genuinely held slot backs the marker; the caller must acquire one. - NotHeld, -} - -/// Classify an `F_OFD_SETLK` failure. -/// -/// `EAGAIN`/`EACCES` are the only retryable outcomes. Everything that means -/// "this kernel or filesystem cannot do open file description locks" maps to -/// [`LockOutcome::Unsupported`] so the gate refuses to run rather than -/// silently degrading to unsynchronized execution. -pub fn classify_lock_errno(errno: Errno) -> LockOutcome { - match errno { - Errno::EAGAIN | Errno::EACCES => LockOutcome::Busy, - Errno::EINVAL | Errno::ENOLCK | Errno::ENOSYS | Errno::EOPNOTSUPP => { - LockOutcome::Unsupported - } - _ => LockOutcome::Environment, - } -} - -fn flock_for(kind: libc::c_short) -> libc::flock { - libc::flock { - l_type: kind, - l_whence: libc::SEEK_SET as libc::c_short, - l_start: 0, - l_len: 0, - l_pid: 0, - } -} - -/// Attempt a whole-file exclusive open file description lock without blocking, -/// operating directly on a raw descriptor. -/// -/// Applying `F_WRLCK` through a descriptor whose open file description already -/// holds the lock is idempotent and succeeds; applying it when a *different* -/// description holds the lock fails with `EAGAIN`/`EACCES`. That distinction is -/// what makes an `F_OFD_SETLK` through an inherited descriptor an atomic proof -/// of which description owns a slot. -fn try_lock_fd(fd: RawFd) -> std::result::Result<(), Errno> { - let lock = flock_for(libc::F_WRLCK as libc::c_short); - fcntl(fd, FcntlArg::F_OFD_SETLK(&lock)).map(drop) -} - -/// Attempt a whole-file exclusive open file description lock without blocking. -fn try_lock(file: &File) -> std::result::Result<(), Errno> { - try_lock_fd(file.as_raw_fd()) -} - -/// Release a whole-file open file description lock. -fn unlock(file: &File) -> std::result::Result<(), Errno> { - let lock = flock_for(libc::F_UNLCK as libc::c_short); - fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&lock)).map(drop) -} - -/// The single canonical namespace root. -/// -/// Non-overridable and not a function of runtime-directory existence: in a -/// released binary the root is the constant [`CANONICAL_GATE_ROOT`], never -/// `TMPDIR`, `XDG_RUNTIME_DIR`, `/run/user/`, or any other value that an -/// attacker - or a well-meaning operator wiring up two lanes - could set or -/// cause to differ between concurrent invocations. The fixed root and its -/// per-uid directories are provisioned by root, so the uid that owns the slot -/// files cannot rename a directory or slot name and mint fresh lock inodes. -/// -/// `test_root` is an injectable seam used *exclusively* by this crate's own -/// tests, which is why the function stays pure (no environment or filesystem -/// access) and its precedence is directly unit-testable. The production caller -/// always passes `None` (see [`GateDir::resolve`]), so a released binary has -/// no code path that consults a caller-selected root. -#[cfg(test)] -pub fn gate_root_from(_uid: u32, test_root: Option<&Path>) -> PathBuf { - match test_root { - Some(root) => root.to_path_buf(), - None => PathBuf::from(CANONICAL_GATE_ROOT), - } -} - -/// Per-uid slot directory under `root`. -#[cfg(test)] -pub fn gate_dir_path(root: &Path, uid: u32) -> PathBuf { - root.join(format!("uid-{uid}")) -} - -/// Whether a system-provisioned directory prevents every unprivileged uid, -/// including the slot owner, from replacing entries beneath it. -/// -/// Sticky world-writable directories are deliberately rejected. Sticky -/// semantics stop one uid from renaming another uid's entry, but the owner of an -/// entry may still rename it; that is the exact inode-splitting attack the gate -/// must prevent. Only a root-owned directory with no group/other write bit is a -/// valid parent for the fixed namespace and slot names. -pub fn system_gate_parent_is_trusted(owner_uid: u32, mode: u32) -> bool { - owner_uid == 0 && mode & 0o022 == 0 -} - -/// A prepared, ownership-checked per-uid slot directory anchored to a verified -/// open directory descriptor. -#[derive(Debug)] -pub struct GateDir { - /// The resolved per-uid slot directory path. Retained only for test - /// assertions on the filesystem layout; production diagnostics name the - /// directory by role, never by path, so no operator- or CI-facing surface - /// reads it. - #[cfg(test)] - path: PathBuf, - dir: File, - /// Test namespaces create their slot fixtures lazily. Released binaries - /// always set this to false and require both slot files to be provisioned. - create_slots: bool, -} - -impl GateDir { - #[cfg(test)] - pub fn path(&self) -> &Path { - &self.path - } - - /// Resolve the gate directory from the single provisioned namespace root. - /// - /// A released binary accepts only [`CANONICAL_GATE_ROOT`] and never creates - /// any namespace component. Missing or unsafe provisioning fails closed with - /// no fallback. Unit-test child modes may inject a private scratch root; that - /// seam is compiled out of released binaries. - pub fn resolve() -> Result { - let uid = getuid().as_raw(); - #[cfg(test)] - if let Some(root) = Self::test_root_override() { - return Self::prepare_test(&root, uid); - } - Self::open_provisioned(Path::new(CANONICAL_GATE_ROOT), uid) - } - - /// The test-only root override. - /// - /// In the crate's test build the child-mode helpers re-exec this same - /// binary with `XDG_RUNTIME_DIR` pointed at a per-test scratch directory, - /// so tests never touch the real namespace. In a released binary this - /// always returns `None`: the production root is the fixed constant - /// [`CANONICAL_GATE_ROOT`] and no environment variable can redirect it. - #[cfg(test)] - fn test_root_override() -> Option { - std::env::var_os("XDG_RUNTIME_DIR") - .filter(|value| !value.is_empty()) - .map(PathBuf::from) - } - - /// Open a fully provisioned root and per-uid slot directory. - /// - /// Nothing is created or repaired here. Both directories must be root-owned - /// and non-writable by unprivileged users, and both slot files must already - /// exist as private regular files owned by `uid`. This makes every pathname - /// in the lock identity immutable to the uid that runs heavy work. - fn open_provisioned(root: &Path, uid: u32) -> Result { - let root_dir = open_directory(root, gate_label::ROOT).map_err(|error| { - provisioning_error( - "the protected heavy-gate root is unavailable", - error.message(), - ) - })?; - verify_system_directory(&root_dir, gate_label::ROOT)?; - - let uid_name = format!("uid-{uid}"); - let dir = - open_directory_at(&root_dir, &uid_name, gate_label::SLOT_DIR).map_err(|error| { - provisioning_error( - "the protected per-user slot directory is unavailable", - error.message(), - ) - })?; - verify_system_directory(&dir, gate_label::SLOT_DIR)?; - - let gate = Self { - #[cfg(test)] - path: gate_dir_path(root, uid), - dir, - create_slots: false, - }; - for index in 0..SLOT_COUNT { - gate.open_slot(index, false)?.ok_or_else(|| { - provisioning_error( - "a provisioned slot is unavailable", - format!("{} is absent", gate_label::slot(index)), - ) - })?; - } - Ok(gate) - } - - /// Build a uid-owned scratch namespace for this module's tests. - /// - /// Released binaries never call this path. It exists only because an - /// unprivileged test cannot synthesize the root-owned production layout. - #[cfg(test)] - fn prepare_test(root: &Path, uid: u32) -> Result { - let root_dir = open_directory(root, gate_label::ROOT)?; - let root_stat = fstat(root_dir.as_raw_fd()).map_err(|errno| { - GateError::environment(format!("cannot stat {}: {errno}", gate_label::ROOT)) - })?; - if root_stat.st_uid != uid || root_stat.st_mode as u32 & 0o022 != 0 { - return Err(GateError::environment(format!( - "{} is not an owned, non-writable test root", - gate_label::ROOT - ))); - } - - let uid_name = format!("uid-{uid}"); - let dir = open_or_create_directory_at(&root_dir, &uid_name, gate_label::SLOT_DIR, 0o700)?; - let metadata = fstat(dir.as_raw_fd()).map_err(|errno| { - GateError::environment(format!("cannot stat {}: {errno}", gate_label::SLOT_DIR)) - })?; - if metadata.st_uid != uid || metadata.st_mode as u32 & 0o077 != 0 { - return Err(GateError::environment(format!( - "{} must be owned by the test uid and private (0700)", - gate_label::SLOT_DIR - ))); - } - Ok(Self { - path: gate_dir_path(root, uid), - dir, - create_slots: true, - }) - } - - #[cfg(test)] - fn slot_path(&self, index: usize) -> PathBuf { - self.path.join(format!("slot-{index}")) - } - - /// Open one slot directly beneath the pinned directory descriptor. - /// - /// Production never creates a slot: a missing file is unsafe provisioning. - /// Unit-test scratch namespaces opt into `O_CREAT`, then pin and normalise - /// the resulting descriptor without re-resolving a pathname. - fn open_slot(&self, index: usize, missing_is_ok: bool) -> Result> { - let name = format!("slot-{index}"); - let mut flags = OFlags::RDWR | OFlags::NOFOLLOW | OFlags::CLOEXEC; - if self.create_slots { - flags |= OFlags::CREATE; - } - let fd = match rustix::fs::openat( - self.dir.as_fd(), - name.as_str(), - flags, - Mode::from_bits_truncate(0o600), - ) { - Ok(fd) => fd, - Err(rustix::io::Errno::NOENT) if missing_is_ok && self.create_slots => { - return Ok(None); - } - Err(rustix::io::Errno::NOENT) => { - return Err(provisioning_error( - "a provisioned slot is unavailable", - format!("{} is absent", gate_label::slot(index)), - )); - } - Err(error) => { - return Err(provisioning_error( - "a provisioned slot is unusable", - format!( - "cannot open {} relative to the pinned slot directory: {error}", - gate_label::slot(index) - ), - )); - } - }; - if self.create_slots { - rustix::fs::fchmod(fd.as_fd(), Mode::from_bits_truncate(0o600)).map_err(|error| { - GateError::environment(format!( - "cannot set the private mode on {}: {error}", - gate_label::slot(index) - )) - })?; - } - let file = File::from(fd); - verify_slot_file(&file, index, getuid().as_raw())?; - Ok(Some(file)) - } - - /// Verify that this filesystem really implements `F_OFD_SETLK`. - /// - /// Probes through a canonical slot rather than creating a throwaway file. - /// Contention proves the mechanism works; an uncontended probe is released - /// immediately. The provisioned slot directory is intentionally immutable - /// to the caller, so no probe leaf needs to be created or removed. - pub fn probe_ofd_support(&self) -> Result<()> { - let probe = self.open_slot(0, false)?.ok_or_else(|| { - GateError::environment(format!( - "{} is absent from the system-provisioned namespace", - gate_label::slot(0) - )) - })?; - self.evaluate_probe(&probe) - } - - fn evaluate_probe(&self, probe: &File) -> Result<()> { - match try_lock(probe) { - Ok(()) => unlock(probe).map_err(|errno| self.probe_error(errno, "release")), - // A contended probe still proves the mechanism works. - Err(Errno::EAGAIN | Errno::EACCES) => Ok(()), - Err(errno) => Err(self.probe_error(errno, "acquire")), - } - } - - fn probe_error(&self, errno: Errno, phase: &str) -> GateError { - match classify_lock_errno(errno) { - LockOutcome::Unsupported => self.unsupported_error(errno), - _ => GateError::environment(format!( - "cannot {phase} the lock on {}: {errno}", - gate_label::slot(0) - )), - } - } - - fn unsupported_error(&self, errno: Errno) -> GateError { - GateError::unsupported(format!( - "open file description locks (F_OFD_SETLK) are unavailable on the filesystem backing \ - {} ({errno}). The heavy gate fails closed rather than falling back to flock or \ - running unsynchronized; the system-provisioned heavy-gate root must live on a \ - filesystem that supports them.", - gate_label::SLOT_DIR - )) - } - - /// One nonblocking attempt at `index`. - /// - /// Returns `Ok(None)` when the slot is held by another lane. - pub fn try_acquire(&self, index: usize) -> Result> { - assert!(index < SLOT_COUNT, "slot index out of range"); - let file = self.open_slot(index, false)?.ok_or_else(|| { - GateError::environment(format!( - "{} is absent from the system-provisioned namespace", - gate_label::slot(index) - )) - })?; - - match try_lock(&file) { - Ok(()) => Ok(Some(SlotGuard { index, file })), - Err(errno) => match classify_lock_errno(errno) { - LockOutcome::Busy => Ok(None), - LockOutcome::Unsupported => Err(self.unsupported_error(errno)), - LockOutcome::Environment => Err(GateError::environment(format!( - "cannot lock {}: {errno}", - gate_label::slot(index) - ))), - }, - } - } - - /// Prove whether `fd` is genuine evidence that this process runs inside a - /// slot an ancestor wrapper already holds, claiming the slot atomically as - /// part of the proof. - /// - /// Returns [`SlotProof::Held`] only when every one of the following holds, - /// so a forged or stale [`GATE_ACTIVE_ENV`] marker can never skip - /// acquisition: - /// - /// * `index` names a real slot (`index < SLOT_COUNT`); - /// * `fd` is an open descriptor (`fstat` succeeds) on a regular file owned - /// by this uid; - /// * that descriptor's device and inode match the canonical - /// `slot-` file in this verified, uid-private gate directory, so - /// it cannot be an attacker-controlled file elsewhere; and - /// * a nonblocking `F_OFD_SETLK` write lock issued *through the inherited - /// descriptor itself* succeeds. - /// - /// That final step is the atomic ownership proof. Placing the lock through - /// `fd` (not a fresh handle) means: - /// - /// * if `fd`'s open file description already holds the slot lock - the only - /// way a genuine ancestor handoff looks - the call is idempotent and - /// succeeds, and the lock stays held through the exec shim because the - /// descriptor stays open; - /// * if the slot is unheld, the lock is safely *acquired* on this same - /// description, so the nested run holds a real slot rather than running a - /// third lane unsynchronised; and - /// * if any *other* open file description holds the slot, the call fails - /// with `EAGAIN`/`EACCES` and nesting is rejected. - /// - /// A verdict of [`SlotProof::NotHeld`] covers every *legitimate* "no slot - /// is held" shape: a malformed marker, a closed (`EBADF`) or foreign - /// descriptor, an absent slot file, a mismatched inode, or a slot a - /// *different* description holds (`EAGAIN`/`EACCES`). A verifier - /// *malfunction* - a slot file that exists but cannot be opened or - /// stat'd, or a lock mechanism that is unsupported or errors for an - /// environmental reason - is returned as an [`Err`] instead of being - /// silently flattened into "unheld", so a caller fails closed on a broken - /// environment rather than re-execing forever against it. - /// - /// The previous form issued `F_OFD_GETLK` on a *fresh* handle, which proved - /// only that *some* description held the slot - not that the advertised - /// descriptor did - so a forged unlocked descriptor passed whenever any - /// unrelated lane happened to hold that slot, and the lock could be dropped - /// between the query and its use. Claiming through `fd` removes both the - /// impersonation and the TOCTOU window. - pub fn descriptor_is_locked_slot(&self, index: usize, fd: RawFd) -> Result { - if index >= SLOT_COUNT || fd < 0 { - return Ok(SlotProof::NotHeld); - } - let uid = getuid().as_raw(); - let inherited = match fstat(fd) { - Ok(stat) => stat, - // A closed or invalid inherited descriptor is a stale or forged - // marker, not a gate malfunction: acquire a real slot instead. - Err(Errno::EBADF) => return Ok(SlotProof::NotHeld), - Err(errno) => { - return Err(GateError::environment(format!( - "cannot stat the inherited heavy-gate slot descriptor: {errno}" - ))); - } - }; - if (inherited.st_mode & libc::S_IFMT) != libc::S_IFREG { - return Ok(SlotProof::NotHeld); - } - if inherited.st_uid != uid { - return Ok(SlotProof::NotHeld); - } - let Some(slot) = self.open_slot(index, true)? else { - // A test scratch namespace may not have created this slot yet. In - // production both slots were verified during `open_provisioned`, so - // disappearance would be a root-level mutation rather than a - // caller-controlled fallback. - return Ok(SlotProof::NotHeld); - }; - let slot_stat = fstat(slot.as_raw_fd()).map_err(|errno| { - GateError::environment(format!("cannot stat {}: {errno}", gate_label::slot(index))) - })?; - if slot_stat.st_dev != inherited.st_dev || slot_stat.st_ino != inherited.st_ino { - return Ok(SlotProof::NotHeld); - } - // Atomic ownership proof: claim the slot lock through the inherited - // descriptor itself. Success means this description now holds the slot - // (idempotently, if it already did); a Busy conflict means another - // description owns it and nesting must be rejected. Anything else is a - // verifier malfunction and fails closed. There is no window between - // checking and using the lock because the check *is* the claim. - match try_lock_fd(fd) { - Ok(()) => Ok(SlotProof::Held), - Err(errno) => match classify_lock_errno(errno) { - LockOutcome::Busy => Ok(SlotProof::NotHeld), - LockOutcome::Unsupported => Err(self.unsupported_error(errno)), - LockOutcome::Environment => Err(GateError::environment(format!( - "cannot verify heavy-gate slot ownership through the inherited \ - descriptor: {errno}" - ))), - }, - } - } -} - -/// Open `path` as a directory, refusing a symlinked final component. -/// -/// `O_DIRECTORY` rejects a non-directory (`ENOTDIR`) and `O_NOFOLLOW` rejects a -/// symlink (`ELOOP`) so the returned descriptor is always a real directory, -/// and every later operation anchored to it stays bound to that inode. -fn open_directory(path: &Path, label: &str) -> Result { - rustix::fs::open( - path, - OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC, - Mode::empty(), - ) - .map(File::from) - .map_err(|error| match error { - rustix::io::Errno::LOOP => GateError::environment(format!( - "{label} is a symlink; refusing to use it as a heavy-gate directory" - )), - rustix::io::Errno::NOTDIR => GateError::environment(format!("{label} is not a directory")), - _ => GateError::environment(format!("cannot open {label}: {error}")), - }) -} - -/// Open a child directory directly relative to a pinned parent descriptor. -fn open_directory_at(parent: &File, name: &str, label: &str) -> Result { - rustix::fs::openat( - parent.as_fd(), - name, - OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC, - Mode::empty(), - ) - .map(File::from) - .map_err(|error| match error { - rustix::io::Errno::LOOP => GateError::environment(format!( - "{label} is a symlink; refusing to use it as a heavy-gate directory" - )), - rustix::io::Errno::NOTDIR => GateError::environment(format!("{label} is not a directory")), - _ => GateError::environment(format!("cannot open {label}: {error}")), - }) -} - -/// Test-only fd-relative directory creation. -#[cfg(test)] -fn open_or_create_directory_at(parent: &File, name: &str, label: &str, mode: u32) -> Result { - if let Ok(dir) = open_directory_at(parent, name, label) { - return Ok(dir); - } - match rustix::fs::mkdirat(parent.as_fd(), name, Mode::from_bits_truncate(mode)) { - Ok(()) | Err(rustix::io::Errno::EXIST) => {} - Err(error) => { - return Err(GateError::environment(format!( - "cannot create {label} relative to its pinned parent: {error}" - ))); - } - } - let dir = open_directory_at(parent, name, label)?; - rustix::fs::fchmod(dir.as_fd(), Mode::from_bits_truncate(mode)).map_err(|error| { - GateError::environment(format!("cannot set mode {mode:o} on {label}: {error}")) - })?; - Ok(dir) -} - -fn verify_system_directory(dir: &File, label: &str) -> Result<()> { - let stat = fstat(dir.as_raw_fd()) - .map_err(|errno| GateError::environment(format!("cannot stat {label}: {errno}")))?; - if !system_gate_parent_is_trusted(stat.st_uid, stat.st_mode as u32) { - return Err(provisioning_error( - "a protected heavy-gate directory has unsafe ownership or mode", - format!( - "{label} must be root-owned and non-writable by group or other; sticky \ - world-writable and uid-owned directories are refused because their entries can \ - still be renamed" - ), - )); - } - Ok(()) -} - -fn verify_slot_file(file: &File, index: usize, uid: u32) -> Result<()> { - let stat = fstat(file.as_raw_fd()).map_err(|errno| { - GateError::environment(format!("cannot stat {}: {errno}", gate_label::slot(index))) - })?; - if (stat.st_mode & libc::S_IFMT) != libc::S_IFREG { - return Err(provisioning_error( - "a provisioned slot has an unsafe file type", - format!("{} is not a regular file", gate_label::slot(index)), - )); - } - if stat.st_uid != uid { - return Err(provisioning_error( - "a provisioned slot has the wrong owner", - format!( - "{} is owned by a different user than the caller", - gate_label::slot(index) - ), - )); - } - if stat.st_mode as u32 & 0o777 != 0o600 { - return Err(provisioning_error( - "a provisioned slot has the wrong mode", - format!( - "{} does not have the required provisioned mode 0600", - gate_label::slot(index) - ), - )); - } - Ok(()) -} - -/// A held slot. Dropping it closes this descriptor; the underlying open file -/// description lock survives as long as any duplicate handed to a child is -/// still open. -#[derive(Debug)] -pub struct SlotGuard { - index: usize, - file: File, -} - -impl SlotGuard { - pub fn index(&self) -> usize { - self.index - } - - /// Duplicate the locked descriptor for the child. - /// - /// `File::try_clone` duplicates the descriptor without duplicating the - /// open file description, which is precisely what makes the child share - /// this slot's lock. Clearing `FD_CLOEXEC` lets the duplicate survive the - /// child's `execve`, so the slot stays held until the child and every - /// process that inherited the descriptor is gone. - pub fn duplicate_for_child(&self) -> Result { - let duplicate = self.file.try_clone().map_err(|error| { - GateError::environment(format!("cannot duplicate heavy-gate slot handle: {error}")) - })?; - fcntl( - duplicate.as_raw_fd(), - FcntlArg::F_SETFD(FdFlag::from_bits_truncate(0)), - ) - .map_err(|errno| { - GateError::environment(format!( - "cannot clear close-on-exec for the heavy-gate slot handle: {errno}" - )) - })?; - Ok(duplicate) - } -} - -/// Retry policy for slot acquisition. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct AcquirePolicy { - pub retry_interval: Duration, - pub timeout: Duration, -} - -impl Default for AcquirePolicy { - fn default() -> Self { - Self { - retry_interval: RETRY_INTERVAL, - timeout: ACQUIRE_TIMEOUT, - } - } -} - -/// Diagnostics emitted while waiting for a slot. -pub trait Progress { - fn waiting(&mut self, waited: Duration); - fn acquired(&mut self, index: usize, waited: Duration); -} - -/// Human-facing progress reporting on stderr. -pub struct StderrProgress; - -impl Progress for StderrProgress { - fn waiting(&mut self, waited: Duration) { - eprintln!( - "heavy-gate: all {SLOT_COUNT} slots busy after {:.0}s; retrying every {}ms \ - (ceiling {}m)", - waited.as_secs_f64(), - RETRY_INTERVAL.as_millis(), - ACQUIRE_TIMEOUT.as_secs() / 60 - ); - } - - fn acquired(&mut self, index: usize, waited: Duration) { - eprintln!( - "heavy-gate: acquired slot {index} of {SLOT_COUNT} after {:.1}s", - waited.as_secs_f64() - ); - } -} - -/// Progress sink that says nothing. Test scaffolding only: production lanes -/// always report their wait through [`StderrProgress`]. -#[cfg(test)] -pub struct SilentProgress; - -#[cfg(test)] -impl Progress for SilentProgress { - fn waiting(&mut self, _waited: Duration) {} - fn acquired(&mut self, _index: usize, _waited: Duration) {} -} - -/// Acquire one of the [`SLOT_COUNT`] slots, or fail closed. -pub fn acquire_slot( - dir: &GateDir, - policy: AcquirePolicy, - progress: &mut dyn Progress, -) -> Result { - let started = Instant::now(); - // Offset the scan so concurrent lanes do not all pile onto slot 0. - let offset = std::process::id() as usize % SLOT_COUNT; - let mut next_notice = WAIT_NOTICE_INTERVAL; - loop { - for step in 0..SLOT_COUNT { - let index = (offset + step) % SLOT_COUNT; - if let Some(guard) = dir.try_acquire(index)? { - progress.acquired(index, started.elapsed()); - return Ok(guard); - } - } - let waited = started.elapsed(); - if waited >= policy.timeout { - return Err(GateError::of( - GateErrorKind::Timeout, - format!( - "no heavy-gate slot became free within {}s; failing closed rather than \ - running unsynchronized. Another heavy lane is still holding both slots \ - under {}.", - policy.timeout.as_secs(), - gate_label::SLOT_DIR - ), - )); - } - if waited >= next_notice { - progress.waiting(waited); - next_notice = waited + WAIT_NOTICE_INTERVAL; - } - sleep( - policy - .retry_interval - .min(policy.timeout.saturating_sub(waited)), - ); - } -} - -/// A parsed `heavy-gate` invocation. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Request { - pub program: OsString, - pub args: Vec, -} - -impl Request { - /// Parse `heavy-gate`'s arguments. - /// - /// The documented form is `heavy-gate -- [args...]`; the - /// separator is optional when the command name is not option-shaped. - pub fn parse(args: &[String]) -> Result> { - if args.is_empty() { - return Err(GateError::usage(format!("{USAGE}\n\nno command given"))); - } - if matches!(args[0].as_str(), "-h" | "--help" | "help") { - return Ok(None); - } - let rest = if args[0] == "--" { &args[1..] } else { args }; - let Some((program, arguments)) = rest.split_first() else { - return Err(GateError::usage(format!( - "{USAGE}\n\nno command follows the `--` separator" - ))); - }; - if program.starts_with('-') { - return Err(GateError::usage(format!( - "{USAGE}\n\nunknown option `{program}`; heavy-gate takes no options" - ))); - } - Ok(Some(Self { - program: OsString::from(program), - args: arguments.iter().map(OsString::from).collect(), - })) - } -} - -const USAGE: &str = "usage: cargo xtask heavy-gate -- [args...]\n\ - usage: cargo xtask heavy-gate verify-slot\n\ - \n\ - Runs under the sole two-slot per-UID heavy-lane semaphore.\n\ - Every Layer-2, host-integration, hardware, live, and perf-heavy command\n\ - must be started this way; the `heavy-*` Makefile targets do it for you.\n\ - \n\ - The command inherits a duplicated handle on the locked slot (its number\n\ - is exported as D2B_HEAVY_GATE_SLOT_FD) and runs in its own process\n\ - group, which the wrapper signals and reaps.\n\ - \n\ - `verify-slot` takes no command: it exits 0 only when this process\n\ - genuinely holds a slot (proved via the inherited descriptor's inode,\n\ - ownership, and an atomic F_OFD_SETLK), and exits 3 otherwise. Shell and\n\ - Make guards use it so a bare, forgeable D2B_HEAVY_GATE cannot bypass the\n\ - semaphore.\n\ - \n\ - If the protected namespace is unavailable, run `make\n\ - heavy-gate-provision`; the gate never falls back to a user-writable\n\ - namespace.\n\ - \n\ - exit codes: 64 usage, 69 open file description locks unsupported,\n\ - 71 cannot start the command, 72 gate directory unusable,\n\ - 75 no slot within the wait ceiling, 3 verify-slot found no held slot.\n\ - Any other code is the command's own."; - -/// Entry point for `cargo xtask heavy-gate`. -pub fn run(args: &[String]) -> ExitCode { - match execute(args) { - Ok(code) => ExitCode::from(code), - Err(error) => { - eprintln!( - "heavy-gate failed [{}]: {}", - error.kind().as_str(), - error.message() - ); - ExitCode::from(error.kind().exit_code()) - } - } -} - -/// How the inherited environment classifies this invocation. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum NestingMarker { - /// No [`GATE_ACTIVE_ENV`] marker; this is a top-level invocation. - TopLevel, - /// A marker is present and the inherited descriptor was verified to be an - /// open handle on the real, currently-locked slot it names. - VerifiedSlot, - /// A marker is present but does not refer to a genuinely held slot: it is - /// forged, stale, closed, or names an unlocked slot. - Unverifiable, -} - -/// Read the inherited slot index and descriptor from the environment. -fn read_nesting_env() -> Option<(usize, RawFd)> { - let index = std::env::var_os(SLOT_INDEX_ENV)? - .to_str()? - .parse::() - .ok()?; - let fd = std::env::var_os(SLOT_FD_ENV)? - .to_str()? - .parse::() - .ok()?; - Some((index, fd)) -} - -/// Classify this invocation against `dir`, verifying any nesting marker rather -/// than trusting its mere presence. -/// -/// A malfunction while proving the inherited descriptor (see -/// [`GateDir::descriptor_is_locked_slot`]) propagates as an [`Err`] so the -/// caller fails closed, rather than being flattened into an unverifiable -/// marker that would silently acquire a fresh slot against a broken -/// environment. -fn classify_nesting(dir: &GateDir) -> Result { - if std::env::var_os(GATE_ACTIVE_ENV).is_none() { - return Ok(NestingMarker::TopLevel); - } - match read_nesting_env() { - Some((index, fd)) => match dir.descriptor_is_locked_slot(index, fd)? { - SlotProof::Held => Ok(NestingMarker::VerifiedSlot), - SlotProof::NotHeld => Ok(NestingMarker::Unverifiable), - }, - None => Ok(NestingMarker::Unverifiable), - } -} - -/// Marker sub-operation selecting the slot-verification check. -/// -/// Unlike the wrapped-command form, this takes no command: it inspects the -/// inherited environment, confirms the caller genuinely holds a heavy-gate -/// slot (matching inode, ownership, and an atomic `F_OFD_SETLK` ownership -/// proof through the inherited descriptor), and reports the verdict purely -/// through its exit status. Shell and Make guards use it so they cannot be -/// fooled by a bare, forgeable `D2B_HEAVY_GATE` marker. -pub const VERIFY_SLOT_OP: &str = "verify-slot"; - -/// Exit code when [`VERIFY_SLOT_OP`] confirms a genuinely held slot. -pub const VERIFY_SLOT_HELD: u8 = 0; - -/// Exit code when [`VERIFY_SLOT_OP`] finds no genuinely held slot. Distinct -/// from every [`GateErrorKind`] code so a caller can tell "not in a slot" (an -/// expected branch that should acquire) apart from a gate malfunction. -pub const VERIFY_SLOT_UNHELD: u8 = 3; - -/// Answer "does this process genuinely hold a heavy-gate slot?" for the shell -/// and Make guards. -/// -/// Returns [`VERIFY_SLOT_HELD`] only when the inherited [`GATE_ACTIVE_ENV`] -/// marker is backed by a descriptor that passes the same inode, ownership, and -/// atomic `F_OFD_SETLK` ownership proof the nesting check uses. A top-level -/// invocation, a forged or stale marker, or a marker naming an unlocked or -/// foreign descriptor all yield [`VERIFY_SLOT_UNHELD`], so a guard that merely -/// exported `D2B_HEAVY_GATE` is told to acquire a real slot instead of running -/// heavy work. A verifier *malfunction* - failing to resolve the gate -/// directory, or an unsupported/environment error while proving ownership - -/// propagates as an [`Err`] and exits with the corresponding -/// [`GateErrorKind`] code, never [`VERIFY_SLOT_UNHELD`], so a caller can tell a -/// genuine "not in a slot" apart from a broken environment and fail closed. -fn verify_slot() -> Result { - let dir = GateDir::resolve()?; - match classify_nesting(&dir)? { - NestingMarker::VerifiedSlot => Ok(VERIFY_SLOT_HELD), - _ => Ok(VERIFY_SLOT_UNHELD), - } -} - -fn execute(args: &[String]) -> Result { - // The internal re-exec shim resolves and verifies its inherited slot; a - // forged marker cannot authorize it into running unsynchronized. - if args.first().is_some_and(|first| first == EXEC_SHIM_FLAG) { - let dir = GateDir::resolve()?; - return exec_wrapped_command(&args[1..], classify_nesting(&dir)?); - } - - // The slot-verification sub-operation reuses the atomic ownership proof - // and reports its verdict through the exit status alone. - if args.first().is_some_and(|first| first == VERIFY_SLOT_OP) { - if args.len() != 1 { - return Err(GateError::usage(format!( - "{USAGE}\n\n`{VERIFY_SLOT_OP}` takes no arguments" - ))); - } - return verify_slot(); - } - - // Parse before touching the filesystem so `--help` works even where the - // gate directory is unusable. - let Some(request) = Request::parse(args)? else { - println!("{USAGE}"); - return Ok(0); - }; - - let dir = GateDir::resolve()?; - match classify_nesting(&dir)? { - NestingMarker::VerifiedSlot => { - eprintln!( - "heavy-gate: reusing the verified inherited slot instead of \ - acquiring a second one" - ); - supervise(&request, None) - } - marker => { - if marker == NestingMarker::Unverifiable { - eprintln!( - "heavy-gate: the inherited D2B_HEAVY_GATE marker does not refer to a \ - genuinely held slot; acquiring a real slot rather than trusting it" - ); - } - dir.probe_ofd_support()?; - let guard = acquire_slot(&dir, AcquirePolicy::default(), &mut StderrProgress)?; - supervise(&request, Some(&guard)) - } - } -} - -/// The one-shot re-exec shim. -/// -/// Runs in the already-forked child, in its own process group, holding the -/// inherited slot descriptor. It clears the signal mask the wrapper needed for -/// its `signalfd` and then replaces itself with the real command, so the -/// command keeps this pid and process group but starts with default signal -/// dispositions and an empty mask. It only returns on failure. -fn exec_wrapped_command(args: &[String], nesting: NestingMarker) -> Result { - // The shim runs no slot acquisition of its own, so it must only ever be - // reachable from a wrapper whose held slot we have actually verified. - if nesting != NestingMarker::VerifiedSlot { - return Err(GateError::usage(format!( - "{USAGE}\n\nheavy-gate takes no options" - ))); - } - let Some(request) = Request::parse(args)? else { - return Err(GateError::usage(format!("{USAGE}\n\nno command given"))); - }; - exec_shim(&request) -} - -/// Clear the wrapper's signal mask and become the wrapped command. -fn exec_shim(request: &Request) -> Result { - let mut mask = SigSet::empty(); - for signal in FORWARDED_SIGNALS { - mask.add(signal); - } - mask.thread_unblock().map_err(|errno| { - GateError::of( - GateErrorKind::Spawn, - format!("cannot restore the heavy lane's signal mask: {errno}"), - ) - })?; - let error = Command::new(&request.program).args(&request.args).exec(); - Err(GateError::of( - GateErrorKind::Spawn, - format!( - "cannot start `{}`: {error}", - crate::diagnostic_redaction::redact_path(Path::new(&request.program)) - ), - )) -} - -/// Blocked-signal relay. Restores the caller's mask on drop. -struct SignalRelay { - mask: SigSet, - fd: SignalFd, -} - -impl SignalRelay { - fn install() -> Result { - let mut mask = SigSet::empty(); - for signal in FORWARDED_SIGNALS { - mask.add(signal); - } - mask.thread_block().map_err(|errno| { - GateError::of( - GateErrorKind::Spawn, - format!("cannot block terminating signals: {errno}"), - ) - })?; - let fd = SignalFd::with_flags(&mask, SfdFlags::SFD_NONBLOCK | SfdFlags::SFD_CLOEXEC) - .map_err(|errno| { - GateError::of( - GateErrorKind::Spawn, - format!("cannot create the heavy-gate signalfd: {errno}"), - ) - })?; - Ok(Self { mask, fd }) - } - - fn drain(&self) -> Vec { - let mut received = Vec::new(); - while let Ok(Some(info)) = self.fd.read_signal() { - if let Ok(signal) = Signal::try_from(info.ssi_signo as i32) { - received.push(signal); - } - } - received - } -} - -impl Drop for SignalRelay { - fn drop(&mut self) { - let _ = self.mask.thread_unblock(); - } -} - -/// Build the command that starts the wrapped lane. -/// -/// The lane is started through a one-shot re-exec of this binary so the real -/// command runs with a cleared signal mask; see the module documentation. -fn shim_command(request: &Request) -> Result { - #[cfg(test)] - if let Some(command) = tests::test_shim_command(request) { - return Ok(command); - } - let shim = std::env::current_exe().map_err(|error| { - GateError::of( - GateErrorKind::Spawn, - format!("cannot locate the heavy-gate binary to re-exec: {error}"), - ) - })?; - let mut command = Command::new(shim); - command.arg("heavy-gate"); - command.arg(EXEC_SHIM_FLAG); - command.arg("--"); - command.arg(&request.program); - command.args(&request.args); - Ok(command) -} - -fn supervise(request: &Request, guard: Option<&SlotGuard>) -> Result { - let relay = SignalRelay::install()?; - - let mut command = shim_command(request)?; - // The child leads its own process group so the wrapper can signal the - // whole lane, and so a terminal `Ctrl-C` reaches the wrapper first. - command.process_group(0); - command.env(GATE_ACTIVE_ENV, "1"); - - let handoff = match guard { - Some(slot) => { - let duplicate = slot.duplicate_for_child()?; - command.env(SLOT_INDEX_ENV, slot.index().to_string()); - command.env(SLOT_FD_ENV, duplicate.as_raw_fd().to_string()); - Some(duplicate) - } - None => None, - }; - - let mut child = command.spawn().map_err(|error| { - GateError::of( - GateErrorKind::Spawn, - format!( - "cannot start the heavy lane for `{}`: {error}", - crate::diagnostic_redaction::redact_path(Path::new(&request.program)) - ), - ) - })?; - // The child now has its own descriptor for the same open file - // description, so the wrapper's duplicate is no longer needed. The slot - // stays locked: the wrapper still holds `guard`, and the child holds an - // inherited handle for its whole life. - drop(handoff); - - let group = Pid::from_raw(child.id() as i32); - let leader = group; - - let outcome = supervise_loop( - // Observe the leader's exit *without reaping it*: `WNOWAIT` leaves the - // zombie in place so its pid and pgid stay reserved until the sweep - // has run. Reaping here (as `try_wait` would) frees the pgid and opens - // a window in which the kernel could recycle it onto an unrelated - // group before the unconditional `SIGKILL` sweep fires. - || observe_exit(leader), - || relay.drain(), - |signal| { - eprintln!("heavy-gate: forwarding {signal} to the heavy lane process group"); - let _ = killpg(group, signal); - }, - || { - let _ = killpg(group, Signal::SIGKILL); - }, - // Reap the leader only after the final sweep has returned, so the - // zombie pinned the pgid across the whole teardown. `Child::wait` - // reaps the zombie `observe_exit` deliberately left behind. - || { - let _ = child.wait(); - }, - || sleep(POLL_INTERVAL), - Instant::now, - )?; - - Ok(resolve_exit_code( - outcome.code, - outcome.signal, - outcome.interrupt.map(|signal| signal as i32), - )) -} - -/// Observe the process leader's terminal disposition without reaping it. -/// -/// `waitid` with `WEXITED | WNOHANG | WNOWAIT` reports a terminated child but -/// leaves it as an unreaped zombie, which keeps its pid and process-group id -/// reserved. That reservation is what lets the caller sweep the group with -/// `SIGKILL` before reaping without risking the pgid being recycled onto an -/// unrelated group in between. `WNOHANG` keeps the poll nonblocking. -fn observe_exit(leader: Pid) -> Result> { - let flags = WaitPidFlag::WEXITED | WaitPidFlag::WNOHANG | WaitPidFlag::WNOWAIT; - match waitid(Id::Pid(leader), flags) { - Ok(WaitStatus::Exited(_, code)) => Ok(Some(ChildExit { - code: Some(code), - signal: None, - })), - Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(ChildExit { - code: None, - signal: Some(signal as i32), - })), - Ok(_) => Ok(None), - Err(errno) => Err(GateError::of( - GateErrorKind::Spawn, - format!("cannot observe the heavy-lane child: {errno}"), - )), - } -} - -/// The child's terminal disposition, abstracted so the supervision loop can be -/// driven deterministically in tests. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct ChildExit { - code: Option, - signal: Option, -} - -/// What the supervision loop observed over the wrapped lane's lifetime. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct SuperviseOutcome { - code: Option, - signal: Option, - interrupt: Option, -} - -/// The core supervision loop, factored out so its signal-drain ordering is -/// unit-testable with injected effects. -/// -/// The ordering is load-bearing for slot release: -/// -/// * Drain pending signals *before* every exit check, so a terminating signal -/// that arrives while the leader is exiting is forwarded, not lost. -/// * Once the child is observed exited, drain *again* while the forwarded -/// signal mask is still blocked, catching a signal delivered between the -/// last drain and the exit. -/// * Unconditionally sweep the whole process group with `SIGKILL` after the -/// leader exits, so no descendant can outlive the wrapper still holding the -/// inherited slot descriptor - even if a signal is pending that will kill -/// the wrapper the moment the mask is restored. -/// * Reap the leader only *after* that final sweep. The leader is observed as -/// an unreaped zombie, so its pid and pgid stay reserved across the sweep; a -/// pgid recycled onto an unrelated group between the exit and the `SIGKILL` -/// is therefore impossible. -fn supervise_loop( - mut poll_exit: Poll, - mut drain: Drain, - mut forward: Forward, - mut sweep: Sweep, - mut reap: Reap, - mut nap: Nap, - mut now: Now, -) -> Result -where - Poll: FnMut() -> Result>, - Drain: FnMut() -> Vec, - Forward: FnMut(Signal), - Sweep: FnMut(), - Reap: FnMut(), - Nap: FnMut(), - Now: FnMut() -> Instant, -{ - let mut interrupt: Option = None; - let mut escalate_at: Option = None; - - let exit = loop { - for signal in drain() { - forward(signal); - if interrupt.is_none() { - interrupt = Some(signal); - } - if escalate_at.is_none() { - escalate_at = Some(now() + TERMINATION_GRACE); - } - } - - if let Some(exit) = poll_exit()? { - break exit; - } - - if let Some(deadline) = escalate_at - && now() >= deadline - { - eprintln!( - "heavy-gate: heavy lane still running {}s after the first signal; \ - sending SIGKILL to its process group", - TERMINATION_GRACE.as_secs() - ); - sweep(); - escalate_at = None; - } - - nap(); - }; - - // The child has exited, but the forwarded-signal mask is still blocked. - // Drain once more so a termination signal that landed between the final - // drain and the exit is still recorded as an interruption for the exit - // code. - for signal in drain() { - if interrupt.is_none() { - interrupt = Some(signal); - } - } - - // Unconditionally sweep the whole process group with SIGKILL before the - // caller restores the forwarded-signal mask. An earlier form swept only - // when an interrupt had been *observed*, which left a gap: a signal - // arriving after this final drain but before a conditional sweep stays - // pending, and `SignalRelay::drop` then unblocks it and kills the wrapper - // while descendants that inherited the slot descriptor are still alive, - // orphaning the slot. Sweeping unconditionally guarantees the process - // group is gone before the mask is restored, so a late pending signal can - // only terminate an already-childless wrapper, which releases the slot as - // it exits. The sweep runs while the leader is still an unreaped zombie so - // its pid and pgid cannot be recycled onto an unrelated group; the caller - // reaps the leader only after the sweep returns. - sweep(); - - // Reap the leader now that the group is gone. Doing this after the sweep - // keeps the leader's zombie pinning the pgid for the whole teardown. - reap(); - - Ok(SuperviseOutcome { - code: exit.code, - signal: exit.signal, - interrupt, - }) -} - -/// Map the child's disposition onto the wrapper's exit code. -/// -/// A signalled child reports `128 + signal`, matching shell convention. When -/// the wrapper itself was interrupted but the child still exited normally, -/// the interruption wins so a `Ctrl-C`-ed lane never looks successful. -fn resolve_exit_code(code: Option, signal: Option, interrupt: Option) -> u8 { - if let Some(signal) = signal.or(interrupt) { - return (128 + signal).clamp(1, 255) as u8; - } - match code { - Some(code) => (code & 0xff) as u8, - None => 1, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rustix::fs::AtFlags; - use std::ffi::OsStr; - use std::fs::{self, OpenOptions}; - use std::io::Write; - use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; - use std::process::Stdio; - use std::sync::atomic::{AtomicU32, Ordering}; - use std::sync::{Mutex, MutexGuard, PoisonError}; - - /// Serialises every test that either asserts exact open file description - /// lock state or spawns a child process. - /// - /// `fork` copies the whole descriptor table, and close-on-exec descriptors - /// stay open in the child until it reaches `execve`. A spawn on one test - /// thread can therefore keep another test's slot descriptor alive for a - /// moment and make a released slot still look held. Production never sees - /// this: one wrapper process spawns exactly one child. - static LOCK_STATE: Mutex<()> = Mutex::new(()); - - fn exclusive() -> MutexGuard<'static, ()> { - LOCK_STATE.lock().unwrap_or_else(PoisonError::into_inner) - } - - fn scan_metadata(path: &Path) -> std::io::Result { - if std::env::var_os("RUNFILES_DIR").is_some() { - fs::metadata(path) - } else { - fs::symlink_metadata(path) - } - } - - /// Env var selecting the in-test slot-holder child mode. - const HOLD_ROOT_ENV: &str = "D2B_HEAVY_GATE_TEST_HOLD_ROOT"; - /// Env var selecting the in-test full-wrapper child mode. - const WRAP_ROOT_ENV: &str = "D2B_HEAVY_GATE_TEST_WRAP_ROOT"; - /// Env var selecting the in-test verify-slot child mode. - const VERIFY_ROOT_ENV: &str = "D2B_HEAVY_GATE_TEST_VERIFY_ROOT"; - - const HOLDER_TEST: &str = "heavy_gate::tests::slot_holder_child_mode"; - const WRAPPER_TEST: &str = "heavy_gate::tests::full_wrapper_child_mode"; - const SHIM_TEST: &str = "heavy_gate::tests::exec_shim_child_mode"; - const VERIFY_TEST: &str = "heavy_gate::tests::verify_slot_child_mode"; - - /// Env vars carrying the wrapped command into the in-test shim. - const SHIM_PROGRAM_ENV: &str = "D2B_HEAVY_GATE_TEST_SHIM_PROGRAM"; - const SHIM_ARGS_ENV: &str = "D2B_HEAVY_GATE_TEST_SHIM_ARGS"; - - /// Separator for the encoded argument vector; a unit separator cannot - /// appear in the arguments these tests pass. - const ARG_SEPARATOR: u8 = 0x1f; - - fn encode_args(args: &[OsString]) -> OsString { - use std::os::unix::ffi::{OsStrExt, OsStringExt}; - let mut bytes = Vec::new(); - for (index, arg) in args.iter().enumerate() { - if index > 0 { - bytes.push(ARG_SEPARATOR); - } - bytes.extend_from_slice(arg.as_bytes()); - } - OsString::from_vec(bytes) - } - - fn decode_args(value: &OsStr) -> Vec { - use std::os::unix::ffi::{OsStrExt, OsStringExt}; - let bytes = value.as_bytes(); - if bytes.is_empty() { - return Vec::new(); - } - bytes - .split(|byte| *byte == ARG_SEPARATOR) - .map(|chunk| OsString::from_vec(chunk.to_vec())) - .collect() - } - - /// In-test stand-in for the production re-exec shim. - /// - /// The test binary is `libtest`, not the `xtask` dispatcher, so it cannot - /// re-invoke itself as `heavy-gate --exec-child`. Tests reach exactly the - /// same shim code through a dedicated child-mode test instead. - pub(super) fn test_shim_command(request: &Request) -> Option { - let exe = std::env::current_exe().ok()?; - let mut command = Command::new(exe); - command.args([SHIM_TEST, "--exact", "--nocapture", "--test-threads=1"]); - command.env(SHIM_PROGRAM_ENV, &request.program); - command.env(SHIM_ARGS_ENV, encode_args(&request.args)); - Some(command) - } - - static SCRATCH_SEQUENCE: AtomicU32 = AtomicU32::new(0); - - /// Self-cleaning scratch directory under the cargo target directory, so - /// no test writes into the repository tree. - struct Scratch { - path: PathBuf, - } - - impl Scratch { - fn new(label: &str) -> Self { - let target = match std::env::var_os("TEST_TMPDIR") - .or_else(|| std::env::var_os("CARGO_TARGET_DIR")) - { - Some(dir) => PathBuf::from(dir), - None => crate::repo_root() - .expect("xtask has a repository root") - .join("target"), - }; - let sequence = SCRATCH_SEQUENCE.fetch_add(1, Ordering::Relaxed); - let path = target - .join("heavy-gate-tests") - .join(format!("{label}-{}-{sequence}", std::process::id())); - let _ = fs::remove_dir_all(&path); - fs::create_dir_all(&path).expect("scratch directory is creatable"); - // Lock the scratch root down to 0700 so the gate's root-trust - // check is deterministic regardless of the runner's umask: an - // owned-but-group-writable root is (correctly) refused by prepare. - fs::set_permissions(&path, fs::Permissions::from_mode(0o700)) - .expect("scratch root mode is settable"); - Self { path } - } - - fn path(&self) -> &Path { - &self.path - } - } - - impl Drop for Scratch { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.path); - } - } - - fn gate_dir_under(root: &Path) -> GateDir { - GateDir::prepare_test(root, getuid().as_raw()).expect("gate directory is preparable") - } - - /// Asserts a gate diagnostic names components by role only. Every message - /// `run` prints to stderr and CI logs verbatim, so it must carry neither - /// an absolute path (a supplied root or a resolved runtime path) nor the - /// caller's numeric uid in any of the historical leak shapes. The redacted - /// diagnostics use role labels and the shell-style `$UID` placeholder. - fn assert_no_path_or_uid(message: &str, roots: &[&Path]) { - for root in roots { - let root = root.to_string_lossy(); - assert!( - !message.contains(root.as_ref()), - "a gate diagnostic must not leak {root}: {message}" - ); - } - assert!( - !message.contains("/home") && !message.contains("/target/"), - "a gate diagnostic must not leak HOME or a build path: {message}" - ); - let uid = getuid().as_raw(); - for leak in [ - format!("uid {uid}"), - format!("uid-{uid}"), - format!("/run/user/{uid}"), - ] { - assert!( - !message.contains(&leak), - "a gate diagnostic must not leak the caller's uid as {leak:?}: {message}" - ); - } - } - - fn wait_for(deadline: Duration, mut ready: impl FnMut() -> bool) -> bool { - let started = Instant::now(); - while started.elapsed() < deadline { - if ready() { - return true; - } - sleep(Duration::from_millis(10)); - } - ready() - } - - // ---- pure contract ------------------------------------------------- - - #[test] - fn gate_leaf_operations_remain_fd_relative_and_procfs_independent() { - let source = include_str!("heavy_gate.rs"); - let production = source - .split("#[cfg(test)]\nmod tests") - .next() - .expect("the production portion precedes the test module"); - let proc_fd_leaf = ["/proc/self", "/fd/"].concat(); - let old_helper = ["anchored", "_path("].concat(); - - assert!( - !production.contains(&proc_fd_leaf), - "production gate operations must not reconstruct paths through procfs" - ); - assert!( - !production.contains(&old_helper), - "the pathname reconstruction helper must not return" - ); - assert!( - production.contains("rustix::fs::openat(") - && production.contains("rustix::fs::mkdirat("), - "slot opens and test-only fixture creation must stay fd-relative" - ); - } - - #[test] - fn gate_root_is_a_fixed_constant_independent_of_the_uid_and_runtime_dir() { - // Production (no injected override) is always the single fixed root, - // never a function of the uid and never a function of whether - // /run/user/ happens to exist. Two different uids resolve to the - // same root; per-uid isolation comes from the uid- slot directory - // beneath it, not from the root. This is what makes two lanes for the - // same uid contend for one slot pool even if a login or logout races - // their startup. - assert_eq!( - gate_root_from(1000, None), - PathBuf::from("/run/d2b-heavy-gates") - ); - assert_eq!( - gate_root_from(1001, None), - PathBuf::from("/run/d2b-heavy-gates") - ); - // The injectable seam (tests only) wins when present, so the crate's - // own tests can redirect the namespace to a scratch directory. - assert_eq!( - gate_root_from(1000, Some(Path::new("/scratch/gate"))), - PathBuf::from("/scratch/gate") - ); - } - - #[test] - fn gate_directory_is_scoped_per_uid() { - let first = gate_dir_path(Path::new("/run/d2b-heavy-gates"), 1000); - let second = gate_dir_path(Path::new("/run/d2b-heavy-gates"), 1001); - assert!(first.ends_with("d2b-heavy-gates/uid-1000")); - assert!(second.ends_with("d2b-heavy-gates/uid-1001")); - assert_ne!(first, second); - } - - #[test] - fn unsupported_locking_is_never_treated_as_retryable() { - for errno in [ - Errno::EINVAL, - Errno::ENOLCK, - Errno::ENOSYS, - Errno::EOPNOTSUPP, - ] { - assert_eq!( - classify_lock_errno(errno), - LockOutcome::Unsupported, - "{errno} must fail closed, never fall back" - ); - } - for errno in [Errno::EAGAIN, Errno::EACCES] { - assert_eq!(classify_lock_errno(errno), LockOutcome::Busy); - } - assert_eq!(classify_lock_errno(Errno::EIO), LockOutcome::Environment); - } - - #[test] - fn every_failure_class_has_a_distinct_nonzero_exit_code() { - let kinds = [ - GateErrorKind::Usage, - GateErrorKind::Unsupported, - GateErrorKind::Timeout, - GateErrorKind::Environment, - GateErrorKind::Spawn, - ]; - let mut codes: Vec = kinds.iter().map(|kind| kind.exit_code()).collect(); - assert!(codes.iter().all(|code| *code != 0)); - codes.sort_unstable(); - codes.dedup(); - assert_eq!(codes.len(), kinds.len(), "exit codes must be distinct"); - } - - #[test] - fn unsupported_error_message_refuses_a_fallback() { - let scratch = Scratch::new("unsupported-message"); - let dir = gate_dir_under(scratch.path()); - let error = dir.unsupported_error(Errno::EINVAL); - assert_eq!(error.kind(), GateErrorKind::Unsupported); - assert!(error.message().contains("F_OFD_SETLK")); - assert!(error.message().contains("fails closed")); - assert!(error.message().contains("flock")); - } - - #[test] - fn provisioning_diagnostic_is_stable_actionable_and_redacted() { - let error = provisioning_error( - "the protected heavy-gate root is unavailable", - "the heavy-gate root directory cannot be opened", - ); - assert_eq!(error.kind(), GateErrorKind::Environment); - assert!(error.message().contains( - "code: heavy-gate-provisioning-required; observed: the protected heavy-gate root is unavailable" - )); - assert!( - error - .message() - .contains("remediation: run `make heavy-gate-provision`") - ); - assert!(error.message().contains("no fallback namespace was used")); - assert_no_path_or_uid(error.message(), &[]); - } - - #[test] - fn exit_code_reports_signals_and_interruptions() { - assert_eq!(resolve_exit_code(Some(0), None, None), 0); - assert_eq!(resolve_exit_code(Some(7), None, None), 7); - assert_eq!( - resolve_exit_code(None, Some(Signal::SIGKILL as i32), None), - 128 + 9 - ); - assert_eq!( - resolve_exit_code(Some(0), None, Some(Signal::SIGINT as i32)), - 128 + 2 - ); - assert_eq!(resolve_exit_code(None, None, None), 1); - } - - #[test] - fn parse_accepts_the_documented_and_bare_forms() { - let separated = Request::parse(&["--".into(), "make".into(), "check".into()]) - .expect("parses") - .expect("is not help"); - let bare = Request::parse(&["make".into(), "check".into()]) - .expect("parses") - .expect("is not help"); - assert_eq!(separated, bare); - assert_eq!(separated.program, OsString::from("make")); - assert_eq!(separated.args, vec![OsString::from("check")]); - } - - #[test] - fn parse_rejects_malformed_invocations() { - assert_eq!( - Request::parse(&[]).unwrap_err().kind(), - GateErrorKind::Usage - ); - assert_eq!( - Request::parse(&["--".into()]).unwrap_err().kind(), - GateErrorKind::Usage - ); - assert_eq!( - Request::parse(&["--jobs".into()]).unwrap_err().kind(), - GateErrorKind::Usage - ); - assert!( - Request::parse(&["--help".into()]) - .expect("help parses") - .is_none() - ); - } - - #[test] - fn the_internal_exec_shim_is_not_an_operator_entry_point() { - // Neither a plain top-level invocation nor an unverifiable (forged or - // stale) nesting marker may authorise the shim; only a verified slot - // may. Otherwise a lane could start unsynchronised by naming it. - for marker in [NestingMarker::TopLevel, NestingMarker::Unverifiable] { - let error = exec_wrapped_command(&["--".into(), "true".into()], marker) - .expect_err("the shim is refused without a verified slot"); - assert_eq!(error.kind(), GateErrorKind::Usage); - assert!( - !error.message().contains(EXEC_SHIM_FLAG), - "the internal marker must stay out of operator-visible text: {}", - error.message() - ); - } - } - - // ---- slot mechanics ------------------------------------------------ - - #[test] - fn ofd_locking_is_available_on_the_scratch_filesystem() { - let _serial = exclusive(); - let scratch = Scratch::new("probe"); - let dir = gate_dir_under(scratch.path()); - dir.probe_ofd_support().expect("probe succeeds"); - assert!( - dir.path().join("slot-0").is_file(), - "the OFD probe uses the canonical slot instead of a removable probe leaf" - ); - } - - #[test] - fn exactly_two_slots_are_available_and_release_frees_them() { - let _serial = exclusive(); - let scratch = Scratch::new("two-slots"); - let dir = gate_dir_under(scratch.path()); - - // Separate `open` calls create separate open file descriptions, so - // these contend exactly as two processes would. - let first = dir.try_acquire(0).unwrap().expect("slot 0 is free"); - let second = dir.try_acquire(1).unwrap().expect("slot 1 is free"); - assert!(dir.try_acquire(0).unwrap().is_none(), "slot 0 stays held"); - assert!(dir.try_acquire(1).unwrap().is_none(), "slot 1 stays held"); - - let index = second.index(); - drop(second); - let reacquired = dir - .try_acquire(index) - .unwrap() - .expect("the released slot is free again"); - assert_eq!(reacquired.index(), index); - drop(reacquired); - drop(first); - } - - #[test] - fn acquire_fails_closed_when_both_slots_stay_held() { - let _serial = exclusive(); - let scratch = Scratch::new("timeout"); - let dir = gate_dir_under(scratch.path()); - let _held: Vec = (0..SLOT_COUNT) - .map(|index| dir.try_acquire(index).unwrap().expect("slot is free")) - .collect(); - - let policy = AcquirePolicy { - retry_interval: Duration::from_millis(5), - timeout: Duration::from_millis(60), - }; - let error = acquire_slot(&dir, policy, &mut SilentProgress).unwrap_err(); - assert_eq!(error.kind(), GateErrorKind::Timeout); - assert!(error.message().contains("failing closed")); - } - - #[test] - fn prepare_rejects_a_symlinked_gate_directory() { - let scratch = Scratch::new("symlink"); - let decoy = scratch.path().join("decoy"); - fs::create_dir_all(&decoy).unwrap(); - std::os::unix::fs::symlink(&decoy, gate_dir_path(scratch.path(), getuid().as_raw())) - .unwrap(); - - let error = GateDir::prepare_test(scratch.path(), getuid().as_raw()).unwrap_err(); - assert_eq!(error.kind(), GateErrorKind::Environment); - assert!( - error.message().contains("symlink") || error.message().contains("not a directory"), - "a symlinked slot directory is refused by fd-relative O_NOFOLLOW: {}", - error.message() - ); - } - - #[test] - fn prepare_rejects_a_group_accessible_slot_directory() { - let scratch = Scratch::new("loose-mode"); - let path = gate_dir_path(scratch.path(), getuid().as_raw()); - fs::create_dir_all(&path).unwrap(); - fs::set_permissions(&path, fs::Permissions::from_mode(0o770)).unwrap(); - - let error = GateDir::prepare_test(scratch.path(), getuid().as_raw()).unwrap_err(); - assert_eq!(error.kind(), GateErrorKind::Environment); - assert!(error.message().contains("private")); - } - - #[test] - fn prepare_rejects_a_group_writable_root() { - // A root we own but that is group- or world-writable is refused: a peer - // in that directory could rename the whole `d2b-heavy-gates` tree out - // from under a later invocation, splitting the semaphore into a second - // namespace. The test seam accepts only a private uid-owned root. - let scratch = Scratch::new("loose-root"); - fs::set_permissions(scratch.path(), fs::Permissions::from_mode(0o777)).unwrap(); - - let error = GateDir::prepare_test(scratch.path(), getuid().as_raw()).unwrap_err(); - assert_eq!(error.kind(), GateErrorKind::Environment); - assert!( - error.message().contains("heavy-gate root"), - "the failure names the untrusted root: {}", - error.message() - ); - // Restore a sane mode so Scratch teardown can remove the tree. - let _ = fs::set_permissions(scratch.path(), fs::Permissions::from_mode(0o700)); - } - - #[test] - fn refused_gate_directories_name_roles_never_paths_or_the_uid() { - // Every GateDir preparation refusal is printed by `run` to stderr and CI - // logs verbatim. Force the two representative refusals and assert each - // names its component by role only - never the resolved runtime path - // and never the caller's numeric uid. The leak escaped twice before - // precisely because nothing asserted on this output. - - // A world- or group-writable root is untrusted (names the ROOT role). - let root = Scratch::new("gate-redaction-root"); - fs::set_permissions(root.path(), fs::Permissions::from_mode(0o777)).unwrap(); - let error = GateDir::prepare_test(root.path(), getuid().as_raw()) - .expect_err("a world-writable root must be refused"); - assert_eq!(error.kind(), GateErrorKind::Environment); - assert_no_path_or_uid(error.message(), &[root.path()]); - let _ = fs::set_permissions(root.path(), fs::Permissions::from_mode(0o700)); - - // A pre-existing group-accessible per-uid slot directory is refused - // (names the slot-directory role). - let loose = Scratch::new("gate-redaction-slot"); - let slot_dir = gate_dir_path(loose.path(), getuid().as_raw()); - fs::create_dir_all(&slot_dir).unwrap(); - fs::set_permissions(&slot_dir, fs::Permissions::from_mode(0o770)).unwrap(); - let error = GateDir::prepare_test(loose.path(), getuid().as_raw()) - .expect_err("a group-accessible slot directory must be refused"); - assert_eq!(error.kind(), GateErrorKind::Environment); - assert_no_path_or_uid(error.message(), &[loose.path(), &slot_dir]); - } - - #[test] - fn prepare_accepts_a_private_root() { - // Test child modes need an unprivileged scratch namespace; production - // never accepts this shape. - let scratch = Scratch::new("private-root"); - let dir = GateDir::prepare_test(scratch.path(), getuid().as_raw()) - .expect("a 0700 owned test root is trusted"); - assert!(dir.path().ends_with(format!("uid-{}", getuid().as_raw()))); - } - - #[test] - fn production_rejects_a_uid_owned_namespace_before_using_slots() { - let scratch = Scratch::new("uid-owned-production-root"); - let uid = getuid().as_raw(); - let slot_dir = gate_dir_path(scratch.path(), uid); - fs::create_dir_all(&slot_dir).unwrap(); - for index in 0..SLOT_COUNT { - fs::write(slot_dir.join(format!("slot-{index}")), "").unwrap(); - } - - let error = GateDir::open_provisioned(scratch.path(), uid) - .expect_err("a uid-owned namespace is renameable and must be refused"); - assert_eq!(error.kind(), GateErrorKind::Environment); - assert!( - error.message().contains("root-owned") - && error.message().contains("sticky") - && error.message().contains("renamed"), - "the refusal clearly diagnoses the unsafe ownership: {}", - error.message() - ); - assert_no_path_or_uid(error.message(), &[scratch.path(), &slot_dir]); - } - - #[test] - fn missing_provisioned_root_fails_closed_without_a_fallback() { - let scratch = Scratch::new("missing-production-root"); - let missing = scratch.path().join("absent"); - let error = GateDir::open_provisioned(&missing, getuid().as_raw()) - .expect_err("a missing provisioned root must fail closed"); - assert_eq!(error.kind(), GateErrorKind::Environment); - assert!(error.message().contains("unavailable")); - assert!(error.message().contains(PROVISIONING_ERROR_CODE)); - assert!(error.message().contains(PROVISIONING_ACTION)); - assert!(error.message().contains("observed:")); - assert!(error.message().contains("no fallback")); - assert!( - !missing.exists(), - "resolution never creates the missing root" - ); - assert_no_path_or_uid(error.message(), &[scratch.path(), &missing]); - } - - #[test] - fn duplicated_handle_survives_exec_and_keeps_the_slot_locked() { - let _serial = exclusive(); - let scratch = Scratch::new("handoff"); - let dir = gate_dir_under(scratch.path()); - let guard = dir.try_acquire(0).unwrap().expect("slot 0 is free"); - let duplicate = guard.duplicate_for_child().expect("duplicate succeeds"); - - let flags = fcntl(duplicate.as_raw_fd(), FcntlArg::F_GETFD).expect("F_GETFD works"); - assert_eq!( - flags & libc::FD_CLOEXEC, - 0, - "the child's handle must survive execve" - ); - - // Dropping the wrapper's own guard while the duplicate is still open - // must NOT release the open file description lock. - drop(guard); - assert!( - dir.try_acquire(0).unwrap().is_none(), - "the slot stays held while the child's duplicate is open" - ); - drop(duplicate); - assert!( - dir.try_acquire(0).unwrap().is_some(), - "the slot frees once every handle is closed" - ); - } - - // ---- multi-process behaviour -------------------------------------- - // - // `xtask` is a binary-only crate, so there is no `tests/` tree to put a - // real integration test in. These tests re-execute the test binary with - // an env-selected mode to get genuinely separate processes. - - /// Child mode: hold one slot until the parent creates `release`. - /// - /// A no-op in ordinary runs; it only does work when the parent selects it. - #[test] - fn slot_holder_child_mode() { - let Some(root) = std::env::var_os(HOLD_ROOT_ENV) else { - return; - }; - let root = PathBuf::from(root); - let dir = GateDir::resolve().expect("the child resolves the same gate directory"); - assert_eq!( - dir.path(), - gate_dir_path(&root, getuid().as_raw()), - "the child must resolve XDG_RUNTIME_DIR to the parent's scratch gate" - ); - dir.probe_ofd_support().expect("probe succeeds"); - let guard = acquire_slot(&dir, AcquirePolicy::default(), &mut SilentProgress) - .expect("the child acquires a slot"); - - let ready = root.join(format!("ready-{}", std::process::id())); - File::create(&ready).unwrap().write_all(b"held").unwrap(); - - let release = root.join("release"); - assert!( - wait_for(Duration::from_secs(30), || release.exists()), - "the parent released the holder" - ); - drop(guard); - } - - /// Child mode: run the whole wrapper over a probe command. - #[test] - fn full_wrapper_child_mode() { - if std::env::var_os(WRAP_ROOT_ENV).is_none() { - return; - } - // The probe asserts the inherited slot descriptor is real and still - // open in the grandchild, that the command starts with none of the - // wrapper's forwarded signals blocked, and then exits with a - // distinctive code. - let blocked_mask = 0x4007u32; // SIGHUP | SIGINT | SIGQUIT | SIGTERM - let script = format!( - "test -n \"${SLOT_FD_ENV}\" || exit 40; \ - test -e /proc/self/fd/${SLOT_FD_ENV} || exit 41; \ - test -n \"${GATE_ACTIVE_ENV}\" || exit 42; \ - blocked=$(sed -n 's/^SigBlk:[[:space:]]*//p' /proc/self/status); \ - test $(( 0x$blocked & {blocked_mask} )) -eq 0 || exit 43; \ - exit 7" - ); - let code = execute(&["--".into(), "sh".into(), "-c".into(), script]) - .expect("the wrapper runs the probe"); - std::process::exit(i32::from(code)); - } - - /// Child mode: the in-test re-exec shim. - /// - /// Selected by [`test_shim_command`]; a no-op in ordinary runs. - #[test] - fn exec_shim_child_mode() { - let Some(program) = std::env::var_os(SHIM_PROGRAM_ENV) else { - return; - }; - let args = decode_args(&std::env::var_os(SHIM_ARGS_ENV).unwrap_or_default()); - let request = Request { program, args }; - let error = exec_shim(&request).expect_err("exec only returns on failure"); - eprintln!("{}", error.message()); - std::process::exit(i32::from(error.kind().exit_code())); - } - - #[test] - fn a_spawn_failure_keeps_unambiguous_repository_context() { - // A wrapped program that does not exist must fail closed while its - // diagnostic keeps repository-relative context without exposing the - // absolute checkout. `exec_shim` runs the exec in-process, so a missing - // program returns the spawn diagnostic directly. - let repo = crate::repo_root().unwrap(); - let program = repo.join(".scratch/redaction-sentinel-lane/does-not-exist"); - let request = Request { - program: program.into_os_string(), - args: Vec::new(), - }; - let error = exec_shim(&request).expect_err("a missing program must fail to exec"); - let message = error.message(); - assert!( - !message.contains(repo.to_str().unwrap()), - "the spawn diagnostic leaked its absolute checkout: {message}" - ); - assert!( - message.contains("/.scratch/redaction-sentinel-lane/does-not-exist"), - "the spawn diagnostic must preserve repository-relative context: {message}" - ); - } - - fn spawn_child_mode(test_name: &str, root: &Path, env_key: &str) -> std::process::Child { - Command::new(std::env::current_exe().expect("the test binary path is known")) - .args([test_name, "--exact", "--nocapture", "--test-threads=1"]) - .env(env_key, root) - .env("XDG_RUNTIME_DIR", root) - .env_remove("TMPDIR") - .env_remove(GATE_ACTIVE_ENV) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("the child mode process starts") - } - - #[test] - fn a_third_lane_blocks_until_a_concurrent_lane_releases_its_slot() { - let _serial = exclusive(); - let scratch = Scratch::new("concurrency"); - let root = scratch.path().to_path_buf(); - let dir = gate_dir_under(&root); - - let mut holders: Vec = (0..SLOT_COUNT) - .map(|_| spawn_child_mode(HOLDER_TEST, &root, HOLD_ROOT_ENV)) - .collect(); - - let ready_count = || { - fs::read_dir(&root) - .map(|entries| { - entries - .filter_map(std::result::Result::ok) - .filter(|entry| entry.file_name().to_string_lossy().starts_with("ready-")) - .count() - }) - .unwrap_or(0) - }; - assert!( - wait_for(Duration::from_secs(30), || ready_count() >= SLOT_COUNT), - "both holder processes acquired a slot (saw {})", - ready_count() - ); - - // Both slots are held by other processes: a nonblocking attempt from - // this process must fail on every slot. - for index in 0..SLOT_COUNT { - assert!( - dir.try_acquire(index).unwrap().is_none(), - "slot {index} is held by a concurrent process" - ); - } - - // Release after a measurable delay and prove the waiter blocked for - // at least that long before it got in. - let release_path = root.join("release"); - let releaser = std::thread::spawn(move || { - sleep(Duration::from_millis(400)); - File::create(&release_path).unwrap(); - }); - - let started = Instant::now(); - let policy = AcquirePolicy { - retry_interval: Duration::from_millis(25), - timeout: Duration::from_secs(30), - }; - let guard = acquire_slot(&dir, policy, &mut SilentProgress) - .expect("the waiter acquires once a holder releases"); - let waited = started.elapsed(); - releaser.join().expect("the releaser thread finishes"); - - assert!( - waited >= Duration::from_millis(300), - "the waiter blocked until a slot was released (waited {waited:?})" - ); - drop(guard); - - for holder in &mut holders { - let status = holder.wait().expect("the holder process is reaped"); - assert!(status.success(), "holder exited cleanly: {status:?}"); - } - } - - #[test] - fn the_wrapper_hands_the_locked_descriptor_to_the_child_and_propagates_its_status() { - let _serial = exclusive(); - let scratch = Scratch::new("wrapper"); - let root = scratch.path().to_path_buf(); - let mut child = spawn_child_mode(WRAPPER_TEST, &root, WRAP_ROOT_ENV); - let status = child.wait().expect("the wrapper child is reaped"); - assert_eq!( - status.code(), - Some(7), - "the wrapper propagates the command's exit code and the command \ - saw a live D2B_HEAVY_GATE_SLOT_FD" - ); - } - - // ---- nesting is verified, not trusted ------------------------------ - - #[test] - fn system_parent_trust_matrix_rejects_every_unprivileged_owner_and_sticky_root() { - let us = 1000; - let peer = 1001; - // A uid-owned directory is renameable by that uid even at 0700. A peer - // owner is equally unsafe. Production admits neither. - assert!(!system_gate_parent_is_trusted(us, 0o700)); - assert!(!system_gate_parent_is_trusted(peer, 0o700)); - // Root ownership alone is insufficient: sticky 1777 still lets an - // entry's owner rename that entry. This is why /tmp is not a safe root. - assert!(!system_gate_parent_is_trusted(0, 0o1777)); - assert!(!system_gate_parent_is_trusted(0, 0o777)); - assert!(!system_gate_parent_is_trusted(0, 0o775)); - assert!(system_gate_parent_is_trusted(0, 0o755)); - assert!(system_gate_parent_is_trusted(0, 0o711)); - } - - #[test] - fn canonical_namespace_parent_is_empirically_not_writable_by_this_uid() { - if getuid().as_raw() == 0 { - return; - } - let run = open_directory(Path::new("/run"), "the canonical namespace parent") - .expect("/run is openable"); - verify_system_directory(&run, "the canonical namespace parent") - .expect("/run is root-owned and non-writable"); - let probe = format!(".d2b-heavy-gate-squat-probe-{}", std::process::id()); - let outcome = - rustix::fs::mkdirat(run.as_fd(), probe.as_str(), Mode::from_bits_truncate(0o700)); - if outcome.is_ok() { - let _ = rustix::fs::unlinkat(run.as_fd(), probe.as_str(), AtFlags::REMOVEDIR); - } - assert!( - matches!( - outcome, - Err(rustix::io::Errno::ACCESS | rustix::io::Errno::PERM | rustix::io::Errno::ROFS) - ), - "an unprivileged uid must not be able to squat a name under /run: {outcome:?}" - ); - } - - #[test] - fn operations_stay_anchored_across_a_rename_of_the_gate_directory() { - let _serial = exclusive(); - let scratch = Scratch::new("rename-anchor"); - let uid = getuid().as_raw(); - let dir = gate_dir_under(scratch.path()); - - // Move the prepared directory aside and drop a fresh decoy in its old - // place. Because every slot operation is anchored to the descriptor - // opened in `prepare_test`, acquisition must act on the moved inode, never - // the decoy at the original path. - let original = gate_dir_path(scratch.path(), uid); - let moved = scratch.path().join("uid-moved"); - fs::rename(&original, &moved).unwrap(); - let root_dir = open_directory(scratch.path(), gate_label::ROOT).unwrap(); - let uid_name = format!("uid-{uid}"); - let decoy = - open_or_create_directory_at(&root_dir, &uid_name, gate_label::SLOT_DIR, 0o700).unwrap(); - drop(decoy); - - let guard = dir - .try_acquire(0) - .unwrap() - .expect("acquisition still works through the anchored descriptor"); - assert!( - moved.join("slot-0").exists(), - "the slot file follows the anchored inode into its new path" - ); - assert!( - !original.join("slot-0").exists(), - "a pathname swap cannot redirect the semaphore to a decoy directory" - ); - drop(guard); - } - - #[test] - fn a_forged_or_stale_nesting_marker_is_never_treated_as_a_held_slot() { - let _serial = exclusive(); - let scratch = Scratch::new("forged-marker"); - let dir = gate_dir_under(scratch.path()); - - // An index past the real slot count is rejected outright. - assert_eq!( - dir.descriptor_is_locked_slot(SLOT_COUNT, 0).unwrap(), - SlotProof::NotHeld - ); - // A negative descriptor is rejected. - assert_eq!( - dir.descriptor_is_locked_slot(0, -1).unwrap(), - SlotProof::NotHeld - ); - // A descriptor number that is not open (nothing was opened at it) - // fails the `fstat` with EBADF, so a forged D2B_HEAVY_GATE_SLOT_FD is - // a legitimate "not held", not a malfunction. - assert_eq!( - dir.descriptor_is_locked_slot(0, 4096).unwrap(), - SlotProof::NotHeld - ); - } - - #[test] - fn a_closed_descriptor_marker_is_rejected() { - let _serial = exclusive(); - let scratch = Scratch::new("closed-fd"); - let dir = gate_dir_under(scratch.path()); - - // Open the real slot, capture its descriptor number, then close it. - // A marker naming a now-closed descriptor must not count as a slot. - let slot = dir.open_slot(0, false).unwrap().expect("slot opens"); - let fd = slot.as_raw_fd(); - drop(slot); - assert_eq!( - dir.descriptor_is_locked_slot(0, fd).unwrap(), - SlotProof::NotHeld, - "a closed descriptor fails fstat with EBADF and is not a held slot" - ); - } - - #[test] - fn an_uncontended_unlocked_descriptor_is_claimed_atomically() { - let _serial = exclusive(); - let scratch = Scratch::new("unlocked-slot"); - let dir = gate_dir_under(scratch.path()); - - // A live O_RDWR descriptor on the real slot inode with no other - // description holding the lock. The atomic proof claims the lock - // through this very descriptor, so verification accepts it and the - // slot is now genuinely held on this description - the nested run - // therefore occupies a real slot rather than running unsynchronised. - let slot = dir.open_slot(0, false).unwrap().expect("slot opens"); - assert_eq!( - dir.descriptor_is_locked_slot(0, slot.as_raw_fd()).unwrap(), - SlotProof::Held, - "an uncontended descriptor is accepted and the slot is claimed atomically" - ); - // Prove the lock is now genuinely held on that description: a separate - // acquisition attempt on the same slot must now see it busy. - assert!( - dir.try_acquire(0).unwrap().is_none(), - "the slot lock is held by the claimed descriptor, so a fresh acquisition is busy" - ); - drop(slot); - } - - #[test] - fn a_separate_open_is_rejected_while_another_description_holds_the_slot() { - let _serial = exclusive(); - let scratch = Scratch::new("separate-open"); - let dir = gate_dir_under(scratch.path()); - - // A genuine lane holds slot 0 through one open file description. - let guard = dir.try_acquire(0).unwrap().expect("slot 0 is free"); - - // A forged marker supplies an independent O_RDWR open on the very same - // slot inode - correct device and inode, current uid, regular file - - // but from a *separate* open file description that does not hold the - // lock. An earlier GETLK-on-a-fresh-handle form accepted this because - // *some* description (the guard's) held the slot; the atomic - // SETLK-through-the-inherited-descriptor form rejects it because the - // claim conflicts with the guard's lock. - let separate = dir.open_slot(0, false).unwrap().expect("slot opens"); - let separate_stat = fstat(separate.as_raw_fd()).expect("fstat succeeds"); - let reopened = dir.open_slot(0, false).unwrap().expect("slot reopens"); - let slot_stat = fstat(reopened.as_raw_fd()).expect("fstat succeeds"); - assert_eq!( - (separate_stat.st_dev, separate_stat.st_ino), - (slot_stat.st_dev, slot_stat.st_ino), - "the separate open really does name the canonical slot inode, so only the \ - lock-ownership check can reject it" - ); - assert_eq!( - dir.descriptor_is_locked_slot(0, separate.as_raw_fd()) - .unwrap(), - SlotProof::NotHeld, - "a descriptor from a separate open is rejected while another description holds \ - the slot, so a forged marker cannot smuggle in a third lane" - ); - drop(separate); - drop(guard); - } - - #[test] - fn a_marker_naming_a_genuinely_locked_slot_is_accepted() { - let _serial = exclusive(); - let scratch = Scratch::new("locked-slot"); - let dir = gate_dir_under(scratch.path()); - - // Hold the slot the way the wrapper does, then hand a duplicate to a - // notional child. The duplicate names the real, locked slot inode, so - // verification must accept it. - let guard = dir.try_acquire(0).unwrap().expect("slot 0 is free"); - let child_handle = guard.duplicate_for_child().expect("duplicate succeeds"); - assert_eq!( - dir.descriptor_is_locked_slot(0, child_handle.as_raw_fd()) - .unwrap(), - SlotProof::Held, - "a live descriptor on the genuinely locked slot is accepted" - ); - // A marker that points at the locked slot but names the *other* index - // must not verify, because its inode will not match. - assert_eq!( - dir.descriptor_is_locked_slot(1, child_handle.as_raw_fd()) - .unwrap(), - SlotProof::NotHeld, - "the descriptor must match the slot index it claims" - ); - drop(child_handle); - drop(guard); - } - - #[test] - fn a_verifier_malfunction_propagates_as_an_error_not_unheld() { - let _serial = exclusive(); - let scratch = Scratch::new("verifier-malfunction"); - let dir = gate_dir_under(scratch.path()); - - // A live, uid-owned regular-file descriptor stands in for the inherited - // slot marker so the check gets past the fstat/ownership screen and - // reaches the canonical-slot open. - let inherited = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .mode(0o600) - .open(scratch.path().join("inherited-marker")) - .expect("inherited marker opens"); - - // Replace the canonical slot-0 path with a *directory*. Opening it - // O_RDWR|O_NOFOLLOW then fails with EISDIR - not NotFound - which is a - // genuine verifier malfunction, not a legitimate "no slot is held". - fs::create_dir(dir.slot_path(0)).expect("slot path is replaceable with a directory"); - - let outcome = dir.descriptor_is_locked_slot(0, inherited.as_raw_fd()); - assert!( - outcome.is_err(), - "a slot file that exists but cannot be opened as a regular file must fail closed \ - with an error, never collapse into SlotProof::NotHeld (exit 3)" - ); - drop(inherited); - } - - // ---- verify-slot through the CLI dispatch (the shell-guard path) ---- - // - // These drive the real `heavy_gate::run` / `execute` CLI dispatch that the - // shell and Make guards reach through `xtask heavy-gate verify-slot`, so - // they prove the guard cannot be fooled by a bare `D2B_HEAVY_GATE` export. - // They run it in a re-exec of *this* test binary rather than the separately - // built `xtask` binary: the canonical per-uid namespace is non-overridable - // in a released binary by design, so only the crate's own test build - // honours the `XDG_RUNTIME_DIR` scratch-root seam. The exercised code path - // (`execute` -> `verify_slot` -> `classify_nesting` -> - // `descriptor_is_locked_slot`) is byte-for-byte the one the real binary - // runs. A slot descriptor is handed to the child exactly as the gate does - // it: an open handle with close-on-exec cleared so it survives `execve` at - // the same fd number the environment advertises. - - /// Clear close-on-exec so a handle is inherited by the child at the same - /// fd number, mirroring [`SlotGuard::duplicate_for_child`]. - fn clear_cloexec(file: &File) { - fcntl( - file.as_raw_fd(), - FcntlArg::F_SETFD(FdFlag::from_bits_truncate(0)), - ) - .expect("close-on-exec is clearable"); - } - - /// Child mode: run `verify-slot` through the real CLI dispatch against the - /// parent's scratch namespace. Selected by [`VERIFY_ROOT_ENV`]; a no-op in - /// ordinary runs. Exits with the exact code the `xtask heavy-gate - /// verify-slot` binary would, mapping a gate error to its kind's exit code - /// exactly as [`run`] does, so a verifier *malfunction* is distinguishable - /// from a plain "unheld". - #[test] - fn verify_slot_child_mode() { - if std::env::var_os(VERIFY_ROOT_ENV).is_none() { - return; - } - let code = match execute(&[VERIFY_SLOT_OP.to_string()]) { - Ok(code) => code, - Err(error) => error.kind().exit_code(), - }; - std::process::exit(i32::from(code)); - } - - /// Run `verify-slot` through a re-exec of this test binary with a - /// controlled environment and return its exit code. `slot` is - /// `Some((index, fd))` to advertise a slot marker, or `None` to export - /// only the bare, forgeable `D2B_HEAVY_GATE`. - fn run_verify_slot(root: &Path, marker: bool, slot: Option<(usize, RawFd)>) -> i32 { - let mut command = - Command::new(std::env::current_exe().expect("the test binary path is known")); - command - .args([VERIFY_TEST, "--exact", "--nocapture", "--test-threads=1"]) - .env(VERIFY_ROOT_ENV, root) - .env("XDG_RUNTIME_DIR", root) - .env_remove("TMPDIR") - .env_remove(GATE_ACTIVE_ENV) - .env_remove(SLOT_INDEX_ENV) - .env_remove(SLOT_FD_ENV) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - if marker { - command.env(GATE_ACTIVE_ENV, "1"); - } - if let Some((index, fd)) = slot { - command.env(SLOT_INDEX_ENV, index.to_string()); - command.env(SLOT_FD_ENV, fd.to_string()); - } - command - .status() - .expect("the verify-slot child runs") - .code() - .expect("verify-slot exits normally") - } - - #[test] - fn verify_slot_rejects_a_bare_marker_through_the_binary() { - let _serial = exclusive(); - let scratch = Scratch::new("verify-bare"); - - // Exactly the headline bypass: export the forgeable marker with no - // real slot descriptor. verify-slot must report no held slot so the - // shell guard acquires a real slot instead of running heavy work. - let code = run_verify_slot(scratch.path(), true, None); - assert_eq!( - code, VERIFY_SLOT_UNHELD as i32, - "a bare D2B_HEAVY_GATE export is not a held slot" - ); - } - - #[test] - fn verify_slot_rejects_a_forged_foreign_descriptor_through_the_binary() { - let _serial = exclusive(); - let scratch = Scratch::new("verify-forged"); - let _dir = gate_dir_under(scratch.path()); - - // A descriptor on a file the caller controls that is NOT the canonical - // slot inode. Correct uid, regular file, but the inode will not match - // slot-0, so verify-slot rejects it. - let bogus_path = scratch.path().join("not-a-slot"); - let bogus = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(&bogus_path) - .expect("the bogus file opens"); - clear_cloexec(&bogus); - - let code = run_verify_slot(scratch.path(), true, Some((0, bogus.as_raw_fd()))); - assert_eq!( - code, VERIFY_SLOT_UNHELD as i32, - "a descriptor on a foreign file is not a held slot" - ); - drop(bogus); - } - - #[test] - fn verify_slot_rejects_a_closed_descriptor_through_the_binary() { - let _serial = exclusive(); - let scratch = Scratch::new("verify-closed"); - let dir = gate_dir_under(scratch.path()); - - // Capture a real slot fd number, then close it. A marker naming a - // now-closed descriptor fails fstat in the child. - let slot = dir.open_slot(0, false).unwrap().expect("slot opens"); - let fd = slot.as_raw_fd(); - drop(slot); - - let code = run_verify_slot(scratch.path(), true, Some((0, fd))); - assert_eq!( - code, VERIFY_SLOT_UNHELD as i32, - "a closed descriptor is not a held slot" - ); - } - - #[test] - fn verify_slot_rejects_a_separate_open_while_another_lane_holds_it() { - let _serial = exclusive(); - let scratch = Scratch::new("verify-contended"); - let dir = gate_dir_under(scratch.path()); - - // A genuine lane holds slot 0 through one open file description. - let guard = dir.try_acquire(0).unwrap().expect("slot 0 is free"); - - // The caller presents a *separate* open on the same slot inode that - // does not hold the lock. The atomic ownership proof through that - // descriptor conflicts with the guard's lock, so verify-slot rejects - // it: a forged marker cannot smuggle in a third lane. - let separate = dir.open_slot(0, false).unwrap().expect("slot opens"); - clear_cloexec(&separate); - - let code = run_verify_slot(scratch.path(), true, Some((0, separate.as_raw_fd()))); - assert_eq!( - code, VERIFY_SLOT_UNHELD as i32, - "a separate open is rejected while another description holds the slot" - ); - drop(separate); - drop(guard); - } - - #[test] - fn verify_slot_accepts_a_genuinely_held_slot_through_the_binary() { - let _serial = exclusive(); - let scratch = Scratch::new("verify-held"); - let dir = gate_dir_under(scratch.path()); - - // Hold the slot the way the wrapper does, hand the child an inherited - // duplicate on the *same* locked description. The atomic proof through - // that descriptor is idempotent, so verify-slot confirms a held slot. - let guard = dir.try_acquire(0).unwrap().expect("slot 0 is free"); - let child_handle = guard.duplicate_for_child().expect("duplicate succeeds"); - - let code = run_verify_slot(scratch.path(), true, Some((0, child_handle.as_raw_fd()))); - assert_eq!( - code, VERIFY_SLOT_HELD as i32, - "a descriptor on the genuinely locked slot is a held slot" - ); - drop(child_handle); - drop(guard); - } - - // ---- supervision drains signals around the leader's exit ----------- - - #[test] - fn supervise_loop_sweeps_the_group_when_a_signal_races_the_leader_exit() { - use std::cell::Cell; - - let poll_calls = Cell::new(0u32); - let drain_calls = Cell::new(0u32); - let forwarded = Cell::new(0u32); - let sweeps = Cell::new(0u32); - let reap_after_sweep = Cell::new(false); - - let outcome = supervise_loop( - || { - let n = poll_calls.get(); - poll_calls.set(n + 1); - if n == 0 { - Ok(None) - } else { - Ok(Some(ChildExit { - code: Some(0), - signal: None, - })) - } - }, - || { - let n = drain_calls.get(); - drain_calls.set(n + 1); - // The terminating signal only becomes visible on the - // post-exit drain: exactly the race that previously orphaned - // a slot holder. - if n == 2 { - vec![Signal::SIGTERM] - } else { - Vec::new() - } - }, - |_signal| forwarded.set(forwarded.get() + 1), - || sweeps.set(sweeps.get() + 1), - || reap_after_sweep.set(sweeps.get() == 1), - || {}, - Instant::now, - ) - .expect("the loop completes"); - - assert!( - reap_after_sweep.get(), - "the leader is reaped only after the group is swept, so its zombie pins the pgid" - ); - assert_eq!( - outcome.interrupt, - Some(Signal::SIGTERM), - "a signal pending at leader exit is still recorded as an interruption" - ); - assert_eq!(outcome.code, Some(0)); - assert_eq!( - forwarded.get(), - 0, - "a signal observed only after exit is not forwarded to a dead group" - ); - assert_eq!( - sweeps.get(), - 1, - "the group is swept once so no descendant keeps holding the slot" - ); - assert_eq!(poll_calls.get(), 2); - assert_eq!( - drain_calls.get(), - 3, - "drained before each poll and once after exit" - ); - } - - #[test] - fn supervise_loop_forwards_and_sweeps_a_signal_seen_while_running() { - use std::cell::Cell; - - let poll_calls = Cell::new(0u32); - let drain_calls = Cell::new(0u32); - let forwarded = Cell::new(0u32); - let sweeps = Cell::new(0u32); - - let outcome = supervise_loop( - || { - let n = poll_calls.get(); - poll_calls.set(n + 1); - if n == 0 { - Ok(None) - } else { - Ok(Some(ChildExit { - code: Some(0), - signal: None, - })) - } - }, - || { - let n = drain_calls.get(); - drain_calls.set(n + 1); - if n == 0 { - vec![Signal::SIGINT] - } else { - Vec::new() - } - }, - |_signal| forwarded.set(forwarded.get() + 1), - || sweeps.set(sweeps.get() + 1), - || {}, - || {}, - Instant::now, - ) - .expect("the loop completes"); - - assert_eq!(outcome.interrupt, Some(Signal::SIGINT)); - assert_eq!( - forwarded.get(), - 1, - "a signal seen while the lane runs is forwarded to its group" - ); - assert_eq!( - sweeps.get(), - 1, - "the interrupted run sweeps the group exactly once" - ); - } - - #[test] - fn supervise_loop_sweeps_the_group_even_after_a_clean_exit() { - use std::cell::Cell; - - let sweeps = Cell::new(0u32); - let reaps = Cell::new(0u32); - let reap_after_sweep = Cell::new(false); - let outcome = supervise_loop( - || { - Ok(Some(ChildExit { - code: Some(3), - signal: None, - })) - }, - Vec::new, - |_signal| panic!("no signal should be forwarded on a clean exit"), - || sweeps.set(sweeps.get() + 1), - || { - reaps.set(reaps.get() + 1); - reap_after_sweep.set(sweeps.get() == 1); - }, - || {}, - Instant::now, - ) - .expect("the loop completes"); - - assert_eq!(outcome.interrupt, None); - assert_eq!(outcome.code, Some(3)); - assert_eq!(reaps.get(), 1, "the leader is reaped exactly once"); - assert!( - reap_after_sweep.get(), - "the leader is reaped after the sweep, so the zombie pins its pid and pgid across \ - teardown and the pgid cannot be recycled onto an unrelated group" - ); - assert_eq!( - sweeps.get(), - 1, - "the group is swept once even on a clean exit, so a pending signal that kills the \ - wrapper after the mask is restored cannot leave a slot-holding descendant behind" - ); - } - - /// Extract the recipe body (the tab-indented command lines) for a Makefile - /// rule so the inventory guard can assert what it routes through. Returns - /// `None` when no such rule exists. - fn makefile_recipe(makefile: &str, target: &str) -> Option { - let mut in_recipe = false; - let mut recipe = String::new(); - for line in makefile.lines() { - if !in_recipe { - if line.starts_with('\t') { - continue; - } - if let Some((lhs, _)) = line.split_once(':') - && lhs.split_whitespace().any(|t| t == target) - { - in_recipe = true; - } - continue; - } - if let Some(command) = line.strip_prefix('\t') { - recipe.push_str(command); - recipe.push('\n'); - } else { - break; - } - } - if in_recipe { Some(recipe) } else { None } - } - - /// True when `line` opens a Makefile rule (`target[s]: [prereqs]`) rather - /// than a variable assignment (`X := ...`, `X = ...`, `X ?= ...`, - /// `X += ...`), a recipe body line, a comment, or a blank line. - fn makefile_rule_parts(line: &str) -> Option<(&str, &str)> { - if line.starts_with('\t') || line.starts_with('#') || line.trim().is_empty() { - return None; - } - let (lhs, rhs) = line.split_once(':')?; - // `:=` immediate assignment: the ':' we split on is the assignment - // operator, so the right side starts with '='. - if rhs.starts_with('=') { - return None; - } - // `=`, `?=`, `+=` assignments keep the operator on the left of any ':'. - if lhs.contains('=') { - return None; - } - Some((lhs, rhs)) - } - - /// Every rule target name declared in the Makefile. - fn makefile_targets(makefile: &str) -> Vec { - let mut targets = Vec::new(); - for line in makefile.lines() { - if let Some((lhs, _)) = makefile_rule_parts(line) { - for tok in lhs.split_whitespace() { - targets.push(tok.to_string()); - } - } - } - targets - } - - /// The prerequisite tokens declared for `target`, or `None` when the - /// Makefile has no rule for it. - fn makefile_prereqs(makefile: &str, target: &str) -> Option> { - for line in makefile.lines() { - if let Some((lhs, rhs)) = makefile_rule_parts(line) - && lhs.split_whitespace().any(|t| t == target) - { - return Some(rhs.split_whitespace().map(str::to_string).collect()); - } - } - None - } - - /// True when a shell line is a `source`/`.` directive whose target basename - /// is `name` (for example `. "$HERE/lib.sh"` or `source ./lib.sh`). - fn line_sources_basename(line: &str, name: &str) -> bool { - let trimmed = line.trim_start(); - let Some(rest) = trimmed - .strip_prefix(". ") - .or_else(|| trimmed.strip_prefix("source ")) - else { - return false; - }; - let Some(tok) = rest.split_whitespace().next() else { - return false; - }; - let tok = tok.trim_matches(|c| c == '"' || c == '\''); - Path::new(tok).file_name().and_then(|n| n.to_str()) == Some(name) - } - - /// Filesystem properties shared by the entrypoint collector and the - /// sourced-library classifier. Keeping this one classifier prevents the - /// two callers from drifting on what "regular", "shell", and "executable" - /// mean. - #[derive(Clone, Copy)] - struct EntrypointProperties { - is_shell: bool, - is_executable: bool, - } - - fn entrypoint_properties(path: &Path, meta: &fs::Metadata) -> Option { - meta.file_type().is_file().then_some(EntrypointProperties { - is_shell: path.extension() == Some(OsStr::new("sh")), - is_executable: meta.permissions().mode() & 0o111 != 0, - }) - } - - /// Whether `candidate` (a non-executable `.sh`) is a genuine shell - /// *library*: an executable regular `.sh` entrypoint in the same directory - /// pulls it in with a `source`/`.` directive. An inert text fixture, data - /// file, directory, symlink, or non-executable shell file is not evidence - /// that the candidate runs only behind a guarded entrypoint. - /// - /// Matching a same-directory entrypoint - the `. "$HERE/lib.sh"` shape - /// every d2b lane uses - makes this a per-file classification rather than - /// the old `basename == "lib.sh"` skip. Both this scan and - /// [`heavy_entrypoint`] use [`entrypoint_properties`], so their filesystem - /// definition cannot drift apart. - fn is_sibling_sourced_library(candidate: &Path) -> bool { - let Some(dir) = candidate.parent() else { - return false; - }; - let Some(name) = candidate.file_name().and_then(|n| n.to_str()) else { - return false; - }; - let Ok(entries) = fs::read_dir(dir) else { - return false; - }; - for entry in entries { - let sibling = entry.expect("a readable dir entry").path(); - if sibling == candidate { - continue; - } - let Ok(meta) = scan_metadata(&sibling) else { - continue; - }; - let Some(properties) = entrypoint_properties(&sibling, &meta) else { - continue; - }; - if !properties.is_shell || !properties.is_executable { - continue; - } - let Ok(text) = fs::read_to_string(&sibling) else { - continue; - }; - if text.lines().any(|line| line_sources_basename(line, name)) { - return true; - } - } - false - } - - /// Classify a single regular file. Returns `Some(path)` when it is a heavy - /// *entrypoint* (an executable regular file, or a non-executable `.sh` that - /// no sibling sources), and `None` when it is inert data or a - /// sibling-sourced shell library. - /// - /// This is the explicit per-file entrypoint-versus-library rule that - /// replaces the old `basename == "lib.sh"` heuristic. Executability is the - /// primary signal - an executable file is runnable as `./file` and is - /// always an entrypoint, even if named `lib.sh` - and a non-executable - /// `.sh` is a library only when a sibling actually sources it (otherwise it - /// is still runnable as `bash file`, so it must be gated). A file that is - /// neither `.sh` nor executable (a `.nix`, `.md`, `.txt`, ...) is inert - /// data. - fn heavy_entrypoint(path: &Path, meta: &fs::Metadata) -> Option { - let properties = entrypoint_properties(path, meta)?; - if !properties.is_shell && !properties.is_executable { - return None; - } - if !properties.is_executable && properties.is_shell && is_sibling_sourced_library(path) { - return None; - } - Some(path.to_path_buf()) - } - - /// Markers of genuinely heavy work: build, container, VM, sudo, or device - /// activity. A lane cannot be exempted from the gate (declared - /// OUT_OF_SCOPE) while any file in it performs one of these; the exemption - /// must be justified by this *checked property*, not by a free-text - /// comment. This is what makes the census closed - an exemption the census - /// cannot verify is refused - and it is exactly the gap that let the - /// genuinely-heavy distro-matrix lane sit exempt behind a comment. - const HEAVY_WORK_MARKERS: &[(&str, &str)] = &[ - ("cargo build", "build"), - ("cargo test", "build"), - ("cargo run", "build"), - ("cargo clippy", "build"), - ("nix build", "build"), - ("nixos-rebuild", "build"), - ("podman", "container"), - ("docker", "container"), - ("buildah", "container"), - ("nerdctl", "container"), - ("cloud-hypervisor", "VM"), - ("qemu", "VM"), - ("virtiofsd", "VM"), - ("swtpm", "VM"), - ("runNixOSTest", "VM"), - ("d2b vm start", "VM"), - ("sudo ", "sudo"), - ("doas ", "sudo"), - ("/dev/kvm", "device"), - ("/dev/dri", "device"), - ("/dev/vfio", "device"), - ("/dev/nvidia", "device"), - ("modprobe", "device"), - ("usbip", "device"), - ]; - - /// Scan every regular file under `dir` for a [`HEAVY_WORK_MARKERS`] token, - /// returning the first offending `(file, kind)`. Used to verify that a lane - /// claimed OUT_OF_SCOPE really performs no heavy work, so the census refuses - /// an exemption it cannot check. - fn lane_heavy_work_marker(dir: &Path) -> Option<(PathBuf, &'static str)> { - let mut stack = vec![dir.to_path_buf()]; - let mut hits: Vec<(PathBuf, &'static str)> = Vec::new(); - while let Some(current) = stack.pop() { - let Ok(entries) = fs::read_dir(¤t) else { - continue; - }; - for entry in entries { - let path = entry.expect("a readable dir entry").path(); - let meta = scan_metadata(&path) - .unwrap_or_else(|e| panic!("cannot stat {}: {e}", path.display())); - if meta.file_type().is_dir() { - stack.push(path); - continue; - } - if !meta.file_type().is_file() { - continue; - } - let Ok(text) = fs::read_to_string(&path) else { - continue; - }; - if let Some((_, kind)) = HEAVY_WORK_MARKERS - .iter() - .find(|(marker, _)| text.contains(marker)) - { - hits.push((path, kind)); - } - } - } - hits.sort(); - hits.into_iter().next() - } - - /// Recursively collect every heavy-entrypoint candidate under `dir`. A - /// sourced shell library is not an entrypoint - it is loaded by an - /// entrypoint that already holds a slot - so it is excluded via - /// [`heavy_entrypoint`]'s explicit classification and never required to - /// self-guard. Returns an empty vec when `dir` is absent (optional lanes). - fn collect_heavy_entrypoints(dir: &Path) -> Vec { - let mut out = Vec::new(); - let Ok(entries) = fs::read_dir(dir) else { - return out; - }; - for entry in entries { - let path = entry.expect("a readable dir entry").path(); - let meta = scan_metadata(&path) - .unwrap_or_else(|e| panic!("cannot stat {}: {e}", path.display())); - if meta.file_type().is_dir() { - out.extend(collect_heavy_entrypoints(&path)); - continue; - } - if let Some(entrypoint) = heavy_entrypoint(&path, &meta) { - out.push(entrypoint); - } - } - out.sort(); - out - } - - /// Inventory guard for the sole-use invariant: every live, hardware, - /// benchmark, cloud, container, and performance entrypoint must route - /// through the heavy-gate semaphore, so a future lane cannot be added that - /// silently bypasses it. This is a CLOSED-WORLD guard - it walks the - /// on-disk entrypoints recursively, censuses every lane directory against - /// an explicit classification, and parses the Makefile for every heavy - /// lane rather than checking a hand-maintained list. Adding a new heavy - /// entrypoint (a nested or non-`.sh` script, a new aggregating runner, a - /// new lane directory, a new `heavy-lane-*` make target, or a new public - /// delegation) fails this test until it is gated. - #[test] - fn every_live_and_heavy_entrypoint_routes_through_the_gate() { - let root = crate::repo_root().expect("packages/xtask resolves to a repo root"); - - // The shared self-guard the shell entrypoints call. Its presence proves - // the script asks the wrapper to verify a genuinely-held slot rather - // than trusting the mere presence of the D2B_HEAVY_GATE variable. - let self_guard_token = "d2b_heavy_gate_reexec"; - - // Closed-world lane classification. Trusting a hand-maintained - // directory list is exactly why the type-9 container lane went missing - // for four rounds, so every lane directory on disk must be explicitly - // classified. GATED lanes route through the heavy-gate and every - // runnable entrypoint they contain must self-guard. OUT_OF_SCOPE lanes - // must additionally prove they hold no slot by a *checked property* - - // no file in them performs build, container, VM, sudo, or device work - // (see HEAVY_WORK_MARKERS / lane_heavy_work_marker) - rather than by a - // free-text comment, which is what let the genuinely-heavy distro-matrix - // lane (sudo + /dev/kvm + cargo build --release --workspace) sit exempt. - // GATED names may sit under different parents (benchmark lives directly - // under tests/), so the walked set is spelled out and the census - // cross-checks that every GATED directory on disk is actually walked. - const GATED_LANE_DIRS: &[&str] = &[ - "live", - "containers", - "cloud", - "hardware", - "benchmark", - "distro-matrix", - ]; - // Every OUT_OF_SCOPE entry is verified against HEAVY_WORK_MARKERS below, - // so an entry that performs heavy work is refused regardless of the - // reason string. Empty today: the previously-exempt distro-matrix lane - // is now GATED. - const OUT_OF_SCOPE_LANE_DIRS: &[(&str, &str)] = &[]; - let walked_heavy_dirs = [ - "tests/integration/live", - "tests/integration/containers", - "tests/integration/cloud", - "tests/host-integration/hardware", - "tests/benchmark", - "tests/integration/distro-matrix", - ]; - - // Census: every entry under the lane parents must be classified. - // Subdirectories are classified as GATED (walked; every entrypoint must - // self-guard) or OUT_OF_SCOPE (verified free of heavy work). Regular - // files *directly* under a lane parent are classified too - an earlier - // form only descended into subdirectories, so a heavy `.sh` dropped - // straight into tests/integration/ escaped the census entirely. A loose - // entrypoint is folded into the guarded set; loose data and - // sibling-sourced libraries contribute nothing. - let mut loose_entrypoints: Vec = Vec::new(); - for parent_rel in ["tests/integration", "tests/host-integration"] { - let parent = root.join(parent_rel); - let Ok(entries) = fs::read_dir(&parent) else { - continue; - }; - for entry in entries { - let path = entry.expect("a readable dir entry").path(); - let meta = scan_metadata(&path) - .unwrap_or_else(|e| panic!("cannot stat {}: {e}", path.display())); - if !meta.file_type().is_dir() { - // A file directly under the lane parent. Classify it with - // the same entrypoint-vs-library rule the lane walk uses; a - // heavy entrypoint here must still self-guard. - if let Some(entrypoint) = heavy_entrypoint(&path, &meta) { - loose_entrypoints.push(entrypoint); - } - continue; - } - let name = path - .file_name() - .and_then(|n| n.to_str()) - .expect("a utf-8 lane directory name") - .to_string(); - let gated = GATED_LANE_DIRS.contains(&name.as_str()); - let out_of_scope = OUT_OF_SCOPE_LANE_DIRS.iter().any(|(n, _)| *n == name); - assert!( - gated ^ out_of_scope, - "lane directory {parent_rel}/{name} is not classified exactly once: classify \ - it as GATED (add it to GATED_LANE_DIRS and to the walked heavy set so its \ - entrypoints self-guard) or OUT_OF_SCOPE (add it to OUT_OF_SCOPE_LANE_DIRS \ - and ensure it performs no build/container/VM/sudo/device work). A new heavy \ - lane must not appear silently unguarded." - ); - if gated { - let walked = walked_heavy_dirs.iter().any(|w| root.join(w) == path); - assert!( - walked, - "GATED lane directory {parent_rel}/{name} is classified but not in the \ - walked heavy set; add it so its entrypoints are required to self-guard" - ); - } - if out_of_scope { - // The exemption is only honoured if it is *checked*: a lane - // declared out of scope must actually perform no heavy work. - // A comment string is not enough - that is precisely how the - // heavy distro-matrix lane stayed exempt for a round. - if let Some((file, kind)) = lane_heavy_work_marker(&path) { - panic!( - "OUT_OF_SCOPE lane {parent_rel}/{name} is not actually out of scope: \ - {} performs {kind} work. An exemption must be justified by a checked \ - property, not a comment; gate the lane (move it to GATED_LANE_DIRS \ - and the walked set) instead of exempting it.", - file.display() - ); - } - } - } - } - - // 1. Filesystem entrypoints. Walk every GATED heavy-lane directory - // recursively and require an executable self-guard on each script. - // performance-budgets.sh lives outside those directories, so it is - // named explicitly. The fixture-contract lane is eval-only and no - // longer belongs to the heavy inventory. Optional directories (benchmark, cloud) are walked - // when present and simply contribute nothing when absent. - let mut entrypoints: Vec = Vec::new(); - for dir in walked_heavy_dirs { - entrypoints.extend(collect_heavy_entrypoints(&root.join(dir))); - } - // Loose entrypoints discovered directly under the lane parents (none - // today) must be gated exactly like lane entrypoints. - entrypoints.extend(loose_entrypoints); - let perf = root.join("tests/unit/gates/performance-budgets.sh"); - assert!( - perf.is_file(), - "expected the performance-budgets entrypoint at {}", - perf.display() - ); - entrypoints.push(perf.clone()); - entrypoints.sort(); - entrypoints.dedup(); - - let live_count = entrypoints - .iter() - .filter(|p| p.starts_with(root.join("tests/integration/live"))) - .count(); - assert!( - live_count >= 7, - "expected the known live-lane scripts to be discovered; found {live_count}" - ); - - // The type-9 container lane must contribute at least its one runnable - // entrypoint; its sourced lib.sh must NOT (it is not an entrypoint). - let container_count = entrypoints - .iter() - .filter(|p| p.starts_with(root.join("tests/integration/containers"))) - .count(); - assert!( - container_count >= 1, - "expected the type-9 container lane entrypoints to be discovered and gated; found \ - {container_count}" - ); - - // The distro-matrix Tier-1 lane is genuinely heavy (sudo + /dev/kvm + - // cargo build --release --workspace). It was exempt by comment for a - // round; it must now be discovered and gated like any other lane. - let distro_count = entrypoints - .iter() - .filter(|p| p.starts_with(root.join("tests/integration/distro-matrix"))) - .count(); - assert!( - distro_count >= 1, - "expected the distro-matrix Tier-1 entrypoint to be discovered and gated; found \ - {distro_count}" - ); - assert!( - !entrypoints - .iter() - .any(|p| p.file_name() == Some(OsStr::new("lib.sh"))), - "a sourced lib.sh was collected as an entrypoint; sourced libraries must not be \ - required to hold a slot" - ); - - for script in &entrypoints { - let meta = scan_metadata(script) - .unwrap_or_else(|e| panic!("cannot stat {}: {e}", script.display())); - assert!( - meta.permissions().mode() & 0o111 != 0, - "heavy entrypoint {} is not executable; a non-executable entrypoint invites a \ - caller to run it with a bare `bash