Integrate core record modularization
This commit is contained in:
commit
c1b40f923a
15 changed files with 5674 additions and 5591 deletions
File diff suppressed because it is too large
Load diff
201
crates/lazaret-core/src/records/configuration.rs
Normal file
201
crates/lazaret-core/src/records/configuration.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
use super::{BTreeMap, Deserialize, HostPath, Serialize};
|
||||
|
||||
pub const DOMAIN_STORE_SCHEMA_ID: &str = "lazaret-domain-store-v2";
|
||||
pub const LEGACY_DOMAIN_STORE_SCHEMA_ID: &str = "lazaret-domain-store-v1";
|
||||
pub const STORAGE_CATALOG_SCHEMA_ID: &str = "lazaret-storage-catalog-v1";
|
||||
pub const DOMAIN_STORE_SCHEMA_VERSION: u32 = 2;
|
||||
pub const LEGACY_DOMAIN_STORE_SCHEMA_VERSION: u32 = 1;
|
||||
pub const STORAGE_CATALOG_SCHEMA_VERSION: u32 = 1;
|
||||
pub const TRUSTED_HOST_CONFIGURATION_SCHEMA_ID: &str = "lazaret-trusted-host-config-v1";
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StorageConfigurationV1 {
|
||||
#[serde(default)]
|
||||
pub data_root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NetworkConfigurationV1 {
|
||||
#[serde(default)]
|
||||
pub default_allow: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub default_deny: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum VolumeKind {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum VolumeAccess {
|
||||
ReadOnly,
|
||||
#[default]
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct VolumeConfigurationV1 {
|
||||
pub host: String,
|
||||
pub container: String,
|
||||
pub kind: VolumeKind,
|
||||
#[serde(default)]
|
||||
pub access: VolumeAccess,
|
||||
#[serde(default)]
|
||||
pub create: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AutoApprovalRuleConfigurationV1 {
|
||||
pub executable: String,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_extra_args: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EscalationConfigurationV1 {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub auto_approve: Vec<AutoApprovalRuleConfigurationV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ProfileConfigurationV1 {
|
||||
pub image: String,
|
||||
#[serde(default = "default_isolate_gitignored")]
|
||||
pub isolate_gitignored: bool,
|
||||
#[serde(default)]
|
||||
pub persistent_dirs: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_egress: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub deny_egress: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub env: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub volumes: Vec<VolumeConfigurationV1>,
|
||||
#[serde(default)]
|
||||
pub escalation: EscalationConfigurationV1,
|
||||
}
|
||||
|
||||
const fn default_isolate_gitignored() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct TrustedHostConfigurationV1 {
|
||||
#[serde(rename = "schema")]
|
||||
schema_id: String,
|
||||
storage: StorageConfigurationV1,
|
||||
network: NetworkConfigurationV1,
|
||||
profiles: BTreeMap<String, ProfileConfigurationV1>,
|
||||
}
|
||||
|
||||
impl TrustedHostConfigurationV1 {
|
||||
#[must_use]
|
||||
pub fn schema_id(&self) -> &str {
|
||||
&self.schema_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn storage(&self) -> &StorageConfigurationV1 {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn network(&self) -> &NetworkConfigurationV1 {
|
||||
&self.network
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn profiles(&self) -> &BTreeMap<String, ProfileConfigurationV1> {
|
||||
&self.profiles
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TrustedHostConfigurationV1 {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Fields {
|
||||
#[serde(rename = "schema")]
|
||||
schema_id: String,
|
||||
#[serde(default)]
|
||||
storage: StorageConfigurationV1,
|
||||
#[serde(default)]
|
||||
network: NetworkConfigurationV1,
|
||||
profiles: BTreeMap<String, ProfileConfigurationV1>,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
if fields.schema_id != TRUSTED_HOST_CONFIGURATION_SCHEMA_ID {
|
||||
return Err(serde::de::Error::custom(
|
||||
"unknown trusted-host configuration schema",
|
||||
));
|
||||
}
|
||||
if fields.profiles.is_empty() {
|
||||
return Err(serde::de::Error::custom(
|
||||
"trusted-host configuration has no profiles",
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
schema_id: fields.schema_id,
|
||||
storage: fields.storage,
|
||||
network: fields.network,
|
||||
profiles: fields.profiles,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct EnvironmentReference {
|
||||
pub sandbox_name: String,
|
||||
pub host_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct ResolvedVolume {
|
||||
pub canonical_host_source: HostPath,
|
||||
pub container_path: String,
|
||||
pub kind: VolumeKind,
|
||||
pub access: VolumeAccess,
|
||||
pub create: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct ResolvedAutoApprovalRule {
|
||||
pub canonical_executable: HostPath,
|
||||
pub args: Vec<String>,
|
||||
pub allow_extra_args: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct ConfigurationFingerprintRecordV1 {
|
||||
pub selected_profile: String,
|
||||
pub storage_root: HostPath,
|
||||
pub runtime_root: HostPath,
|
||||
pub image: String,
|
||||
pub isolate_gitignored: bool,
|
||||
pub persistent_dirs: Vec<String>,
|
||||
pub environment_references: Vec<EnvironmentReference>,
|
||||
pub volumes: Vec<ResolvedVolume>,
|
||||
pub effective_allow_prefixes: Vec<([u8; 4], u8)>,
|
||||
pub effective_deny_prefixes: Vec<([u8; 4], u8)>,
|
||||
pub escalation_enabled: bool,
|
||||
pub auto_approval_rules: Vec<ResolvedAutoApprovalRule>,
|
||||
}
|
||||
632
crates/lazaret-core/src/records/credentials.rs
Normal file
632
crates/lazaret-core/src/records/credentials.rs
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
use super::{
|
||||
CapabilityId, CertificateDigest, ClientId, ConfigurationFingerprint, CoordinatorId,
|
||||
Deserialize, DomainId, GenerationId, ProtocolCredentialId, RequestDigest, RequestId, SandboxId,
|
||||
Serialize, SessionId, StreamDigest, TrustManifestDigest, fmt,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum ProtocolVersion {
|
||||
V1,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RecordInvariantError {
|
||||
CredentialBundleRoles,
|
||||
ParticipantSnapshotState,
|
||||
SessionTerminalSnapshotState,
|
||||
}
|
||||
|
||||
impl fmt::Display for RecordInvariantError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::CredentialBundleRoles => {
|
||||
formatter.write_str("credential bundle roles are invalid")
|
||||
}
|
||||
Self::ParticipantSnapshotState => {
|
||||
formatter.write_str("participant ownership snapshot is invalid for its role/state")
|
||||
}
|
||||
Self::SessionTerminalSnapshotState => {
|
||||
formatter.write_str("Session Terminal evidence is invalid for its state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RecordInvariantError {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RecordTransitionError {
|
||||
RecordIdentityChanged,
|
||||
StateRegression,
|
||||
EvidenceChanged,
|
||||
EvidenceLost,
|
||||
ProcessStageSkipped,
|
||||
PriorAcquisitionSnapshotRequired,
|
||||
AbsenceProofRequired,
|
||||
InvalidCleanTarget,
|
||||
}
|
||||
|
||||
impl fmt::Display for RecordTransitionError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::RecordIdentityChanged => formatter.write_str("record identity changed"),
|
||||
Self::StateRegression => formatter.write_str("lifecycle transition is invalid"),
|
||||
Self::EvidenceChanged => formatter.write_str("recorded evidence changed"),
|
||||
Self::EvidenceLost => formatter.write_str("recorded evidence was cleared"),
|
||||
Self::ProcessStageSkipped => {
|
||||
formatter.write_str("process ownership did not follow the staged barrier")
|
||||
}
|
||||
Self::PriorAcquisitionSnapshotRequired => {
|
||||
formatter.write_str("active state requires a prior acquisition snapshot")
|
||||
}
|
||||
Self::AbsenceProofRequired => formatter.write_str("cleanup requires absence proof"),
|
||||
Self::InvalidCleanTarget => formatter.write_str("Clean target is not fully cleared"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RecordTransitionError {}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct GenerationIdentity {
|
||||
pub agent_profile: String,
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
pub configuration_fingerprint: ConfigurationFingerprint,
|
||||
pub coordinator_id: CoordinatorId,
|
||||
pub sandbox_id: SandboxId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TrustManifest {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub generation_identity: GenerationIdentity,
|
||||
pub tls_ca_sha256: CertificateDigest,
|
||||
pub protocol_issuer_public_key: Vec<u8>,
|
||||
pub issuer_signature: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum TlsRole {
|
||||
SessionCoordinator,
|
||||
ClientCli,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum TlsSubjectId {
|
||||
SessionCoordinator(CoordinatorId),
|
||||
ClientCli(ClientId),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct TlsCredential {
|
||||
pub generation_identity: GenerationIdentity,
|
||||
role: TlsRole,
|
||||
subject_id: TlsSubjectId,
|
||||
pub certificate_der: Vec<u8>,
|
||||
pub certificate_sha256: CertificateDigest,
|
||||
}
|
||||
|
||||
impl TlsCredential {
|
||||
#[must_use]
|
||||
pub fn session_coordinator(
|
||||
generation_identity: GenerationIdentity,
|
||||
subject_id: CoordinatorId,
|
||||
certificate_der: Vec<u8>,
|
||||
certificate_sha256: CertificateDigest,
|
||||
) -> Self {
|
||||
Self {
|
||||
generation_identity,
|
||||
role: TlsRole::SessionCoordinator,
|
||||
subject_id: TlsSubjectId::SessionCoordinator(subject_id),
|
||||
certificate_der,
|
||||
certificate_sha256,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn client_cli(
|
||||
generation_identity: GenerationIdentity,
|
||||
subject_id: ClientId,
|
||||
certificate_der: Vec<u8>,
|
||||
certificate_sha256: CertificateDigest,
|
||||
) -> Self {
|
||||
Self {
|
||||
generation_identity,
|
||||
role: TlsRole::ClientCli,
|
||||
subject_id: TlsSubjectId::ClientCli(subject_id),
|
||||
certificate_der,
|
||||
certificate_sha256,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn role(&self) -> TlsRole {
|
||||
self.role
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn subject_id(&self) -> TlsSubjectId {
|
||||
self.subject_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TlsCredential {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
generation_identity: GenerationIdentity,
|
||||
role: TlsRole,
|
||||
subject_id: TlsSubjectId,
|
||||
certificate_der: Vec<u8>,
|
||||
certificate_sha256: CertificateDigest,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
match (fields.role, fields.subject_id) {
|
||||
(TlsRole::SessionCoordinator, TlsSubjectId::SessionCoordinator(subject_id)) => {
|
||||
Ok(Self::session_coordinator(
|
||||
fields.generation_identity,
|
||||
subject_id,
|
||||
fields.certificate_der,
|
||||
fields.certificate_sha256,
|
||||
))
|
||||
}
|
||||
(TlsRole::ClientCli, TlsSubjectId::ClientCli(subject_id)) => Ok(Self::client_cli(
|
||||
fields.generation_identity,
|
||||
subject_id,
|
||||
fields.certificate_der,
|
||||
fields.certificate_sha256,
|
||||
)),
|
||||
_ => Err(serde::de::Error::custom(
|
||||
"TLS role does not match its subject identity",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct ProtocolPublicationIdentity {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub generation_identity: GenerationIdentity,
|
||||
pub tls_ca_certificate_der: Vec<u8>,
|
||||
pub trust_manifest_sha256: TrustManifestDigest,
|
||||
pub coordinator_tls_certificate_sha256: CertificateDigest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum ProtocolCredentialRole {
|
||||
SessionCoordinator,
|
||||
AgentSandbox,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum ProtocolSubjectId {
|
||||
SessionCoordinator(CoordinatorId),
|
||||
AgentSandbox(SandboxId),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct ProtocolCredential {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub credential_id: ProtocolCredentialId,
|
||||
pub generation_identity: GenerationIdentity,
|
||||
role: ProtocolCredentialRole,
|
||||
subject_id: ProtocolSubjectId,
|
||||
pub public_key: Vec<u8>,
|
||||
pub issuer_signature: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ProtocolCredential {
|
||||
#[must_use]
|
||||
pub fn session_coordinator(
|
||||
credential_id: ProtocolCredentialId,
|
||||
generation_identity: GenerationIdentity,
|
||||
subject_id: CoordinatorId,
|
||||
public_key: Vec<u8>,
|
||||
issuer_signature: Vec<u8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
credential_id,
|
||||
generation_identity,
|
||||
role: ProtocolCredentialRole::SessionCoordinator,
|
||||
subject_id: ProtocolSubjectId::SessionCoordinator(subject_id),
|
||||
public_key,
|
||||
issuer_signature,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn agent_sandbox(
|
||||
credential_id: ProtocolCredentialId,
|
||||
generation_identity: GenerationIdentity,
|
||||
subject_id: SandboxId,
|
||||
public_key: Vec<u8>,
|
||||
issuer_signature: Vec<u8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
credential_id,
|
||||
generation_identity,
|
||||
role: ProtocolCredentialRole::AgentSandbox,
|
||||
subject_id: ProtocolSubjectId::AgentSandbox(subject_id),
|
||||
public_key,
|
||||
issuer_signature,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn role(&self) -> ProtocolCredentialRole {
|
||||
self.role
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn subject_id(&self) -> ProtocolSubjectId {
|
||||
self.subject_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ProtocolCredential {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
protocol_version: ProtocolVersion,
|
||||
credential_id: ProtocolCredentialId,
|
||||
generation_identity: GenerationIdentity,
|
||||
role: ProtocolCredentialRole,
|
||||
subject_id: ProtocolSubjectId,
|
||||
public_key: Vec<u8>,
|
||||
issuer_signature: Vec<u8>,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
match fields.protocol_version {
|
||||
ProtocolVersion::V1 => {}
|
||||
}
|
||||
match (fields.role, fields.subject_id) {
|
||||
(
|
||||
ProtocolCredentialRole::SessionCoordinator,
|
||||
ProtocolSubjectId::SessionCoordinator(subject_id),
|
||||
) => Ok(Self::session_coordinator(
|
||||
fields.credential_id,
|
||||
fields.generation_identity,
|
||||
subject_id,
|
||||
fields.public_key,
|
||||
fields.issuer_signature,
|
||||
)),
|
||||
(ProtocolCredentialRole::AgentSandbox, ProtocolSubjectId::AgentSandbox(subject_id)) => {
|
||||
Ok(Self::agent_sandbox(
|
||||
fields.credential_id,
|
||||
fields.generation_identity,
|
||||
subject_id,
|
||||
fields.public_key,
|
||||
fields.issuer_signature,
|
||||
))
|
||||
}
|
||||
_ => Err(serde::de::Error::custom(
|
||||
"protocol credential role does not match its subject identity",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct SandboxCredentialBundle {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub generation_identity: GenerationIdentity,
|
||||
pub trust_manifest: TrustManifest,
|
||||
coordinator_protocol_credential: ProtocolCredential,
|
||||
sandbox_protocol_credential: ProtocolCredential,
|
||||
pub sandbox_private_key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SandboxCredentialBundle {
|
||||
/// Constructs a bundle only when its two credentials have their required roles.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when either credential has the wrong role.
|
||||
pub fn new(
|
||||
generation_identity: GenerationIdentity,
|
||||
trust_manifest: TrustManifest,
|
||||
coordinator_protocol_credential: ProtocolCredential,
|
||||
sandbox_protocol_credential: ProtocolCredential,
|
||||
sandbox_private_key: Vec<u8>,
|
||||
) -> Result<Self, RecordInvariantError> {
|
||||
if coordinator_protocol_credential.role() != ProtocolCredentialRole::SessionCoordinator
|
||||
|| sandbox_protocol_credential.role() != ProtocolCredentialRole::AgentSandbox
|
||||
{
|
||||
return Err(RecordInvariantError::CredentialBundleRoles);
|
||||
}
|
||||
Ok(Self {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
generation_identity,
|
||||
trust_manifest,
|
||||
coordinator_protocol_credential,
|
||||
sandbox_protocol_credential,
|
||||
sandbox_private_key,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn coordinator_protocol_credential(&self) -> &ProtocolCredential {
|
||||
&self.coordinator_protocol_credential
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn sandbox_protocol_credential(&self) -> &ProtocolCredential {
|
||||
&self.sandbox_protocol_credential
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SandboxCredentialBundle {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
protocol_version: ProtocolVersion,
|
||||
generation_identity: GenerationIdentity,
|
||||
trust_manifest: TrustManifest,
|
||||
coordinator_protocol_credential: ProtocolCredential,
|
||||
sandbox_protocol_credential: ProtocolCredential,
|
||||
sandbox_private_key: Vec<u8>,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
match fields.protocol_version {
|
||||
ProtocolVersion::V1 => {}
|
||||
}
|
||||
Self::new(
|
||||
fields.generation_identity,
|
||||
fields.trust_manifest,
|
||||
fields.coordinator_protocol_credential,
|
||||
fields.sandbox_protocol_credential,
|
||||
fields.sandbox_private_key,
|
||||
)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct SessionBinding {
|
||||
pub generation_identity: GenerationIdentity,
|
||||
pub cli_id: ClientId,
|
||||
pub session_id: SessionId,
|
||||
pub session_anchor_public_key: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct SessionRequestCapability {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub capability_id: CapabilityId,
|
||||
pub session_binding: SessionBinding,
|
||||
pub coordinator_credential_id: ProtocolCredentialId,
|
||||
pub sandbox_credential_id: ProtocolCredentialId,
|
||||
pub coordinator_signature: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct SignedProtocolRecord {
|
||||
pub payload_bytes: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct MailboxRequest {
|
||||
protocol_version: ProtocolVersion,
|
||||
session_binding: SessionBinding,
|
||||
request_id: RequestId,
|
||||
session_capability: SessionRequestCapability,
|
||||
sandbox_credential_id: ProtocolCredentialId,
|
||||
command_argv: Vec<Vec<u8>>,
|
||||
justification: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MailboxRequestError {
|
||||
EmptyCommand,
|
||||
InvalidArgument,
|
||||
NonAbsoluteExecutable,
|
||||
}
|
||||
|
||||
impl fmt::Display for MailboxRequestError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::EmptyCommand => formatter.write_str("command argument vector is empty"),
|
||||
Self::InvalidArgument => formatter.write_str("command argument is not NUL-free UTF-8"),
|
||||
Self::NonAbsoluteExecutable => {
|
||||
formatter.write_str("command executable is not an absolute path")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MailboxRequestError {}
|
||||
|
||||
impl MailboxRequest {
|
||||
/// # Errors
|
||||
/// The command must contain an absolute executable followed by NUL-free UTF-8 arguments.
|
||||
pub fn new(
|
||||
session_binding: SessionBinding,
|
||||
request_id: RequestId,
|
||||
session_capability: SessionRequestCapability,
|
||||
sandbox_credential_id: ProtocolCredentialId,
|
||||
command_argv: Vec<Vec<u8>>,
|
||||
justification: String,
|
||||
) -> Result<Self, MailboxRequestError> {
|
||||
let Some(executable) = command_argv.first() else {
|
||||
return Err(MailboxRequestError::EmptyCommand);
|
||||
};
|
||||
if command_argv
|
||||
.iter()
|
||||
.any(|argument| argument.contains(&0) || std::str::from_utf8(argument).is_err())
|
||||
{
|
||||
return Err(MailboxRequestError::InvalidArgument);
|
||||
}
|
||||
if !executable.starts_with(b"/") {
|
||||
return Err(MailboxRequestError::NonAbsoluteExecutable);
|
||||
}
|
||||
Ok(Self {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding,
|
||||
request_id,
|
||||
session_capability,
|
||||
sandbox_credential_id,
|
||||
command_argv,
|
||||
justification,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn protocol_version(&self) -> ProtocolVersion {
|
||||
self.protocol_version
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn session_binding(&self) -> &SessionBinding {
|
||||
&self.session_binding
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn request_id(&self) -> RequestId {
|
||||
self.request_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn session_capability(&self) -> &SessionRequestCapability {
|
||||
&self.session_capability
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn sandbox_credential_id(&self) -> ProtocolCredentialId {
|
||||
self.sandbox_credential_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn command_argv(&self) -> &[Vec<u8>] {
|
||||
&self.command_argv
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn justification(&self) -> &str {
|
||||
&self.justification
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MailboxRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
protocol_version: ProtocolVersion,
|
||||
session_binding: SessionBinding,
|
||||
request_id: RequestId,
|
||||
session_capability: SessionRequestCapability,
|
||||
sandbox_credential_id: ProtocolCredentialId,
|
||||
command_argv: Vec<Vec<u8>>,
|
||||
justification: String,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
if fields.protocol_version != ProtocolVersion::V1 {
|
||||
return Err(serde::de::Error::custom("unknown mailbox request version"));
|
||||
}
|
||||
Self::new(
|
||||
fields.session_binding,
|
||||
fields.request_id,
|
||||
fields.session_capability,
|
||||
fields.sandbox_credential_id,
|
||||
fields.command_argv,
|
||||
fields.justification,
|
||||
)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct MailboxCancellation {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub session_binding: SessionBinding,
|
||||
pub request_id: RequestId,
|
||||
pub session_capability: SessionRequestCapability,
|
||||
pub sandbox_credential_id: ProtocolCredentialId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct MailboxResultAck {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub session_binding: SessionBinding,
|
||||
pub request_id: RequestId,
|
||||
pub request_sha256: RequestDigest,
|
||||
pub session_capability: SessionRequestCapability,
|
||||
pub sandbox_credential_id: ProtocolCredentialId,
|
||||
pub highest_contiguous_sequence: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum DeveloperDecisionKind {
|
||||
Approve,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct DeveloperDecision {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub session_binding: SessionBinding,
|
||||
pub request_id: RequestId,
|
||||
pub request_sha256: RequestDigest,
|
||||
pub decision: DeveloperDecisionKind,
|
||||
pub approved_command_argv: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum EscalationOutcome {
|
||||
Completed,
|
||||
Denied,
|
||||
TimedOut,
|
||||
Cancelled,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct MailboxResult {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub session_binding: SessionBinding,
|
||||
pub request_id: RequestId,
|
||||
pub request_sha256: RequestDigest,
|
||||
pub coordinator_credential_id: ProtocolCredentialId,
|
||||
pub outcome: EscalationOutcome,
|
||||
pub exit_code: Option<i32>,
|
||||
pub chunk_count: u64,
|
||||
pub stdout_sha256: StreamDigest,
|
||||
pub stderr_sha256: StreamDigest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum ResultStream {
|
||||
Stdout,
|
||||
Stderr,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct MailboxResultChunk {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub session_binding: SessionBinding,
|
||||
pub request_id: RequestId,
|
||||
pub request_sha256: RequestDigest,
|
||||
pub coordinator_credential_id: ProtocolCredentialId,
|
||||
pub sequence: u64,
|
||||
pub stream: ResultStream,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
1535
crates/lazaret-core/src/records/lifecycle.rs
Normal file
1535
crates/lazaret-core/src/records/lifecycle.rs
Normal file
File diff suppressed because it is too large
Load diff
373
crates/lazaret-core/src/records/network.rs
Normal file
373
crates/lazaret-core/src/records/network.rs
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
use super::{
|
||||
ConfigurationFingerprint, DerivedPolicyDigest, Deserialize, DomainId, GenerationId,
|
||||
NamespaceIdentity, NetworkSidecarId, PidfsProcessIdentityV1, ReadinessFingerprint, Serialize,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum AddressFamily {
|
||||
Ipv4,
|
||||
Ipv6,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum AddressFamilyDispositionValue {
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct AddressFamilyDisposition {
|
||||
pub family: AddressFamily,
|
||||
pub disposition: AddressFamilyDispositionValue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub enum IpAddress {
|
||||
V4([u8; 4]),
|
||||
V6([u8; 16]),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct IpNetwork {
|
||||
pub address: IpAddress,
|
||||
pub prefix_length: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum PolicyAction {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct DefaultAction {
|
||||
pub family: AddressFamily,
|
||||
pub action: PolicyAction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct PrefixRule {
|
||||
pub family: AddressFamily,
|
||||
pub network: IpNetwork,
|
||||
pub action: PolicyAction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct Resolver {
|
||||
pub family: AddressFamily,
|
||||
pub address: IpAddress,
|
||||
}
|
||||
|
||||
pub const DERIVED_NETWORK_POLICY_SCHEMA_ID: &str = "lazaret-derived-network-policy-v1";
|
||||
pub const NETWORK_READINESS_ATTESTATION_SCHEMA_ID: &str = "lazaret-network-attestation-v1";
|
||||
pub const NETWORK_READINESS_IDENTITY_SCHEMA_ID: &str = "lazaret-network-readiness-identity-v1";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DerivedNetworkPolicyFields {
|
||||
pub address_families: Vec<AddressFamilyDisposition>,
|
||||
pub default_actions: Vec<DefaultAction>,
|
||||
pub prefix_rules: Vec<PrefixRule>,
|
||||
pub resolvers: Vec<Resolver>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct DerivedNetworkPolicy {
|
||||
schema_id: String,
|
||||
pub address_families: Vec<AddressFamilyDisposition>,
|
||||
pub default_actions: Vec<DefaultAction>,
|
||||
pub prefix_rules: Vec<PrefixRule>,
|
||||
pub resolvers: Vec<Resolver>,
|
||||
}
|
||||
|
||||
impl DerivedNetworkPolicy {
|
||||
#[must_use]
|
||||
pub fn v1(fields: DerivedNetworkPolicyFields) -> Self {
|
||||
Self {
|
||||
schema_id: DERIVED_NETWORK_POLICY_SCHEMA_ID.into(),
|
||||
address_families: fields.address_families,
|
||||
default_actions: fields.default_actions,
|
||||
prefix_rules: fields.prefix_rules,
|
||||
resolvers: fields.resolvers,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn schema_id(&self) -> &str {
|
||||
&self.schema_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DerivedNetworkPolicy {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
schema_id: String,
|
||||
address_families: Vec<AddressFamilyDisposition>,
|
||||
default_actions: Vec<DefaultAction>,
|
||||
prefix_rules: Vec<PrefixRule>,
|
||||
resolvers: Vec<Resolver>,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
if fields.schema_id != DERIVED_NETWORK_POLICY_SCHEMA_ID {
|
||||
return Err(serde::de::Error::custom(
|
||||
"unknown derived-network-policy schema identity",
|
||||
));
|
||||
}
|
||||
Ok(Self::v1(DerivedNetworkPolicyFields {
|
||||
address_families: fields.address_families,
|
||||
default_actions: fields.default_actions,
|
||||
prefix_rules: fields.prefix_rules,
|
||||
resolvers: fields.resolvers,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum PolicyDelivery {
|
||||
SealedMemfd,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct NetworkObservation {
|
||||
pub policy_delivery: PolicyDelivery,
|
||||
pub helper_ready: bool,
|
||||
pub observed_routes: Vec<IpNetwork>,
|
||||
pub routes_attested: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct NetworkReadinessAttestationFields {
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
pub configuration_fingerprint: ConfigurationFingerprint,
|
||||
pub network_sidecar_id: NetworkSidecarId,
|
||||
pub derived_policy_digest: DerivedPolicyDigest,
|
||||
pub enforcement_namespace: NamespaceIdentity,
|
||||
pub runtime_supervisor: PidfsProcessIdentityV1,
|
||||
pub connectivity_helper: PidfsProcessIdentityV1,
|
||||
pub observation: NetworkObservation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct NetworkReadinessAttestation {
|
||||
schema_id: String,
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
pub configuration_fingerprint: ConfigurationFingerprint,
|
||||
pub network_sidecar_id: NetworkSidecarId,
|
||||
pub derived_policy_digest: DerivedPolicyDigest,
|
||||
pub enforcement_namespace: NamespaceIdentity,
|
||||
pub runtime_supervisor: PidfsProcessIdentityV1,
|
||||
pub connectivity_helper: PidfsProcessIdentityV1,
|
||||
pub observation: NetworkObservation,
|
||||
}
|
||||
|
||||
impl NetworkReadinessAttestation {
|
||||
#[must_use]
|
||||
pub fn v1(fields: NetworkReadinessAttestationFields) -> Self {
|
||||
Self {
|
||||
schema_id: NETWORK_READINESS_ATTESTATION_SCHEMA_ID.into(),
|
||||
domain_id: fields.domain_id,
|
||||
generation_id: fields.generation_id,
|
||||
configuration_fingerprint: fields.configuration_fingerprint,
|
||||
network_sidecar_id: fields.network_sidecar_id,
|
||||
derived_policy_digest: fields.derived_policy_digest,
|
||||
enforcement_namespace: fields.enforcement_namespace,
|
||||
runtime_supervisor: fields.runtime_supervisor,
|
||||
connectivity_helper: fields.connectivity_helper,
|
||||
observation: fields.observation,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn schema_id(&self) -> &str {
|
||||
&self.schema_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NetworkReadinessAttestation {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
schema_id: String,
|
||||
domain_id: DomainId,
|
||||
generation_id: GenerationId,
|
||||
configuration_fingerprint: ConfigurationFingerprint,
|
||||
network_sidecar_id: NetworkSidecarId,
|
||||
derived_policy_digest: DerivedPolicyDigest,
|
||||
enforcement_namespace: NamespaceIdentity,
|
||||
runtime_supervisor: PidfsProcessIdentityV1,
|
||||
connectivity_helper: PidfsProcessIdentityV1,
|
||||
observation: NetworkObservation,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
if fields.schema_id != NETWORK_READINESS_ATTESTATION_SCHEMA_ID {
|
||||
return Err(serde::de::Error::custom(
|
||||
"unknown network-readiness-attestation schema identity",
|
||||
));
|
||||
}
|
||||
Ok(Self::v1(NetworkReadinessAttestationFields {
|
||||
domain_id: fields.domain_id,
|
||||
generation_id: fields.generation_id,
|
||||
configuration_fingerprint: fields.configuration_fingerprint,
|
||||
network_sidecar_id: fields.network_sidecar_id,
|
||||
derived_policy_digest: fields.derived_policy_digest,
|
||||
enforcement_namespace: fields.enforcement_namespace,
|
||||
runtime_supervisor: fields.runtime_supervisor,
|
||||
connectivity_helper: fields.connectivity_helper,
|
||||
observation: fields.observation,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct NetworkReadinessIdentityFields {
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
pub configuration_fingerprint: ConfigurationFingerprint,
|
||||
pub network_sidecar_id: NetworkSidecarId,
|
||||
pub derived_policy_digest: DerivedPolicyDigest,
|
||||
pub enforcement_namespace: NamespaceIdentity,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct NetworkReadinessIdentity {
|
||||
schema_id: String,
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
pub configuration_fingerprint: ConfigurationFingerprint,
|
||||
pub network_sidecar_id: NetworkSidecarId,
|
||||
pub derived_policy_digest: DerivedPolicyDigest,
|
||||
pub enforcement_namespace: NamespaceIdentity,
|
||||
}
|
||||
|
||||
impl NetworkReadinessIdentity {
|
||||
#[must_use]
|
||||
pub fn v1(fields: NetworkReadinessIdentityFields) -> Self {
|
||||
Self {
|
||||
schema_id: NETWORK_READINESS_IDENTITY_SCHEMA_ID.into(),
|
||||
domain_id: fields.domain_id,
|
||||
generation_id: fields.generation_id,
|
||||
configuration_fingerprint: fields.configuration_fingerprint,
|
||||
network_sidecar_id: fields.network_sidecar_id,
|
||||
derived_policy_digest: fields.derived_policy_digest,
|
||||
enforcement_namespace: fields.enforcement_namespace,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn schema_id(&self) -> &str {
|
||||
&self.schema_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NetworkReadinessIdentity {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
schema_id: String,
|
||||
domain_id: DomainId,
|
||||
generation_id: GenerationId,
|
||||
configuration_fingerprint: ConfigurationFingerprint,
|
||||
network_sidecar_id: NetworkSidecarId,
|
||||
derived_policy_digest: DerivedPolicyDigest,
|
||||
enforcement_namespace: NamespaceIdentity,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
if fields.schema_id != NETWORK_READINESS_IDENTITY_SCHEMA_ID {
|
||||
return Err(serde::de::Error::custom(
|
||||
"unknown network-readiness-identity schema identity",
|
||||
));
|
||||
}
|
||||
Ok(Self::v1(NetworkReadinessIdentityFields {
|
||||
domain_id: fields.domain_id,
|
||||
generation_id: fields.generation_id,
|
||||
configuration_fingerprint: fields.configuration_fingerprint,
|
||||
network_sidecar_id: fields.network_sidecar_id,
|
||||
derived_policy_digest: fields.derived_policy_digest,
|
||||
enforcement_namespace: fields.enforcement_namespace,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct DeniedDestinationObservation {
|
||||
pub destination: IpAddress,
|
||||
pub unreachable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct PermittedDestinationObservation {
|
||||
pub destination: IpAddress,
|
||||
pub reachable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct DnsObservation {
|
||||
pub resolver: IpAddress,
|
||||
pub query_name: String,
|
||||
pub resolved_addresses: Vec<IpAddress>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct NetworkReachabilityProbe {
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
pub enforcement_namespace: NamespaceIdentity,
|
||||
pub denied_destinations: Vec<DeniedDestinationObservation>,
|
||||
pub permitted_destinations: Vec<PermittedDestinationObservation>,
|
||||
pub dns_observations: Vec<DnsObservation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct NetworkReadinessEvidence {
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
pub network_sidecar_id: NetworkSidecarId,
|
||||
pub readiness_fingerprint: ReadinessFingerprint,
|
||||
pub attestation: NetworkReadinessAttestation,
|
||||
pub readiness_identity: NetworkReadinessIdentity,
|
||||
pub reachability_probe: NetworkReachabilityProbe,
|
||||
}
|
||||
|
||||
impl NetworkReadinessEvidence {
|
||||
pub(super) fn matches(
|
||||
&self,
|
||||
generation_id: GenerationId,
|
||||
derived_policy_digest: DerivedPolicyDigest,
|
||||
readiness_fingerprint: ReadinessFingerprint,
|
||||
namespace: NamespaceIdentity,
|
||||
runtime_supervisor: &PidfsProcessIdentityV1,
|
||||
) -> bool {
|
||||
self.generation_id == generation_id
|
||||
&& self.readiness_fingerprint == readiness_fingerprint
|
||||
&& self.domain_id == self.attestation.domain_id
|
||||
&& self.domain_id == self.readiness_identity.domain_id
|
||||
&& self.domain_id == self.reachability_probe.domain_id
|
||||
&& self.generation_id == self.attestation.generation_id
|
||||
&& self.generation_id == self.readiness_identity.generation_id
|
||||
&& self.generation_id == self.reachability_probe.generation_id
|
||||
&& self.network_sidecar_id == self.attestation.network_sidecar_id
|
||||
&& self.network_sidecar_id == self.readiness_identity.network_sidecar_id
|
||||
&& self.attestation.configuration_fingerprint
|
||||
== self.readiness_identity.configuration_fingerprint
|
||||
&& self.attestation.derived_policy_digest == derived_policy_digest
|
||||
&& self.readiness_identity.derived_policy_digest == derived_policy_digest
|
||||
&& self.attestation.enforcement_namespace == namespace
|
||||
&& self.readiness_identity.enforcement_namespace == namespace
|
||||
&& self.reachability_probe.enforcement_namespace == namespace
|
||||
&& self.attestation.runtime_supervisor == *runtime_supervisor
|
||||
}
|
||||
}
|
||||
138
crates/lazaret-core/src/records/registry.rs
Normal file
138
crates/lazaret-core/src/records/registry.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use super::{
|
||||
AddressFamily, AddressFamilyDisposition, ArtifactLease, ArtifactState,
|
||||
ConfigurationFingerprintRecordV1, DefaultAction, DeniedDestinationObservation,
|
||||
DerivedNetworkPolicy, DeveloperDecision, DeveloperDecisionKind, DnsObservation, Domain,
|
||||
DomainStore, EnvironmentReference, EscalationOutcome, Generation, GenerationIdentity,
|
||||
GitRoutingOwnership, GuestProcessIdentity, HelperOwnership, HostProcessIdentity,
|
||||
HostProcessOwnershipSlot, LegacyUnverifiedProcessIdentityV1, Mailbox, MailboxCancellation,
|
||||
MailboxRequest, MailboxResult, MailboxResultAck, MailboxResultChunk, MountIdentity,
|
||||
NamespaceIdentity, NetworkObservation, NetworkOwnership, NetworkReachabilityProbe,
|
||||
NetworkReadinessAttestation, NetworkReadinessEvidence, NetworkReadinessIdentity, Participant,
|
||||
ParticipantOwnership, ParticipantState, PermittedDestinationObservation,
|
||||
PersistedHostProcessOwnership, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyAction,
|
||||
PolicyDelivery, PrefixRule, PrepareOperation, PreparedArtifact, PreparedLowerOwnership,
|
||||
ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion, ProtocolCredential,
|
||||
ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity, PublicationOwnership,
|
||||
ResolvedAutoApprovalRule, ResolvedVolume, Resolver, ResultStream, SandboxCredentialBundle,
|
||||
SandboxOwnership, Session, SessionBinding, SessionClose, SessionExit, SessionExitOutcome,
|
||||
SessionRequestCapability, SessionTerminal, SessionTerminalEvent, SessionTerminalMessage,
|
||||
SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership, StateAnchorOwnership,
|
||||
StorageCatalog, StoreMetadata, TerminalInput, TerminalOutput, TerminalResize, TerminalSignal,
|
||||
TerminalSignalKind, TlsCredential, TlsRole, TrustManifest, TrustedHostConfigurationV1,
|
||||
};
|
||||
|
||||
pub trait SchemaType {
|
||||
const NAME: &'static str;
|
||||
}
|
||||
|
||||
macro_rules! schema_types {
|
||||
($($name:literal => $record:ty),+; $($declared:literal),* $(,)?) => {
|
||||
$(
|
||||
impl SchemaType for $record {
|
||||
const NAME: &'static str = $name;
|
||||
}
|
||||
)+
|
||||
|
||||
pub const IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES: &[&str] = &[$($name),+];
|
||||
pub const DECLARED_SERIALIZED_SCHEMA_TYPE_NAMES: &[&str] = &[
|
||||
$($name),+,
|
||||
$($declared),*
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
schema_types!(
|
||||
"StoreMetadata" => StoreMetadata,
|
||||
"Domain" => Domain,
|
||||
"Generation" => Generation,
|
||||
"Participant" => Participant,
|
||||
"ParticipantState" => ParticipantState,
|
||||
"ParticipantOwnership" => ParticipantOwnership,
|
||||
"Session" => Session,
|
||||
"SessionTerminal" => SessionTerminal,
|
||||
"SessionTerminalState" => SessionTerminalState,
|
||||
"Mailbox" => Mailbox,
|
||||
"PtyIdentity" => PtyIdentity,
|
||||
"PidfsProcessAuthorityV1" => PidfsProcessAuthorityV1,
|
||||
"PidfsProcessIdentityV1" => PidfsProcessIdentityV1,
|
||||
"LegacyUnverifiedProcessIdentityV1" => LegacyUnverifiedProcessIdentityV1,
|
||||
"HostProcessIdentity" => HostProcessIdentity,
|
||||
"PersistedHostProcessOwnership" => PersistedHostProcessOwnership,
|
||||
"HostProcessOwnershipSlot" => HostProcessOwnershipSlot,
|
||||
"ProcessOwnershipMessageVersion" => ProcessOwnershipMessageVersion,
|
||||
"StagedProcessOwnership" => StagedProcessOwnership,
|
||||
"ProcessOwnershipCommitAck" => ProcessOwnershipCommitAck,
|
||||
"GuestProcessIdentity" => GuestProcessIdentity,
|
||||
"SessionTerminalEvent" => SessionTerminalEvent,
|
||||
"TerminalInput" => TerminalInput,
|
||||
"TerminalResize" => TerminalResize,
|
||||
"TerminalSignal" => TerminalSignal,
|
||||
"TerminalSignalKind" => TerminalSignalKind,
|
||||
"SessionClose" => SessionClose,
|
||||
"SessionTerminalMessage" => SessionTerminalMessage,
|
||||
"TerminalOutput" => TerminalOutput,
|
||||
"SessionExit" => SessionExit,
|
||||
"SessionExitOutcome" => SessionExitOutcome,
|
||||
"GenerationIdentity" => GenerationIdentity,
|
||||
"TrustManifest" => TrustManifest,
|
||||
"TlsCredential" => TlsCredential,
|
||||
"ProtocolPublicationIdentity" => ProtocolPublicationIdentity,
|
||||
"SandboxCredentialBundle" => SandboxCredentialBundle,
|
||||
"TlsRole" => TlsRole,
|
||||
"ProtocolCredential" => ProtocolCredential,
|
||||
"ProtocolCredentialRole" => ProtocolCredentialRole,
|
||||
"SessionBinding" => SessionBinding,
|
||||
"SessionRequestCapability" => SessionRequestCapability,
|
||||
"SignedProtocolRecord" => SignedProtocolRecord,
|
||||
"MailboxRequest" => MailboxRequest,
|
||||
"MailboxCancellation" => MailboxCancellation,
|
||||
"MailboxResultAck" => MailboxResultAck,
|
||||
"DeveloperDecision" => DeveloperDecision,
|
||||
"DeveloperDecisionKind" => DeveloperDecisionKind,
|
||||
"MailboxResult" => MailboxResult,
|
||||
"MailboxResultChunk" => MailboxResultChunk,
|
||||
"ResultStream" => ResultStream,
|
||||
"EscalationOutcome" => EscalationOutcome,
|
||||
"NamespaceIdentity" => NamespaceIdentity,
|
||||
"MountIdentity" => MountIdentity,
|
||||
"PreparedLowerOwnership" => PreparedLowerOwnership,
|
||||
"StateAnchorOwnership" => StateAnchorOwnership,
|
||||
"GitRoutingOwnership" => GitRoutingOwnership,
|
||||
"NetworkOwnership" => NetworkOwnership,
|
||||
"HelperOwnership" => HelperOwnership,
|
||||
"SandboxOwnership" => SandboxOwnership,
|
||||
"PublicationOwnership" => PublicationOwnership,
|
||||
"PreparedArtifact" => PreparedArtifact,
|
||||
"ArtifactState" => ArtifactState,
|
||||
"ArtifactLease" => ArtifactLease,
|
||||
"PrepareOperation" => PrepareOperation,
|
||||
"AddressFamilyDisposition" => AddressFamilyDisposition,
|
||||
"DefaultAction" => DefaultAction,
|
||||
"PrefixRule" => PrefixRule,
|
||||
"Resolver" => Resolver,
|
||||
"AddressFamily" => AddressFamily,
|
||||
"PolicyAction" => PolicyAction,
|
||||
"DerivedNetworkPolicy" => DerivedNetworkPolicy,
|
||||
"NetworkReadinessEvidence" => NetworkReadinessEvidence,
|
||||
"NetworkReadinessAttestation" => NetworkReadinessAttestation,
|
||||
"NetworkObservation" => NetworkObservation,
|
||||
"PolicyDelivery" => PolicyDelivery,
|
||||
"NetworkReadinessIdentity" => NetworkReadinessIdentity,
|
||||
"NetworkReachabilityProbe" => NetworkReachabilityProbe,
|
||||
"DeniedDestinationObservation" => DeniedDestinationObservation,
|
||||
"PermittedDestinationObservation" => PermittedDestinationObservation,
|
||||
"DnsObservation" => DnsObservation,
|
||||
"TrustedHostConfigurationV1" => TrustedHostConfigurationV1,
|
||||
"ConfigurationFingerprintRecordV1" => ConfigurationFingerprintRecordV1,
|
||||
"EnvironmentReference" => EnvironmentReference,
|
||||
"ResolvedVolume" => ResolvedVolume,
|
||||
"ResolvedAutoApprovalRule" => ResolvedAutoApprovalRule,
|
||||
"DomainStore" => DomainStore,
|
||||
"StorageCatalog" => StorageCatalog;
|
||||
);
|
||||
|
||||
pub const DECLARED_RUNTIME_SCHEMA_TYPE_NAMES: &[&str] = &[
|
||||
"ResolvedDomainConfiguration",
|
||||
"SandboxLayoutV1",
|
||||
"ConfinedPathPlan",
|
||||
];
|
||||
49
crates/lazaret-core/src/records/storage.rs
Normal file
49
crates/lazaret-core/src/records/storage.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use super::{
|
||||
Deserialize, DeviceId, DomainId, GenerationId, HostPath, IdMapDigest, InodeId, PolicyId,
|
||||
PrepareOperationId, PreparedArtifactId, SelectedManifestDigest, Serialize, SourceIdentity,
|
||||
StorageCatalogMetadata,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum ArtifactState {
|
||||
Available,
|
||||
Reclaiming,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct PreparedArtifact {
|
||||
pub prepared_artifact_id: PreparedArtifactId,
|
||||
pub source_identity: SourceIdentity,
|
||||
pub selected_manifest_digest: SelectedManifestDigest,
|
||||
pub policy_id: PolicyId,
|
||||
pub id_map_digest: IdMapDigest,
|
||||
pub path: HostPath,
|
||||
pub device: DeviceId,
|
||||
pub inode: InodeId,
|
||||
pub state: ArtifactState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct ArtifactLease {
|
||||
pub prepared_artifact_id: PreparedArtifactId,
|
||||
pub domain_id: DomainId,
|
||||
pub generation_id: GenerationId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct PrepareOperation {
|
||||
pub operation_id: PrepareOperationId,
|
||||
pub expected_artifact_id: PreparedArtifactId,
|
||||
pub source_identity: SourceIdentity,
|
||||
pub stage_path: HostPath,
|
||||
pub device: DeviceId,
|
||||
pub inode: InodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct StorageCatalog {
|
||||
pub metadata: StorageCatalogMetadata,
|
||||
pub prepared_artifacts: Vec<PreparedArtifact>,
|
||||
pub artifact_leases: Vec<ArtifactLease>,
|
||||
pub prepare_operations: Vec<PrepareOperation>,
|
||||
}
|
||||
138
crates/lazaret-core/src/records/terminal.rs
Normal file
138
crates/lazaret-core/src/records/terminal.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use super::{Deserialize, NonZeroU16, Serialize, SessionId};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalInput {
|
||||
pub session_id: SessionId,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalResize {
|
||||
pub session_id: SessionId,
|
||||
pub rows: NonZeroU16,
|
||||
pub columns: NonZeroU16,
|
||||
pub pixel_width: u16,
|
||||
pub pixel_height: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum TerminalSignalKind {
|
||||
Interrupt,
|
||||
Quit,
|
||||
Suspend,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalSignal {
|
||||
pub session_id: SessionId,
|
||||
pub signal: TerminalSignalKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct SessionClose {
|
||||
pub session_id: SessionId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum SessionTerminalEvent {
|
||||
Input(TerminalInput),
|
||||
Resize(TerminalResize),
|
||||
Signal(TerminalSignal),
|
||||
Close(SessionClose),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalOutput {
|
||||
pub session_id: SessionId,
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum SessionExitOutcome {
|
||||
Exited,
|
||||
Terminated,
|
||||
RuntimeLost,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct SessionExit {
|
||||
session_id: SessionId,
|
||||
outcome: SessionExitOutcome,
|
||||
exit_code: Option<i32>,
|
||||
}
|
||||
|
||||
impl SessionExit {
|
||||
#[must_use]
|
||||
pub const fn exited(session_id: SessionId, exit_code: i32) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
outcome: SessionExitOutcome::Exited,
|
||||
exit_code: Some(exit_code),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn terminated(session_id: SessionId) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
outcome: SessionExitOutcome::Terminated,
|
||||
exit_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn runtime_lost(session_id: SessionId) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
outcome: SessionExitOutcome::RuntimeLost,
|
||||
exit_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn session_id(self) -> SessionId {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn outcome(self) -> SessionExitOutcome {
|
||||
self.outcome
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn exit_code(self) -> Option<i32> {
|
||||
self.exit_code
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SessionExit {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
session_id: SessionId,
|
||||
outcome: SessionExitOutcome,
|
||||
exit_code: Option<i32>,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
match (fields.outcome, fields.exit_code) {
|
||||
(SessionExitOutcome::Exited, Some(exit_code)) => {
|
||||
Ok(Self::exited(fields.session_id, exit_code))
|
||||
}
|
||||
(SessionExitOutcome::Terminated, None) => Ok(Self::terminated(fields.session_id)),
|
||||
(SessionExitOutcome::RuntimeLost, None) => Ok(Self::runtime_lost(fields.session_id)),
|
||||
_ => Err(serde::de::Error::custom(
|
||||
"exit_code must be present exactly for Exited",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum SessionTerminalMessage {
|
||||
Output(TerminalOutput),
|
||||
Exit(SessionExit),
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
86
crates/lazaret-core/tests/schema_records/credentials.rs
Normal file
86
crates/lazaret-core/tests/schema_records/credentials.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn participant_and_credential_role_mismatches_are_rejected() {
|
||||
let participant = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantState::LeaseHeld,
|
||||
Some(ParticipantOwnership::PreparedLower(prepared_ownership())),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(participant.role(), ParticipantRole::PreparedLower);
|
||||
let mismatched_participant = encode(&RawParticipant {
|
||||
generation_id: generation_id(),
|
||||
role: ParticipantRole::Network,
|
||||
state: ParticipantState::LeaseHeld,
|
||||
ownership: Some(ParticipantOwnership::PreparedLower(prepared_ownership())),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<Participant>(&mismatched_participant),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
|
||||
let tls = TlsCredential::session_coordinator(
|
||||
generation_identity(),
|
||||
coordinator_id(),
|
||||
vec![1],
|
||||
CertificateDigest::new(opaque(2)),
|
||||
);
|
||||
assert_eq!(tls.role(), TlsRole::SessionCoordinator);
|
||||
let mismatched_tls = encode(&RawTlsCredential {
|
||||
generation_identity: generation_identity(),
|
||||
role: TlsRole::SessionCoordinator,
|
||||
subject_id: TlsSubjectId::ClientCli(ClientId::new(opaque(3))),
|
||||
certificate_der: vec![1],
|
||||
certificate_sha256: CertificateDigest::new(opaque(2)),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<TlsCredential>(&mismatched_tls),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
|
||||
let mismatched_protocol = encode(&RawProtocolCredential {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
credential_id: ProtocolCredentialId::new(opaque(4)),
|
||||
generation_identity: generation_identity(),
|
||||
role: ProtocolCredentialRole::AgentSandbox,
|
||||
subject_id: ProtocolSubjectId::SessionCoordinator(coordinator_id()),
|
||||
public_key: vec![5; 32],
|
||||
issuer_signature: vec![6; 64],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<ProtocolCredential>(&mismatched_protocol),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
|
||||
let coordinator = protocol_credential(ProtocolCredentialRole::SessionCoordinator);
|
||||
let sandbox = protocol_credential(ProtocolCredentialRole::AgentSandbox);
|
||||
assert!(
|
||||
SandboxCredentialBundle::new(
|
||||
generation_identity(),
|
||||
trust_manifest(),
|
||||
sandbox.clone(),
|
||||
coordinator.clone(),
|
||||
vec![7; 32],
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let swapped_bundle = encode(&RawSandboxCredentialBundle {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
generation_identity: generation_identity(),
|
||||
trust_manifest: trust_manifest(),
|
||||
coordinator_protocol_credential: sandbox,
|
||||
sandbox_protocol_credential: coordinator,
|
||||
sandbox_private_key: vec![7; 32],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<SandboxCredentialBundle>(&swapped_bundle),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
300
crates/lazaret-core/tests/schema_records/documents.rs
Normal file
300
crates/lazaret-core/tests/schema_records/documents.rs
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn store_identity_and_versions_dispatch_before_rows() {
|
||||
for metadata in [
|
||||
RawMetadata {
|
||||
schema_id: String::new(),
|
||||
schema_version: DOMAIN_STORE_SCHEMA_VERSION,
|
||||
},
|
||||
RawMetadata {
|
||||
schema_id: DOMAIN_STORE_SCHEMA_ID.into(),
|
||||
schema_version: 0,
|
||||
},
|
||||
RawMetadata {
|
||||
schema_id: DOMAIN_STORE_SCHEMA_ID.into(),
|
||||
schema_version: DOMAIN_STORE_SCHEMA_VERSION + 1,
|
||||
},
|
||||
RawMetadata {
|
||||
schema_id: "unknown-store".into(),
|
||||
schema_version: DOMAIN_STORE_SCHEMA_VERSION,
|
||||
},
|
||||
] {
|
||||
assert!(matches!(
|
||||
decode::<StoreMetadata>(&encode(&metadata).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
decode::<StoreMetadata>(
|
||||
&postcard::to_stdvec(&MissingVersion {
|
||||
schema_id: DOMAIN_STORE_SCHEMA_ID.into(),
|
||||
})
|
||||
.unwrap()
|
||||
),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
decode::<DomainStoreMetadata>(&encode(&StoreMetadata::storage_catalog_v1()).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
for domain_metadata in [
|
||||
StoreMetadata::domain_store_v2(),
|
||||
StoreMetadata::legacy_domain_store_v1(),
|
||||
] {
|
||||
assert!(matches!(
|
||||
decode::<StorageCatalogMetadata>(&encode(&domain_metadata).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
let legacy = StoreMetadata::legacy_domain_store_v1();
|
||||
assert_eq!(
|
||||
decode::<LegacyDomainStoreMetadata>(&encode(&legacy).unwrap()).unwrap(),
|
||||
LegacyDomainStoreMetadata::v1()
|
||||
);
|
||||
assert!(matches!(
|
||||
decode::<DomainStoreMetadata>(&encode(&legacy).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
|
||||
let wrong_domain_store = postcard::to_stdvec(&RawDomainStore {
|
||||
metadata: StoreMetadata::storage_catalog_v1(),
|
||||
domain: Domain {
|
||||
domain_id: domain_id(),
|
||||
profile_name: "trusted".into(),
|
||||
canonical_workspace: HostPath::new(b"workspace".to_vec()),
|
||||
},
|
||||
generations: vec![],
|
||||
participants: vec![],
|
||||
sessions: vec![],
|
||||
session_terminals: vec![],
|
||||
mailboxes: vec![],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<DomainStore>(&wrong_domain_store),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
|
||||
let wrong_catalog = postcard::to_stdvec(&RawStorageCatalog {
|
||||
metadata: StoreMetadata::domain_store_v2(),
|
||||
prepared_artifacts: vec![],
|
||||
artifact_leases: vec![],
|
||||
prepare_operations: vec![],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<StorageCatalog>(&wrong_catalog),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_document_schema_identities_are_fixed_to_v1() {
|
||||
let invalid_ids = |schema_id: &str, wrong_document_id: &str| {
|
||||
[
|
||||
String::new(),
|
||||
schema_id.replace("-v1", "-v0"),
|
||||
schema_id.replace("-v1", "-v2"),
|
||||
wrong_document_id.into(),
|
||||
]
|
||||
};
|
||||
|
||||
let policy = DerivedNetworkPolicy::v1(DerivedNetworkPolicyFields {
|
||||
address_families: vec![],
|
||||
default_actions: vec![],
|
||||
prefix_rules: vec![],
|
||||
resolvers: vec![],
|
||||
});
|
||||
assert_eq!(policy.schema_id(), DERIVED_NETWORK_POLICY_SCHEMA_ID);
|
||||
for schema_id in invalid_ids(
|
||||
DERIVED_NETWORK_POLICY_SCHEMA_ID,
|
||||
NETWORK_READINESS_ATTESTATION_SCHEMA_ID,
|
||||
) {
|
||||
let encoded = postcard::to_stdvec(&RawDerivedNetworkPolicy {
|
||||
schema_id,
|
||||
address_families: vec![],
|
||||
default_actions: vec![],
|
||||
prefix_rules: vec![],
|
||||
resolvers: vec![],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<DerivedNetworkPolicy>(&encoded),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
||||
let attestation = readiness_attestation();
|
||||
assert_eq!(
|
||||
attestation.schema_id(),
|
||||
NETWORK_READINESS_ATTESTATION_SCHEMA_ID
|
||||
);
|
||||
for schema_id in invalid_ids(
|
||||
NETWORK_READINESS_ATTESTATION_SCHEMA_ID,
|
||||
NETWORK_READINESS_IDENTITY_SCHEMA_ID,
|
||||
) {
|
||||
let encoded = postcard::to_stdvec(&RawNetworkReadinessAttestation {
|
||||
schema_id,
|
||||
domain_id: domain_id(),
|
||||
generation_id: generation_id(),
|
||||
configuration_fingerprint: ConfigurationFingerprint::new(opaque(1)),
|
||||
network_sidecar_id: sidecar_id(),
|
||||
derived_policy_digest: DerivedPolicyDigest::new(opaque(2)),
|
||||
enforcement_namespace: namespace(),
|
||||
runtime_supervisor: process(3),
|
||||
connectivity_helper: process(4),
|
||||
observation: network_observation(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<NetworkReadinessAttestation>(&encoded),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
||||
let identity = readiness_identity();
|
||||
assert_eq!(identity.schema_id(), NETWORK_READINESS_IDENTITY_SCHEMA_ID);
|
||||
for schema_id in invalid_ids(
|
||||
NETWORK_READINESS_IDENTITY_SCHEMA_ID,
|
||||
NETWORK_READINESS_ATTESTATION_SCHEMA_ID,
|
||||
) {
|
||||
let encoded = postcard::to_stdvec(&RawNetworkReadinessIdentity {
|
||||
schema_id,
|
||||
domain_id: domain_id(),
|
||||
generation_id: generation_id(),
|
||||
configuration_fingerprint: ConfigurationFingerprint::new(opaque(1)),
|
||||
network_sidecar_id: sidecar_id(),
|
||||
derived_policy_digest: DerivedPolicyDigest::new(opaque(2)),
|
||||
enforcement_namespace: namespace(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<NetworkReadinessIdentity>(&encoded),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_host_configuration_postcard_dispatch_rejects_unknown_schema() {
|
||||
let mut profiles = BTreeMap::new();
|
||||
profiles.insert(
|
||||
"default".into(),
|
||||
ProfileConfigurationV1 {
|
||||
image: "example.invalid/agent:v1".into(),
|
||||
isolate_gitignored: true,
|
||||
persistent_dirs: vec![],
|
||||
allow_egress: vec![],
|
||||
deny_egress: vec![],
|
||||
env: BTreeMap::new(),
|
||||
volumes: vec![],
|
||||
escalation: EscalationConfigurationV1::default(),
|
||||
},
|
||||
);
|
||||
for schema_id in [
|
||||
"",
|
||||
"lazaret-trusted-host-config-v0",
|
||||
"lazaret-trusted-host-config-v2",
|
||||
] {
|
||||
let raw = RawTrustedHostConfigurationV1 {
|
||||
schema_id: schema_id.into(),
|
||||
storage: StorageConfigurationV1::default(),
|
||||
network: NetworkConfigurationV1::default(),
|
||||
profiles: profiles.clone(),
|
||||
};
|
||||
assert!(matches!(
|
||||
decode::<TrustedHostConfigurationV1>(&encode(&raw).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_terminal_dimensions_are_rejected() {
|
||||
assert!(NonZeroU16::new(0).is_none());
|
||||
|
||||
for (rows, columns) in [(0, 80), (24, 0)] {
|
||||
let encoded = postcard::to_stdvec(&RawTerminalResize {
|
||||
session_id: session_id(),
|
||||
rows,
|
||||
columns,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<TerminalResize>(&encoded),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_schema_record_limit_is_enforced_before_decode() {
|
||||
let oversized = vec![0; MAX_SCHEMA_RECORD_BYTES + 1];
|
||||
assert!(matches!(
|
||||
decode::<Vec<u8>>(&oversized),
|
||||
Err(CodecError::RecordTooLarge { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
encode(&oversized),
|
||||
Err(CodecError::RecordTooLarge { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_request_command_vector_is_closed_and_direct() {
|
||||
for command_argv in [
|
||||
vec![],
|
||||
vec![b"true".to_vec()],
|
||||
vec![b"/usr/bin/true".to_vec(), vec![0xff]],
|
||||
vec![b"/usr/bin/true".to_vec(), b"bad\0argument".to_vec()],
|
||||
] {
|
||||
let raw = RawMailboxRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(42)),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(17)),
|
||||
command_argv,
|
||||
justification: "test".into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
decode::<MailboxRequest>(&encode(&raw).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
assert!(
|
||||
MailboxRequest::new(
|
||||
session_binding(),
|
||||
RequestId::new(opaque(42)),
|
||||
session_capability(),
|
||||
ProtocolCredentialId::new(opaque(17)),
|
||||
vec![b"/bin/sh".to_vec(), b"-c".to_vec(), b"fixed".to_vec()],
|
||||
"exact direct vector".into(),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_exit_code_is_accepted_only_for_exited() {
|
||||
let encoded = encode(&InvalidSessionExit {
|
||||
session_id: session_id(),
|
||||
outcome: SessionExitOutcome::Terminated,
|
||||
exit_code: Some(1),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
decode::<SessionExit>(&encoded),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_encoding_has_stable_byte_order() {
|
||||
assert_eq!(encode(&ParticipantState::Intent).unwrap(), [0]);
|
||||
}
|
||||
845
crates/lazaret-core/tests/schema_records/encoding.rs
Normal file
845
crates/lazaret-core/tests/schema_records/encoding.rs
Normal file
|
|
@ -0,0 +1,845 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
||||
let terminal = SessionTerminal::snapshot(SessionTerminalFields {
|
||||
generation_id: generation_id(),
|
||||
session_id: session_id(),
|
||||
state: SessionTerminalState::Running,
|
||||
container_id: Some(ContainerId::new(opaque(29))),
|
||||
host_coordinator: owned_process(30),
|
||||
pty_master: Some(PtyIdentity {
|
||||
master_fd: FileDescriptor::new(31),
|
||||
device: DeviceId::new(32),
|
||||
inode: InodeId::new(33),
|
||||
}),
|
||||
exec_client: owned_process(34),
|
||||
guest_process: Some(GuestProcessIdentity {
|
||||
leader_pid: GuestProcessId::new(35),
|
||||
leader_start_time: GuestProcessStartTimeTicks::new(36),
|
||||
process_group_id: GuestProcessGroupId::new(37),
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
let domain = Domain {
|
||||
domain_id: domain_id(),
|
||||
profile_name: "trusted".into(),
|
||||
canonical_workspace: HostPath::new(vec![b'/', b'w', 0xfe]),
|
||||
};
|
||||
let generation = Generation {
|
||||
domain_id: domain_id(),
|
||||
generation_id: generation_id(),
|
||||
configuration_fingerprint: ConfigurationFingerprint::new(opaque(6)),
|
||||
coordinator_id: coordinator_id(),
|
||||
network_sidecar_id: sidecar_id(),
|
||||
sandbox_id: sandbox_id(),
|
||||
durable_decision: DurableDecision::Preparing,
|
||||
};
|
||||
let participant = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantState::LeaseHeld,
|
||||
Some(ParticipantOwnership::PreparedLower(prepared_ownership())),
|
||||
)
|
||||
.unwrap();
|
||||
let session = Session {
|
||||
generation_id: generation_id(),
|
||||
session_id: session_id(),
|
||||
cli_id: ClientId::new(opaque(13)),
|
||||
session_anchor_id: SessionAnchorId::new(opaque(38)),
|
||||
session_phase: SessionPhase::Attached,
|
||||
};
|
||||
let mailbox = Mailbox {
|
||||
generation_id: generation_id(),
|
||||
identity_path: HostPath::new(b"identity".to_vec()),
|
||||
inbox_path: HostPath::new(b"inbox".to_vec()),
|
||||
outbox_path: HostPath::new(b"outbox".to_vec()),
|
||||
state: ParticipantState::Committed,
|
||||
};
|
||||
let metadata = StoreMetadata::domain_store_v2();
|
||||
let tls_credential = TlsCredential::session_coordinator(
|
||||
generation_identity(),
|
||||
coordinator_id(),
|
||||
vec![39, 40],
|
||||
CertificateDigest::new(opaque(41)),
|
||||
);
|
||||
let request = MailboxRequest::new(
|
||||
session_binding(),
|
||||
RequestId::new(opaque(42)),
|
||||
session_capability(),
|
||||
ProtocolCredentialId::new(opaque(17)),
|
||||
vec![b"/usr/bin/printf".to_vec(), b"value".to_vec()],
|
||||
"build dependency".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let prepared_artifact = PreparedArtifact {
|
||||
prepared_artifact_id: PreparedArtifactId::new(opaque(24)),
|
||||
source_identity: SourceIdentity::new(vec![25, 26]),
|
||||
selected_manifest_digest: SelectedManifestDigest::new(opaque(43)),
|
||||
policy_id: PolicyId::new(opaque(44)),
|
||||
id_map_digest: IdMapDigest::new(opaque(45)),
|
||||
path: HostPath::new(b"artifact".to_vec()),
|
||||
device: DeviceId::new(46),
|
||||
inode: InodeId::new(47),
|
||||
state: ArtifactState::Available,
|
||||
};
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
verify_shape(metadata.clone(), &mut seen);
|
||||
verify_shape(domain.clone(), &mut seen);
|
||||
verify_shape(generation.clone(), &mut seen);
|
||||
verify_shape(participant.clone(), &mut seen);
|
||||
verify_shape(ParticipantState::ReleaseIntent, &mut seen);
|
||||
verify_shape(
|
||||
ParticipantOwnership::Network(Box::new(network_ownership())),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(session.clone(), &mut seen);
|
||||
verify_shape(terminal.clone(), &mut seen);
|
||||
verify_shape(SessionTerminalState::ReleaseIntent, &mut seen);
|
||||
verify_shape(mailbox.clone(), &mut seen);
|
||||
verify_shape(terminal.pty_master().unwrap(), &mut seen);
|
||||
let pidfs_identity = process(48);
|
||||
let staged = StagedProcessOwnership::v1(
|
||||
domain_id(),
|
||||
generation_id(),
|
||||
coordinator_id(),
|
||||
HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||
pidfs_identity,
|
||||
);
|
||||
verify_shape(pidfs_identity.authority(), &mut seen);
|
||||
verify_shape(pidfs_identity, &mut seen);
|
||||
verify_shape(
|
||||
LegacyUnverifiedProcessIdentityV1::new(
|
||||
HostPidLocator::new(48),
|
||||
ProcessStartTimeTicks::new(480),
|
||||
),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(HostProcessIdentity::pidfs_v1(pidfs_identity), &mut seen);
|
||||
verify_shape(
|
||||
PersistedHostProcessOwnership::staged(pidfs_identity),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
HostProcessOwnershipSlot::SessionTerminalExecClient(session_id()),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(ProcessOwnershipMessageVersion::V1, &mut seen);
|
||||
verify_shape(staged, &mut seen);
|
||||
verify_shape(ProcessOwnershipCommitAck::v1(staged), &mut seen);
|
||||
verify_shape(terminal.guest_process().unwrap(), &mut seen);
|
||||
verify_shape(
|
||||
SessionTerminalEvent::Input(TerminalInput {
|
||||
session_id: session_id(),
|
||||
bytes: vec![49],
|
||||
}),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
TerminalInput {
|
||||
session_id: session_id(),
|
||||
bytes: vec![50],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
TerminalResize {
|
||||
session_id: session_id(),
|
||||
rows: NonZeroU16::new(24).unwrap(),
|
||||
columns: NonZeroU16::new(80).unwrap(),
|
||||
pixel_width: 640,
|
||||
pixel_height: 480,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
TerminalSignal {
|
||||
session_id: session_id(),
|
||||
signal: TerminalSignalKind::Suspend,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(TerminalSignalKind::Interrupt, &mut seen);
|
||||
verify_shape(
|
||||
SessionClose {
|
||||
session_id: session_id(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
SessionTerminalMessage::Output(TerminalOutput {
|
||||
session_id: session_id(),
|
||||
bytes: vec![51],
|
||||
}),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
TerminalOutput {
|
||||
session_id: session_id(),
|
||||
bytes: vec![52],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(SessionExit::exited(session_id(), 3), &mut seen);
|
||||
verify_shape(SessionExitOutcome::RuntimeLost, &mut seen);
|
||||
verify_shape(generation_identity(), &mut seen);
|
||||
verify_shape(trust_manifest(), &mut seen);
|
||||
verify_shape(tls_credential.clone(), &mut seen);
|
||||
verify_shape(
|
||||
ProtocolPublicationIdentity {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
generation_identity: generation_identity(),
|
||||
tls_ca_certificate_der: vec![53],
|
||||
trust_manifest_sha256: TrustManifestDigest::new(opaque(54)),
|
||||
coordinator_tls_certificate_sha256: CertificateDigest::new(opaque(55)),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
SandboxCredentialBundle::new(
|
||||
generation_identity(),
|
||||
trust_manifest(),
|
||||
protocol_credential(ProtocolCredentialRole::SessionCoordinator),
|
||||
protocol_credential(ProtocolCredentialRole::AgentSandbox),
|
||||
vec![56; 32],
|
||||
)
|
||||
.unwrap(),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(TlsRole::ClientCli, &mut seen);
|
||||
verify_shape(
|
||||
protocol_credential(ProtocolCredentialRole::AgentSandbox),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(ProtocolCredentialRole::SessionCoordinator, &mut seen);
|
||||
verify_shape(session_binding(), &mut seen);
|
||||
verify_shape(session_capability(), &mut seen);
|
||||
verify_shape(
|
||||
SignedProtocolRecord {
|
||||
payload_bytes: vec![57],
|
||||
signature: vec![58; 64],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(request.clone(), &mut seen);
|
||||
verify_shape(
|
||||
MailboxCancellation {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(42)),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(17)),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
MailboxResultAck {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(42)),
|
||||
request_sha256: RequestDigest::new(opaque(59)),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(17)),
|
||||
highest_contiguous_sequence: 4,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
DeveloperDecision {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(42)),
|
||||
request_sha256: RequestDigest::new(opaque(59)),
|
||||
decision: DeveloperDecisionKind::Approve,
|
||||
approved_command_argv: request.command_argv().to_vec(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(DeveloperDecisionKind::Deny, &mut seen);
|
||||
verify_shape(
|
||||
MailboxResult {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(42)),
|
||||
request_sha256: RequestDigest::new(opaque(59)),
|
||||
coordinator_credential_id: ProtocolCredentialId::new(opaque(16)),
|
||||
outcome: EscalationOutcome::Completed,
|
||||
exit_code: Some(0),
|
||||
chunk_count: 2,
|
||||
stdout_sha256: StreamDigest::new(opaque(60)),
|
||||
stderr_sha256: StreamDigest::new(opaque(61)),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
MailboxResultChunk {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(42)),
|
||||
request_sha256: RequestDigest::new(opaque(59)),
|
||||
coordinator_credential_id: ProtocolCredentialId::new(opaque(16)),
|
||||
sequence: 1,
|
||||
stream: ResultStream::Stderr,
|
||||
bytes: vec![62],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(ResultStream::Stdout, &mut seen);
|
||||
verify_shape(EscalationOutcome::TimedOut, &mut seen);
|
||||
verify_shape(namespace(), &mut seen);
|
||||
verify_shape(mount(), &mut seen);
|
||||
verify_shape(prepared_ownership(), &mut seen);
|
||||
verify_shape(
|
||||
StateAnchorOwnership {
|
||||
lower_artifact_id: Some(PreparedArtifactId::new(opaque(24))),
|
||||
upper_path: Some(HostPath::new(b"upper".to_vec())),
|
||||
work_path: Some(HostPath::new(b"work".to_vec())),
|
||||
merged_path: Some(HostPath::new(b"merged".to_vec())),
|
||||
daemon: owned_process(63),
|
||||
mount: Some(mount()),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
GitRoutingOwnership {
|
||||
mountpoint: Some(HostPath::new(b"workspace".to_vec())),
|
||||
host_root: Some(HostPath::new(b"host".to_vec())),
|
||||
isolated_root: Some(HostPath::new(b"isolated".to_vec())),
|
||||
daemon: owned_process(64),
|
||||
mount: Some(mount()),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(network_ownership(), &mut seen);
|
||||
verify_shape(
|
||||
HelperOwnership {
|
||||
network_sidecar_id: Some(sidecar_id()),
|
||||
connectivity_helper: owned_process(65),
|
||||
runtime_supervisor: owned_process(21),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
SandboxOwnership {
|
||||
container_id: Some(ContainerId::new(opaque(29))),
|
||||
runtime_root: Some(HostPath::new(b"runtime".to_vec())),
|
||||
bundle_digest: Some(BundleDigest::new(opaque(66))),
|
||||
host_coordinator: owned_process(30),
|
||||
runtime_supervisor: owned_process(21),
|
||||
attached_runsc: owned_process(67),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
PublicationOwnership {
|
||||
socket_path: Some(HostPath::new(b"socket".to_vec())),
|
||||
identity_path: Some(HostPath::new(b"identity".to_vec())),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(prepared_artifact.clone(), &mut seen);
|
||||
verify_shape(ArtifactState::Reclaiming, &mut seen);
|
||||
verify_shape(
|
||||
ArtifactLease {
|
||||
prepared_artifact_id: PreparedArtifactId::new(opaque(24)),
|
||||
domain_id: domain_id(),
|
||||
generation_id: generation_id(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
PrepareOperation {
|
||||
operation_id: PrepareOperationId::new(opaque(68)),
|
||||
expected_artifact_id: PreparedArtifactId::new(opaque(24)),
|
||||
source_identity: SourceIdentity::new(vec![25, 26]),
|
||||
stage_path: HostPath::new(b"stage".to_vec()),
|
||||
device: DeviceId::new(69),
|
||||
inode: InodeId::new(70),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
AddressFamilyDisposition {
|
||||
family: AddressFamily::Ipv6,
|
||||
disposition: AddressFamilyDispositionValue::Disabled,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
DefaultAction {
|
||||
family: AddressFamily::Ipv4,
|
||||
action: PolicyAction::Allow,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
PrefixRule {
|
||||
family: AddressFamily::Ipv4,
|
||||
network: IpNetwork {
|
||||
address: IpAddress::V4([192, 168, 0, 0]),
|
||||
prefix_length: 16,
|
||||
},
|
||||
action: PolicyAction::Deny,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
Resolver {
|
||||
family: AddressFamily::Ipv4,
|
||||
address: IpAddress::V4([9, 9, 9, 9]),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(AddressFamily::Ipv4, &mut seen);
|
||||
verify_shape(PolicyAction::Deny, &mut seen);
|
||||
verify_shape(
|
||||
DerivedNetworkPolicy::v1(DerivedNetworkPolicyFields {
|
||||
address_families: vec![AddressFamilyDisposition {
|
||||
family: AddressFamily::Ipv4,
|
||||
disposition: AddressFamilyDispositionValue::Enabled,
|
||||
}],
|
||||
default_actions: vec![DefaultAction {
|
||||
family: AddressFamily::Ipv4,
|
||||
action: PolicyAction::Allow,
|
||||
}],
|
||||
prefix_rules: vec![],
|
||||
resolvers: vec![Resolver {
|
||||
family: AddressFamily::Ipv4,
|
||||
address: IpAddress::V4([9, 9, 9, 9]),
|
||||
}],
|
||||
}),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(readiness_evidence(), &mut seen);
|
||||
verify_shape(readiness_attestation(), &mut seen);
|
||||
verify_shape(network_observation(), &mut seen);
|
||||
verify_shape(PolicyDelivery::SealedMemfd, &mut seen);
|
||||
verify_shape(readiness_identity(), &mut seen);
|
||||
verify_shape(reachability_probe(), &mut seen);
|
||||
verify_shape(
|
||||
DeniedDestinationObservation {
|
||||
destination: IpAddress::V4([10, 0, 0, 1]),
|
||||
unreachable: true,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
PermittedDestinationObservation {
|
||||
destination: IpAddress::V4([1, 0, 0, 1]),
|
||||
reachable: true,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
DnsObservation {
|
||||
resolver: IpAddress::V4([9, 9, 9, 9]),
|
||||
query_name: "example.invalid".into(),
|
||||
resolved_addresses: vec![IpAddress::V4([203, 0, 113, 1])],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
DomainStore {
|
||||
metadata: DomainStoreMetadata::v2(),
|
||||
domain,
|
||||
generations: vec![generation],
|
||||
participants: vec![participant],
|
||||
sessions: vec![session],
|
||||
session_terminals: vec![terminal],
|
||||
mailboxes: vec![mailbox],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
StorageCatalog {
|
||||
metadata: StorageCatalogMetadata::v1(),
|
||||
prepared_artifacts: vec![prepared_artifact],
|
||||
artifact_leases: vec![],
|
||||
prepare_operations: vec![],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
parse_trusted_configuration(
|
||||
br#"schema = "lazaret-trusted-host-config-v1"
|
||||
[profiles.trusted]
|
||||
image = "example.invalid/agent:v1"
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
ConfigurationFingerprintRecordV1 {
|
||||
selected_profile: "trusted".into(),
|
||||
storage_root: HostPath::new(b"/data/lazaret".to_vec()),
|
||||
runtime_root: HostPath::new(b"/run/user/1000/lazaret".to_vec()),
|
||||
image: "example.invalid/agent:v1".into(),
|
||||
isolate_gitignored: true,
|
||||
persistent_dirs: vec!["/root".into()],
|
||||
environment_references: vec![EnvironmentReference {
|
||||
sandbox_name: "TOKEN".into(),
|
||||
host_name: "HOST_TOKEN".into(),
|
||||
}],
|
||||
volumes: vec![ResolvedVolume {
|
||||
canonical_host_source: HostPath::new(b"/data/source".to_vec()),
|
||||
container_path: "/root/source".into(),
|
||||
kind: VolumeKind::Directory,
|
||||
access: VolumeAccess::ReadOnly,
|
||||
create: false,
|
||||
}],
|
||||
effective_allow_prefixes: vec![([1, 1, 1, 1], 32)],
|
||||
effective_deny_prefixes: vec![([10, 0, 0, 0], 8)],
|
||||
escalation_enabled: true,
|
||||
auto_approval_rules: vec![ResolvedAutoApprovalRule {
|
||||
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
|
||||
args: vec!["--version".into()],
|
||||
allow_extra_args: false,
|
||||
}],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
EnvironmentReference {
|
||||
sandbox_name: "TOKEN".into(),
|
||||
host_name: "HOST_TOKEN".into(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
ResolvedVolume {
|
||||
canonical_host_source: HostPath::new(b"/data/source".to_vec()),
|
||||
container_path: "/root/source".into(),
|
||||
kind: VolumeKind::Directory,
|
||||
access: VolumeAccess::ReadWrite,
|
||||
create: false,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
ResolvedAutoApprovalRule {
|
||||
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
|
||||
args: vec![],
|
||||
allow_extra_args: false,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
|
||||
assert_eq!(seen.len(), IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES.len());
|
||||
assert!(
|
||||
IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES
|
||||
.iter()
|
||||
.all(|name| seen.contains(name))
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum SchemaCategory {
|
||||
Serialized,
|
||||
Runtime,
|
||||
}
|
||||
|
||||
fn parse_schema_categories(
|
||||
schema: &str,
|
||||
serialized_registry: &HashSet<&str>,
|
||||
runtime_registry: &HashSet<&str>,
|
||||
) -> Result<HashMap<String, SchemaCategory>, String> {
|
||||
if let Some(name) = serialized_registry.intersection(runtime_registry).next() {
|
||||
return Err(format!("schema registries overlap at {name}"));
|
||||
}
|
||||
|
||||
let mut documented = HashMap::new();
|
||||
let mut current: Option<(&str, Option<SchemaCategory>)> = None;
|
||||
|
||||
for line in schema.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(declaration) = trimmed.strip_prefix("class ") {
|
||||
if current.is_some() {
|
||||
return Err("nested class declaration".into());
|
||||
}
|
||||
let name = declaration
|
||||
.split(|character: char| character.is_whitespace() || character == '{')
|
||||
.next()
|
||||
.filter(|name| !name.is_empty())
|
||||
.ok_or_else(|| "class declaration has no name".to_owned())?;
|
||||
if !declaration.contains('{') {
|
||||
return Err(format!("class {name} has no stereotype block"));
|
||||
}
|
||||
if !serialized_registry.contains(name) && !runtime_registry.contains(name) {
|
||||
return Err(format!("unknown schema class {name}"));
|
||||
}
|
||||
current = Some((name, None));
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((name, category)) = current.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
if trimmed == "}" {
|
||||
let category = category.ok_or_else(|| format!("class {name} is unmarked"))?;
|
||||
match documented.insert((*name).to_owned(), category) {
|
||||
Some(existing) if existing != category => {
|
||||
return Err(format!("class {name} has conflicting categories"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
current = None;
|
||||
continue;
|
||||
}
|
||||
let Some(stereotype) = trimmed
|
||||
.strip_prefix("<<")
|
||||
.and_then(|value| value.strip_suffix(">>"))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let parsed = match stereotype {
|
||||
"serialized" => Some(SchemaCategory::Serialized),
|
||||
"runtime" => Some(SchemaCategory::Runtime),
|
||||
"enumeration" | "union" => None,
|
||||
_ => return Err(format!("class {name} has unknown stereotype {stereotype}")),
|
||||
};
|
||||
if let Some(parsed) = parsed
|
||||
&& category.replace(parsed).is_some()
|
||||
{
|
||||
return Err(format!("class {name} has multiple category markers"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((name, _)) = current {
|
||||
return Err(format!("class {name} has no closing brace"));
|
||||
}
|
||||
for name in serialized_registry.union(runtime_registry) {
|
||||
if !documented.contains_key(*name) {
|
||||
return Err(format!("registered schema class {name} is undocumented"));
|
||||
}
|
||||
}
|
||||
Ok(documented)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_categories_match_exact_disjoint_registries() {
|
||||
let serialized = DECLARED_SERIALIZED_SCHEMA_TYPE_NAMES
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<HashSet<_>>();
|
||||
let runtime = DECLARED_RUNTIME_SCHEMA_TYPE_NAMES
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
serialized.len(),
|
||||
DECLARED_SERIALIZED_SCHEMA_TYPE_NAMES.len()
|
||||
);
|
||||
assert_eq!(runtime.len(), DECLARED_RUNTIME_SCHEMA_TYPE_NAMES.len());
|
||||
|
||||
let documented = parse_schema_categories(
|
||||
include_str!("../../../../docs/schema.md"),
|
||||
&serialized,
|
||||
&runtime,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let documented_serialized = documented
|
||||
.iter()
|
||||
.filter_map(|(name, category)| {
|
||||
(*category == SchemaCategory::Serialized).then_some(name.as_str())
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
let documented_runtime = documented
|
||||
.iter()
|
||||
.filter_map(|(name, category)| {
|
||||
(*category == SchemaCategory::Runtime).then_some(name.as_str())
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(documented_serialized, serialized);
|
||||
assert_eq!(documented_runtime, runtime);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_category_parser_fails_closed() {
|
||||
let serialized = HashSet::from(["Known"]);
|
||||
let runtime = HashSet::from(["Runtime"]);
|
||||
let overlapping = HashSet::from(["Known"]);
|
||||
|
||||
for invalid in [
|
||||
"class Known {\nfield\n}",
|
||||
"class Known {\n<<serialized>>\n<<serialized>>\n}",
|
||||
"class Known {\n<<serialized>>\n<<runtime>>\n}",
|
||||
"class Unknown {\n<<serialized>>\n}",
|
||||
"class Known {\n<<unexpected>>\n}",
|
||||
] {
|
||||
assert!(parse_schema_categories(invalid, &serialized, &runtime).is_err());
|
||||
}
|
||||
assert!(parse_schema_categories("", &serialized, &runtime).is_err());
|
||||
assert!(parse_schema_categories("", &serialized, &overlapping).is_err());
|
||||
|
||||
assert!(
|
||||
parse_schema_categories(
|
||||
"class Known {\n<<serialized>>\n}\nclass Runtime {\n<<runtime>>\n}",
|
||||
&serialized,
|
||||
&runtime,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_unknown_protocol_version<T>(record: &T)
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let mut encoded = encode(record).unwrap();
|
||||
encoded[0] = u8::MAX;
|
||||
assert!(matches!(
|
||||
decode::<T>(&encoded),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_and_nonminimal_postcard_encodings_are_rejected() {
|
||||
assert!(matches!(
|
||||
decode::<ParticipantState>(&[0, 0]),
|
||||
Err(CodecError::TrailingPayload)
|
||||
));
|
||||
assert!(matches!(
|
||||
decode::<ParticipantState>(&[0x80, 0]),
|
||||
Err(CodecError::NonMinimalPayload)
|
||||
));
|
||||
|
||||
let input = TerminalInput {
|
||||
session_id: session_id(),
|
||||
bytes: vec![1],
|
||||
};
|
||||
let mut overlong_length = encode(&input).unwrap();
|
||||
overlong_length[32] = 0x81;
|
||||
overlong_length.insert(33, 0);
|
||||
assert!(matches!(
|
||||
decode::<TerminalInput>(&overlong_length),
|
||||
Err(CodecError::NonMinimalPayload)
|
||||
));
|
||||
|
||||
let mut overlong_protocol_version = encode(&trust_manifest()).unwrap();
|
||||
overlong_protocol_version[0] = 0x80;
|
||||
overlong_protocol_version.insert(1, 0);
|
||||
assert!(matches!(
|
||||
decode::<TrustManifest>(&overlong_protocol_version),
|
||||
Err(CodecError::NonMinimalPayload)
|
||||
));
|
||||
|
||||
let mut overlong_store_id = encode(&StoreMetadata::domain_store_v2()).unwrap();
|
||||
overlong_store_id[0] |= 0x80;
|
||||
overlong_store_id.insert(1, 0);
|
||||
assert!(matches!(
|
||||
decode::<StoreMetadata>(&overlong_store_id),
|
||||
Err(CodecError::NonMinimalPayload)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_enums_and_protocol_versions_are_rejected() {
|
||||
assert!(matches!(
|
||||
decode::<ParticipantState>(&[u8::MAX]),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
|
||||
assert_unknown_protocol_version(&trust_manifest());
|
||||
assert_unknown_protocol_version(&ProtocolPublicationIdentity {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
generation_identity: generation_identity(),
|
||||
tls_ca_certificate_der: vec![1],
|
||||
trust_manifest_sha256: TrustManifestDigest::new(opaque(2)),
|
||||
coordinator_tls_certificate_sha256: CertificateDigest::new(opaque(3)),
|
||||
});
|
||||
assert_unknown_protocol_version(
|
||||
&SandboxCredentialBundle::new(
|
||||
generation_identity(),
|
||||
trust_manifest(),
|
||||
protocol_credential(ProtocolCredentialRole::SessionCoordinator),
|
||||
protocol_credential(ProtocolCredentialRole::AgentSandbox),
|
||||
vec![4; 32],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
assert_unknown_protocol_version(&protocol_credential(
|
||||
ProtocolCredentialRole::SessionCoordinator,
|
||||
));
|
||||
assert_unknown_protocol_version(&session_capability());
|
||||
|
||||
let request = MailboxRequest::new(
|
||||
session_binding(),
|
||||
RequestId::new(opaque(5)),
|
||||
session_capability(),
|
||||
ProtocolCredentialId::new(opaque(6)),
|
||||
vec![b"/usr/bin/true".to_vec()],
|
||||
"test".into(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_unknown_protocol_version(&request);
|
||||
assert_unknown_protocol_version(&MailboxCancellation {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id(),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(6)),
|
||||
});
|
||||
assert_unknown_protocol_version(&MailboxResultAck {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id(),
|
||||
request_sha256: RequestDigest::new(opaque(7)),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(6)),
|
||||
highest_contiguous_sequence: 0,
|
||||
});
|
||||
assert_unknown_protocol_version(&DeveloperDecision {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id(),
|
||||
request_sha256: RequestDigest::new(opaque(7)),
|
||||
decision: DeveloperDecisionKind::Deny,
|
||||
approved_command_argv: vec![],
|
||||
});
|
||||
assert_unknown_protocol_version(&MailboxResult {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id(),
|
||||
request_sha256: RequestDigest::new(opaque(7)),
|
||||
coordinator_credential_id: ProtocolCredentialId::new(opaque(8)),
|
||||
outcome: EscalationOutcome::Denied,
|
||||
exit_code: None,
|
||||
chunk_count: 0,
|
||||
stdout_sha256: StreamDigest::new(opaque(9)),
|
||||
stderr_sha256: StreamDigest::new(opaque(10)),
|
||||
});
|
||||
assert_unknown_protocol_version(&MailboxResultChunk {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id(),
|
||||
request_sha256: RequestDigest::new(opaque(7)),
|
||||
coordinator_credential_id: ProtocolCredentialId::new(opaque(8)),
|
||||
sequence: 0,
|
||||
stream: ResultStream::Stdout,
|
||||
bytes: vec![],
|
||||
});
|
||||
|
||||
let staged = StagedProcessOwnership::v1(
|
||||
domain_id(),
|
||||
generation_id(),
|
||||
coordinator_id(),
|
||||
HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
||||
process(90),
|
||||
);
|
||||
assert_unknown_protocol_version(&staged);
|
||||
assert_unknown_protocol_version(&ProcessOwnershipCommitAck::v1(staged));
|
||||
assert!(matches!(
|
||||
decode::<HostProcessIdentity>(&[u8::MAX]),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
decode::<PersistedHostProcessOwnership>(&[u8::MAX]),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
|
@ -0,0 +1,564 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn participant_transitions_preserve_and_clear_exact_evidence() {
|
||||
let bare = Participant::bare_intent(generation_id(), ParticipantRole::StateAnchor);
|
||||
let initial_ownership = ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
None,
|
||||
));
|
||||
let initial = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(initial_ownership),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bare.validate_transition_to(&initial), Ok(()));
|
||||
|
||||
let staged_identity = process(96);
|
||||
let staged_ownership = ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
PersistedHostProcessOwnership::staged(staged_identity),
|
||||
None,
|
||||
));
|
||||
let staged = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(staged_ownership.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
staged.validate_transition_to(&initial),
|
||||
Err(RecordTransitionError::EvidenceLost)
|
||||
);
|
||||
assert_eq!(initial.validate_transition_to(&staged), Ok(()));
|
||||
|
||||
let committed_ownership = ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
PersistedHostProcessOwnership::committed(staged_identity),
|
||||
None,
|
||||
));
|
||||
let committed = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(committed_ownership),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(staged.validate_transition_to(&committed), Ok(()));
|
||||
|
||||
let mounted_ownership = ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
PersistedHostProcessOwnership::committed(staged_identity),
|
||||
Some(mount()),
|
||||
));
|
||||
let mounted = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(mounted_ownership.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(committed.validate_transition_to(&mounted), Ok(()));
|
||||
let active = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Prepared,
|
||||
Some(mounted_ownership.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(mounted.validate_transition_to(&active), Ok(()));
|
||||
assert_eq!(
|
||||
bare.validate_transition_to(&active),
|
||||
Err(RecordTransitionError::PriorAcquisitionSnapshotRequired)
|
||||
);
|
||||
|
||||
let release = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::ReleaseIntent,
|
||||
Some(mounted_ownership),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(active.validate_transition_to(&release), Ok(()));
|
||||
let clean = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Clean,
|
||||
Some(ParticipantOwnership::empty(ParticipantRole::StateAnchor)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
release.validate_transition_to(&clean),
|
||||
Err(RecordTransitionError::AbsenceProofRequired)
|
||||
);
|
||||
assert_eq!(
|
||||
release.validate_transition_to_clean_after_absence(&clean),
|
||||
Ok(())
|
||||
);
|
||||
assert!(clean.ownership().unwrap().is_empty());
|
||||
|
||||
let bare_clean = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Clean,
|
||||
Some(ParticipantOwnership::empty(ParticipantRole::StateAnchor)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
bare.validate_transition_to_clean_after_absence(&bare_clean),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
initial.validate_transition_to_clean_after_absence(&bare_clean),
|
||||
Err(RecordTransitionError::StateRegression)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn newly_recorded_existing_processes_do_not_bypass_child_staging() {
|
||||
let existing_edges = [
|
||||
ParticipantProcessEdge::NetworkRuntimeSupervisor,
|
||||
ParticipantProcessEdge::HelperRuntimeSupervisor,
|
||||
ParticipantProcessEdge::SandboxHostCoordinator,
|
||||
];
|
||||
for (index, edge) in existing_edges.into_iter().enumerate() {
|
||||
let current = process(130 + u32::try_from(index).unwrap());
|
||||
let unassigned =
|
||||
participant_edge_snapshot(edge, PersistedHostProcessOwnership::unassigned());
|
||||
let committed =
|
||||
participant_edge_snapshot(edge, PersistedHostProcessOwnership::committed(current));
|
||||
let bare = Participant::bare_intent(generation_id(), committed.role());
|
||||
assert_eq!(bare.validate_transition_to(&committed), Ok(()), "{edge:?}");
|
||||
assert_eq!(
|
||||
unassigned.validate_transition_to(&committed),
|
||||
Ok(()),
|
||||
"{edge:?}"
|
||||
);
|
||||
|
||||
let legacy = participant_edge_snapshot(
|
||||
edge,
|
||||
PersistedHostProcessOwnership::legacy_unverified(
|
||||
LegacyUnverifiedProcessIdentityV1::new(
|
||||
current.host_pid(),
|
||||
current.start_time_ticks(),
|
||||
),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
unassigned.validate_transition_to(&legacy),
|
||||
Err(RecordTransitionError::ProcessStageSkipped),
|
||||
"{edge:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let acquired_edges = [
|
||||
ParticipantProcessEdge::StateAnchorDaemon,
|
||||
ParticipantProcessEdge::GitRoutingDaemon,
|
||||
ParticipantProcessEdge::HelperConnectivityHelper,
|
||||
ParticipantProcessEdge::SandboxRuntimeSupervisor,
|
||||
ParticipantProcessEdge::SandboxAttachedRunsc,
|
||||
];
|
||||
for (index, edge) in acquired_edges.into_iter().enumerate() {
|
||||
let current = process(140 + u32::try_from(index).unwrap());
|
||||
let unassigned =
|
||||
participant_edge_snapshot(edge, PersistedHostProcessOwnership::unassigned());
|
||||
let staged =
|
||||
participant_edge_snapshot(edge, PersistedHostProcessOwnership::staged(current));
|
||||
let committed =
|
||||
participant_edge_snapshot(edge, PersistedHostProcessOwnership::committed(current));
|
||||
assert_eq!(
|
||||
unassigned.validate_transition_to(&staged),
|
||||
Ok(()),
|
||||
"{edge:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
staged.validate_transition_to(&committed),
|
||||
Ok(()),
|
||||
"{edge:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
unassigned.validate_transition_to(&committed),
|
||||
Err(RecordTransitionError::ProcessStageSkipped),
|
||||
"{edge:?}"
|
||||
);
|
||||
let wrong_commit = participant_edge_snapshot(
|
||||
edge,
|
||||
PersistedHostProcessOwnership::committed(process(150 + u32::try_from(index).unwrap())),
|
||||
);
|
||||
assert_eq!(
|
||||
staged.validate_transition_to(&wrong_commit),
|
||||
Err(RecordTransitionError::EvidenceChanged),
|
||||
"{edge:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let terminal_unassigned = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let terminal_existing_hc = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
owned_process(101),
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
terminal_unassigned.validate_transition_to(&terminal_existing_hc),
|
||||
Ok(())
|
||||
);
|
||||
let terminal_legacy_hc = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
PersistedHostProcessOwnership::legacy_unverified(LegacyUnverifiedProcessIdentityV1::new(
|
||||
HostPidLocator::new(101),
|
||||
ProcessStartTimeTicks::new(1_010),
|
||||
)),
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
terminal_unassigned.validate_transition_to(&terminal_legacy_hc),
|
||||
Err(RecordTransitionError::ProcessStageSkipped)
|
||||
);
|
||||
|
||||
let terminal_exec_staged = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
owned_process(101),
|
||||
PersistedHostProcessOwnership::staged(process(102)),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let terminal_exec_committed = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
owned_process(101),
|
||||
owned_process(102),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
terminal_existing_hc.validate_transition_to(&terminal_exec_staged),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
terminal_exec_staged.validate_transition_to(&terminal_exec_committed),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
terminal_existing_hc.validate_transition_to(&terminal_exec_committed),
|
||||
Err(RecordTransitionError::ProcessStageSkipped)
|
||||
);
|
||||
|
||||
let terminal_wrong_exec = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
owned_process(101),
|
||||
owned_process(103),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
terminal_exec_staged.validate_transition_to(&terminal_wrong_exec),
|
||||
Err(RecordTransitionError::EvidenceChanged)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn participant_transitions_reject_field_loss_replacement_and_stage_skips() {
|
||||
let current = process(97);
|
||||
let initial_ownership =
|
||||
state_anchor_ownership(PersistedHostProcessOwnership::unassigned(), None);
|
||||
let initial = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(ParticipantOwnership::StateAnchor(initial_ownership.clone())),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let direct_commit = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
PersistedHostProcessOwnership::committed(current),
|
||||
None,
|
||||
))),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
initial.validate_transition_to(&direct_commit),
|
||||
Err(RecordTransitionError::ProcessStageSkipped)
|
||||
);
|
||||
let staged = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
PersistedHostProcessOwnership::staged(current),
|
||||
None,
|
||||
))),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
direct_commit.validate_transition_to(&staged),
|
||||
Err(RecordTransitionError::EvidenceLost)
|
||||
);
|
||||
let same_authority_different_locator = PidfsProcessIdentityV1::new(
|
||||
current.authority(),
|
||||
HostPidLocator::new(current.host_pid().get() + 1),
|
||||
current.start_time_ticks(),
|
||||
);
|
||||
let replaced = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
PersistedHostProcessOwnership::committed(same_authority_different_locator),
|
||||
None,
|
||||
))),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(current.has_same_authority(&same_authority_different_locator));
|
||||
assert_eq!(
|
||||
staged.validate_transition_to(&replaced),
|
||||
Err(RecordTransitionError::EvidenceChanged)
|
||||
);
|
||||
|
||||
let mut losses = Vec::new();
|
||||
let mut lost = initial_ownership.clone();
|
||||
lost.lower_artifact_id = None;
|
||||
losses.push(lost);
|
||||
let mut lost = initial_ownership.clone();
|
||||
lost.upper_path = None;
|
||||
losses.push(lost);
|
||||
let mut lost = initial_ownership.clone();
|
||||
lost.work_path = None;
|
||||
losses.push(lost);
|
||||
let mut lost = initial_ownership;
|
||||
lost.merged_path = None;
|
||||
losses.push(lost);
|
||||
for lost in losses {
|
||||
let lost = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(ParticipantOwnership::StateAnchor(lost)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
initial.validate_transition_to(&lost),
|
||||
Err(RecordTransitionError::EvidenceLost)
|
||||
);
|
||||
}
|
||||
|
||||
let partial_release = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::ReleaseIntent,
|
||||
initial.ownership().cloned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(initial.validate_transition_to(&partial_release), Ok(()));
|
||||
assert_eq!(partial_release.ownership(), initial.ownership());
|
||||
|
||||
let staged_release = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::ReleaseIntent,
|
||||
staged.ownership().cloned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(staged.validate_transition_to(&staged_release), Ok(()));
|
||||
let ParticipantOwnership::StateAnchor(staged_release_ownership) =
|
||||
staged_release.ownership().unwrap()
|
||||
else {
|
||||
panic!("wrong ownership role")
|
||||
};
|
||||
assert_eq!(
|
||||
staged_release_ownership
|
||||
.daemon
|
||||
.authoritative_pidfs()
|
||||
.unwrap(),
|
||||
¤t
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn participant_and_terminal_transition_tables_reject_every_regression() {
|
||||
let intent = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
Some(complete_ownership(
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Intent,
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
let active = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Prepared,
|
||||
Some(complete_ownership(
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Prepared,
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
let release = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::ReleaseIntent,
|
||||
Some(release_ownership(ParticipantRole::StateAnchor)),
|
||||
)
|
||||
.unwrap();
|
||||
let clean = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Clean,
|
||||
Some(ParticipantOwnership::empty(ParticipantRole::StateAnchor)),
|
||||
)
|
||||
.unwrap();
|
||||
let states = [&intent, &active, &release, &clean];
|
||||
let expected = [
|
||||
[
|
||||
Ok(()),
|
||||
Ok(()),
|
||||
Ok(()),
|
||||
Err(RecordTransitionError::AbsenceProofRequired),
|
||||
],
|
||||
[
|
||||
Err(RecordTransitionError::StateRegression),
|
||||
Ok(()),
|
||||
Ok(()),
|
||||
Err(RecordTransitionError::AbsenceProofRequired),
|
||||
],
|
||||
[
|
||||
Err(RecordTransitionError::StateRegression),
|
||||
Err(RecordTransitionError::StateRegression),
|
||||
Ok(()),
|
||||
Err(RecordTransitionError::AbsenceProofRequired),
|
||||
],
|
||||
[
|
||||
Err(RecordTransitionError::StateRegression),
|
||||
Err(RecordTransitionError::StateRegression),
|
||||
Err(RecordTransitionError::StateRegression),
|
||||
Ok(()),
|
||||
],
|
||||
];
|
||||
for (old_index, old) in states.into_iter().enumerate() {
|
||||
for (next_index, next) in states.into_iter().enumerate() {
|
||||
assert_eq!(
|
||||
old.validate_transition_to(next),
|
||||
expected[old_index][next_index]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let terminal_intent = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
owned_process(101),
|
||||
owned_process(102),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let terminal_running = terminal_snapshot(
|
||||
SessionTerminalState::Running,
|
||||
owned_process(101),
|
||||
owned_process(102),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let terminal_release = terminal_snapshot(
|
||||
SessionTerminalState::ReleaseIntent,
|
||||
owned_process(101),
|
||||
owned_process(102),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let terminal_clean = terminal_snapshot(
|
||||
SessionTerminalState::Clean,
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let terminal_states = [
|
||||
&terminal_intent,
|
||||
&terminal_running,
|
||||
&terminal_release,
|
||||
&terminal_clean,
|
||||
];
|
||||
for (old_index, old) in terminal_states.into_iter().enumerate() {
|
||||
for (next_index, next) in terminal_states.into_iter().enumerate() {
|
||||
assert_eq!(
|
||||
old.validate_transition_to(next),
|
||||
expected[old_index][next_index]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepared_lower_release_changes_only_the_lease_phase() {
|
||||
let bare = Participant::bare_intent(generation_id(), ParticipantRole::PreparedLower);
|
||||
let intent = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantState::Intent,
|
||||
Some(complete_ownership(
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantState::Intent,
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bare.validate_transition_to(&intent), Ok(()));
|
||||
let active = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantState::LeaseHeld,
|
||||
Some(complete_ownership(
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantState::LeaseHeld,
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(intent.validate_transition_to(&active), Ok(()));
|
||||
assert_eq!(
|
||||
bare.validate_transition_to(&active),
|
||||
Err(RecordTransitionError::PriorAcquisitionSnapshotRequired)
|
||||
);
|
||||
let release = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantState::ReleaseIntent,
|
||||
Some(release_ownership(ParticipantRole::PreparedLower)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(active.validate_transition_to(&release), Ok(()));
|
||||
|
||||
let ParticipantOwnership::PreparedLower(mut changed) =
|
||||
release_ownership(ParticipantRole::PreparedLower)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
changed.path = Some(HostPath::new(b"changed".to_vec()));
|
||||
let changed = Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantState::ReleaseIntent,
|
||||
Some(ParticipantOwnership::PreparedLower(changed)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
active.validate_transition_to(&changed),
|
||||
Err(RecordTransitionError::EvidenceChanged)
|
||||
);
|
||||
}
|
||||
559
crates/lazaret-core/tests/schema_records/process_evidence.rs
Normal file
559
crates/lazaret-core/tests/schema_records/process_evidence.rs
Normal file
|
|
@ -0,0 +1,559 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pidfs_authority_is_separate_from_locator_and_negative_evidence() {
|
||||
let maximum = PidfsProcessIdentityV1::new(
|
||||
PidfsProcessAuthorityV1 {
|
||||
boot_id: BootId::new([u8::MAX; 16]),
|
||||
pidfs_st_dev: PidfsDeviceId::new(u64::MAX),
|
||||
pidfs_st_ino: PidfsInodeId::new(u64::MAX),
|
||||
},
|
||||
HostPidLocator::new(u32::MAX),
|
||||
ProcessStartTimeTicks::new(u64::MAX),
|
||||
);
|
||||
assert_eq!(
|
||||
decode::<PidfsProcessIdentityV1>(&encode(&maximum).unwrap()).unwrap(),
|
||||
maximum
|
||||
);
|
||||
assert_eq!(maximum.authority().pidfs_st_dev.get(), u64::MAX);
|
||||
assert_eq!(maximum.authority().pidfs_st_ino.get(), u64::MAX);
|
||||
|
||||
let changed_locator = PidfsProcessIdentityV1::new(
|
||||
maximum.authority(),
|
||||
HostPidLocator::new(1),
|
||||
ProcessStartTimeTicks::new(2),
|
||||
);
|
||||
assert!(maximum.has_same_authority(&changed_locator));
|
||||
assert_ne!(maximum, changed_locator);
|
||||
|
||||
for unequal_authority in [
|
||||
PidfsProcessAuthorityV1 {
|
||||
boot_id: BootId::new([0; 16]),
|
||||
..maximum.authority()
|
||||
},
|
||||
PidfsProcessAuthorityV1 {
|
||||
pidfs_st_dev: PidfsDeviceId::new(u64::MAX - 1),
|
||||
..maximum.authority()
|
||||
},
|
||||
PidfsProcessAuthorityV1 {
|
||||
pidfs_st_ino: PidfsInodeId::new(u64::MAX - 1),
|
||||
..maximum.authority()
|
||||
},
|
||||
] {
|
||||
let unequal = PidfsProcessIdentityV1::new(
|
||||
unequal_authority,
|
||||
maximum.host_pid(),
|
||||
maximum.start_time_ticks(),
|
||||
);
|
||||
assert!(!maximum.has_same_authority(&unequal));
|
||||
assert_ne!(
|
||||
maximum.authority().cmp(&unequal.authority()),
|
||||
std::cmp::Ordering::Equal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_slot_separates_cleanup_authority_from_serviceability() {
|
||||
let current = process(91);
|
||||
assert_eq!(
|
||||
PersistedHostProcessOwnership::committed(current)
|
||||
.serviceable_pidfs()
|
||||
.unwrap(),
|
||||
¤t
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedHostProcessOwnership::staged(current).serviceable_pidfs(),
|
||||
Err(ProcessOwnershipRefusal::Staged)
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedHostProcessOwnership::staged(current)
|
||||
.authoritative_pidfs()
|
||||
.unwrap(),
|
||||
¤t
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedHostProcessOwnership::unassigned().authoritative_pidfs(),
|
||||
Err(ProcessAuthorityRefusal::Unassigned)
|
||||
);
|
||||
|
||||
let legacy = LegacyUnverifiedProcessIdentityV1::new(
|
||||
HostPidLocator::new(91),
|
||||
ProcessStartTimeTicks::new(910),
|
||||
);
|
||||
assert_eq!(
|
||||
HostProcessIdentity::legacy_unverified_v1(legacy).verified_pidfs(),
|
||||
Err(LegacyProcessIdentityRefusal)
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedHostProcessOwnership::legacy_unverified(legacy).serviceable_pidfs(),
|
||||
Err(ProcessOwnershipRefusal::LegacyUnverified)
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedHostProcessOwnership::legacy_unverified(legacy).authoritative_pidfs(),
|
||||
Err(ProcessAuthorityRefusal::LegacyUnverified)
|
||||
);
|
||||
|
||||
let staged_anchor = ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
PersistedHostProcessOwnership::staged(current),
|
||||
Some(mount()),
|
||||
));
|
||||
assert_eq!(
|
||||
Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Prepared,
|
||||
Some(staged_anchor.clone()),
|
||||
),
|
||||
Err(RecordInvariantError::ParticipantSnapshotState)
|
||||
);
|
||||
assert!(matches!(
|
||||
decode::<Participant>(
|
||||
&encode(&RawParticipant {
|
||||
generation_id: generation_id(),
|
||||
role: ParticipantRole::StateAnchor,
|
||||
state: ParticipantState::Prepared,
|
||||
ownership: Some(staged_anchor),
|
||||
})
|
||||
.unwrap()
|
||||
),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
SessionTerminal::snapshot(SessionTerminalFields {
|
||||
generation_id: generation_id(),
|
||||
session_id: session_id(),
|
||||
state: SessionTerminalState::Running,
|
||||
container_id: Some(ContainerId::new(opaque(9))),
|
||||
host_coordinator: owned_process(10),
|
||||
pty_master: Some(pty()),
|
||||
exec_client: PersistedHostProcessOwnership::staged(current),
|
||||
guest_process: Some(guest_process()),
|
||||
}),
|
||||
Err(RecordInvariantError::SessionTerminalSnapshotState)
|
||||
);
|
||||
assert!(matches!(
|
||||
decode::<SessionTerminal>(
|
||||
&encode(&RawSessionTerminal {
|
||||
generation_id: generation_id(),
|
||||
session_id: session_id(),
|
||||
state: SessionTerminalState::Running,
|
||||
container_id: Some(ContainerId::new(opaque(9))),
|
||||
host_coordinator: owned_process(10),
|
||||
pty_master: Some(pty()),
|
||||
exec_client: PersistedHostProcessOwnership::staged(current),
|
||||
guest_process: Some(guest_process()),
|
||||
})
|
||||
.unwrap()
|
||||
),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn participant_snapshot_matrix_is_phase_complete_and_strictly_decoded() {
|
||||
let roles = [
|
||||
ParticipantRole::PreparedLower,
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantRole::GitRouting,
|
||||
ParticipantRole::Network,
|
||||
ParticipantRole::Helper,
|
||||
ParticipantRole::Sandbox,
|
||||
ParticipantRole::Publication,
|
||||
];
|
||||
let active_states = [
|
||||
ParticipantState::LeaseHeld,
|
||||
ParticipantState::Prepared,
|
||||
ParticipantState::Committed,
|
||||
];
|
||||
|
||||
for role in roles {
|
||||
assert_eq!(
|
||||
Participant::bare_intent(generation_id(), role).ownership(),
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
Participant::snapshot(
|
||||
generation_id(),
|
||||
role,
|
||||
ParticipantState::Intent,
|
||||
Some(complete_ownership(role, ParticipantState::Intent)),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
Participant::snapshot(
|
||||
generation_id(),
|
||||
role,
|
||||
ParticipantState::ReleaseIntent,
|
||||
Some(release_ownership(role)),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
Participant::snapshot(
|
||||
generation_id(),
|
||||
role,
|
||||
ParticipantState::Clean,
|
||||
Some(ParticipantOwnership::empty(role)),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
Participant::snapshot(
|
||||
generation_id(),
|
||||
role,
|
||||
ParticipantState::Intent,
|
||||
Some(ParticipantOwnership::empty(role)),
|
||||
),
|
||||
Err(RecordInvariantError::ParticipantSnapshotState)
|
||||
);
|
||||
|
||||
for state in active_states {
|
||||
let candidate = Participant::snapshot(
|
||||
generation_id(),
|
||||
role,
|
||||
state,
|
||||
Some(complete_ownership(role, state)),
|
||||
);
|
||||
assert_eq!(candidate.is_ok(), state == active_state(role));
|
||||
}
|
||||
for state in [
|
||||
ParticipantState::LeaseHeld,
|
||||
ParticipantState::Prepared,
|
||||
ParticipantState::Committed,
|
||||
ParticipantState::ReleaseIntent,
|
||||
ParticipantState::Clean,
|
||||
] {
|
||||
assert_eq!(
|
||||
Participant::snapshot(generation_id(), role, state, None),
|
||||
Err(RecordInvariantError::ParticipantSnapshotState)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for raw in [
|
||||
RawParticipant {
|
||||
generation_id: generation_id(),
|
||||
role: ParticipantRole::StateAnchor,
|
||||
state: ParticipantState::Intent,
|
||||
ownership: Some(ParticipantOwnership::empty(ParticipantRole::StateAnchor)),
|
||||
},
|
||||
RawParticipant {
|
||||
generation_id: generation_id(),
|
||||
role: ParticipantRole::StateAnchor,
|
||||
state: ParticipantState::Prepared,
|
||||
ownership: None,
|
||||
},
|
||||
RawParticipant {
|
||||
generation_id: generation_id(),
|
||||
role: ParticipantRole::Network,
|
||||
state: ParticipantState::Prepared,
|
||||
ownership: Some(complete_ownership(
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Prepared,
|
||||
)),
|
||||
},
|
||||
] {
|
||||
assert!(matches!(
|
||||
decode::<Participant>(&encode(&raw).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_process_owners_require_complete_current_pidfs_evidence() {
|
||||
let current = process(94);
|
||||
let legacy = LegacyUnverifiedProcessIdentityV1::new(
|
||||
HostPidLocator::new(94),
|
||||
ProcessStartTimeTicks::new(940),
|
||||
);
|
||||
let invalid_slots = [
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
PersistedHostProcessOwnership::staged(current),
|
||||
PersistedHostProcessOwnership::legacy_unverified(legacy),
|
||||
];
|
||||
for daemon in invalid_slots {
|
||||
assert_eq!(
|
||||
Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Prepared,
|
||||
Some(ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
daemon,
|
||||
Some(mount()),
|
||||
))),
|
||||
),
|
||||
Err(RecordInvariantError::ParticipantSnapshotState)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::StateAnchor,
|
||||
ParticipantState::Prepared,
|
||||
Some(ParticipantOwnership::StateAnchor(state_anchor_ownership(
|
||||
owned_process(94),
|
||||
None,
|
||||
))),
|
||||
),
|
||||
Err(RecordInvariantError::ParticipantSnapshotState)
|
||||
);
|
||||
|
||||
let mut inconsistent_network = network_ownership();
|
||||
inconsistent_network.runtime_supervisor = owned_process(95);
|
||||
assert_eq!(
|
||||
Participant::snapshot(
|
||||
generation_id(),
|
||||
ParticipantRole::Network,
|
||||
ParticipantState::Committed,
|
||||
Some(ParticipantOwnership::Network(Box::new(
|
||||
inconsistent_network
|
||||
))),
|
||||
),
|
||||
Err(RecordInvariantError::ParticipantSnapshotState)
|
||||
);
|
||||
assert_eq!(
|
||||
Participant::snapshot(
|
||||
GenerationId::new(generation_id().get() + 1),
|
||||
ParticipantRole::Network,
|
||||
ParticipantState::Committed,
|
||||
Some(ParticipantOwnership::Network(Box::new(network_ownership()))),
|
||||
),
|
||||
Err(RecordInvariantError::ParticipantSnapshotState)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn active_network_rejects_every_mismatched_readiness_relation() {
|
||||
let other_domain = DomainId::new(opaque(80));
|
||||
let other_generation = GenerationId::new(81);
|
||||
let other_sidecar = NetworkSidecarId::new(opaque(82));
|
||||
let other_configuration = ConfigurationFingerprint::new(opaque(83));
|
||||
let other_policy = DerivedPolicyDigest::new(opaque(84));
|
||||
let other_fingerprint = ReadinessFingerprint::new(opaque(85));
|
||||
let other_namespace = NamespaceIdentity {
|
||||
device: DeviceId::new(86),
|
||||
inode: InodeId::new(87),
|
||||
};
|
||||
let mismatches = [
|
||||
(
|
||||
"enclosing generation",
|
||||
other_generation,
|
||||
network_ownership(),
|
||||
),
|
||||
(
|
||||
"readiness fingerprint",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership.readiness_fingerprint = Some(other_fingerprint);
|
||||
}),
|
||||
),
|
||||
(
|
||||
"attestation domain",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.attestation
|
||||
.domain_id = other_domain;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"readiness identity domain",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.readiness_identity
|
||||
.domain_id = other_domain;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"reachability domain",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.reachability_probe
|
||||
.domain_id = other_domain;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"attestation generation",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.attestation
|
||||
.generation_id = other_generation;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"readiness identity generation",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.readiness_identity
|
||||
.generation_id = other_generation;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"reachability generation",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.reachability_probe
|
||||
.generation_id = other_generation;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"attestation sidecar",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.attestation
|
||||
.network_sidecar_id = other_sidecar;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"readiness identity sidecar",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.readiness_identity
|
||||
.network_sidecar_id = other_sidecar;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"configuration fingerprint",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.readiness_identity
|
||||
.configuration_fingerprint = other_configuration;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"attestation policy",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.attestation
|
||||
.derived_policy_digest = other_policy;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"readiness identity policy",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.readiness_identity
|
||||
.derived_policy_digest = other_policy;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"attestation namespace",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.attestation
|
||||
.enforcement_namespace = other_namespace;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"readiness identity namespace",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.readiness_identity
|
||||
.enforcement_namespace = other_namespace;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"reachability namespace",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership
|
||||
.readiness_evidence
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.reachability_probe
|
||||
.enforcement_namespace = other_namespace;
|
||||
}),
|
||||
),
|
||||
(
|
||||
"runtime supervisor",
|
||||
generation_id(),
|
||||
mutate_network_ownership(|ownership| {
|
||||
ownership.runtime_supervisor = owned_process(88);
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, enclosing_generation, ownership) in mismatches {
|
||||
assert_eq!(
|
||||
Participant::snapshot(
|
||||
enclosing_generation,
|
||||
ParticipantRole::Network,
|
||||
ParticipantState::Committed,
|
||||
Some(ParticipantOwnership::Network(Box::new(ownership.clone()))),
|
||||
),
|
||||
Err(RecordInvariantError::ParticipantSnapshotState),
|
||||
"{name}"
|
||||
);
|
||||
let encoded = encode(&RawParticipant {
|
||||
generation_id: enclosing_generation,
|
||||
role: ParticipantRole::Network,
|
||||
state: ParticipantState::Committed,
|
||||
ownership: Some(ParticipantOwnership::Network(Box::new(ownership))),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(
|
||||
decode::<Participant>(&encoded),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
),
|
||||
"{name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
227
crates/lazaret-core/tests/schema_records/terminal.rs
Normal file
227
crates/lazaret-core/tests/schema_records/terminal.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn session_terminal_snapshot_and_transition_matrix_is_fail_closed() {
|
||||
let current_hc = process(101);
|
||||
let current_exec = process(102);
|
||||
let legacy = LegacyUnverifiedProcessIdentityV1::new(
|
||||
HostPidLocator::new(102),
|
||||
ProcessStartTimeTicks::new(1_020),
|
||||
);
|
||||
let bare = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
for invalid in [
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
PersistedHostProcessOwnership::staged(current_exec),
|
||||
PersistedHostProcessOwnership::legacy_unverified(legacy),
|
||||
] {
|
||||
assert_eq!(
|
||||
terminal_snapshot(
|
||||
SessionTerminalState::Running,
|
||||
PersistedHostProcessOwnership::committed(current_hc),
|
||||
invalid,
|
||||
true,
|
||||
),
|
||||
Err(RecordInvariantError::SessionTerminalSnapshotState)
|
||||
);
|
||||
}
|
||||
|
||||
let staged = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
PersistedHostProcessOwnership::staged(current_hc),
|
||||
PersistedHostProcessOwnership::staged(current_exec),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bare.validate_transition_to(&staged), Ok(()));
|
||||
let running = terminal_snapshot(
|
||||
SessionTerminalState::Running,
|
||||
PersistedHostProcessOwnership::committed(current_hc),
|
||||
PersistedHostProcessOwnership::committed(current_exec),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(staged.validate_transition_to(&running), Ok(()));
|
||||
|
||||
let direct_commit = terminal_snapshot(
|
||||
SessionTerminalState::Intent,
|
||||
PersistedHostProcessOwnership::committed(current_hc),
|
||||
PersistedHostProcessOwnership::committed(current_exec),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
bare.validate_transition_to(&direct_commit),
|
||||
Err(RecordTransitionError::ProcessStageSkipped)
|
||||
);
|
||||
|
||||
let release = terminal_snapshot(
|
||||
SessionTerminalState::ReleaseIntent,
|
||||
PersistedHostProcessOwnership::committed(current_hc),
|
||||
PersistedHostProcessOwnership::committed(current_exec),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(running.validate_transition_to(&release), Ok(()));
|
||||
let clean = terminal_snapshot(
|
||||
SessionTerminalState::Clean,
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
PersistedHostProcessOwnership::unassigned(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
release.validate_transition_to(&clean),
|
||||
Err(RecordTransitionError::AbsenceProofRequired)
|
||||
);
|
||||
assert_eq!(
|
||||
release.validate_transition_to_clean_after_absence(&clean),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
bare.validate_transition_to_clean_after_absence(&clean),
|
||||
Ok(())
|
||||
);
|
||||
assert!(clean.host_coordinator().is_unassigned());
|
||||
assert!(clean.exec_client().is_unassigned());
|
||||
assert_eq!(clean.container_id(), None);
|
||||
assert_eq!(clean.pty_master(), None);
|
||||
assert_eq!(clean.guest_process(), None);
|
||||
|
||||
let malformed_clean = RawSessionTerminal {
|
||||
generation_id: generation_id(),
|
||||
session_id: session_id(),
|
||||
state: SessionTerminalState::Clean,
|
||||
container_id: Some(ContainerId::new(opaque(29))),
|
||||
host_coordinator: PersistedHostProcessOwnership::unassigned(),
|
||||
pty_master: None,
|
||||
exec_client: PersistedHostProcessOwnership::unassigned(),
|
||||
guest_process: None,
|
||||
};
|
||||
assert!(matches!(
|
||||
decode::<SessionTerminal>(&encode(&malformed_clean).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn commit_acknowledges_only_the_exact_staged_acquisition() {
|
||||
let staged = StagedProcessOwnership::v1(
|
||||
domain_id(),
|
||||
generation_id(),
|
||||
coordinator_id(),
|
||||
HostProcessOwnershipSlot::HelperConnectivityHelper,
|
||||
process(92),
|
||||
);
|
||||
let acknowledgment = ProcessOwnershipCommitAck::v1(staged);
|
||||
assert!(acknowledgment.acknowledges(&staged));
|
||||
|
||||
let identity = staged.identity();
|
||||
let same_authority_different_locator = PidfsProcessIdentityV1::new(
|
||||
identity.authority(),
|
||||
HostPidLocator::new(identity.host_pid().get() + 1),
|
||||
identity.start_time_ticks(),
|
||||
);
|
||||
assert!(identity.has_same_authority(&same_authority_different_locator));
|
||||
|
||||
let mismatches = [
|
||||
StagedProcessOwnership::v1(
|
||||
DomainId::new(opaque(99)),
|
||||
staged.generation_id(),
|
||||
staged.coordinator_id(),
|
||||
staged.slot(),
|
||||
identity,
|
||||
),
|
||||
StagedProcessOwnership::v1(
|
||||
staged.domain_id(),
|
||||
GenerationId::new(staged.generation_id().get() + 1),
|
||||
staged.coordinator_id(),
|
||||
staged.slot(),
|
||||
identity,
|
||||
),
|
||||
StagedProcessOwnership::v1(
|
||||
staged.domain_id(),
|
||||
staged.generation_id(),
|
||||
CoordinatorId::new(opaque(98)),
|
||||
staged.slot(),
|
||||
identity,
|
||||
),
|
||||
StagedProcessOwnership::v1(
|
||||
staged.domain_id(),
|
||||
staged.generation_id(),
|
||||
staged.coordinator_id(),
|
||||
HostProcessOwnershipSlot::HelperRuntimeSupervisor,
|
||||
identity,
|
||||
),
|
||||
StagedProcessOwnership::v1(
|
||||
staged.domain_id(),
|
||||
staged.generation_id(),
|
||||
staged.coordinator_id(),
|
||||
staged.slot(),
|
||||
PidfsProcessIdentityV1::new(
|
||||
PidfsProcessAuthorityV1 {
|
||||
boot_id: BootId::new([0x43; 16]),
|
||||
..identity.authority()
|
||||
},
|
||||
identity.host_pid(),
|
||||
identity.start_time_ticks(),
|
||||
),
|
||||
),
|
||||
StagedProcessOwnership::v1(
|
||||
staged.domain_id(),
|
||||
staged.generation_id(),
|
||||
staged.coordinator_id(),
|
||||
staged.slot(),
|
||||
PidfsProcessIdentityV1::new(
|
||||
PidfsProcessAuthorityV1 {
|
||||
pidfs_st_dev: PidfsDeviceId::new(identity.authority().pidfs_st_dev.get() + 1),
|
||||
..identity.authority()
|
||||
},
|
||||
identity.host_pid(),
|
||||
identity.start_time_ticks(),
|
||||
),
|
||||
),
|
||||
StagedProcessOwnership::v1(
|
||||
staged.domain_id(),
|
||||
staged.generation_id(),
|
||||
staged.coordinator_id(),
|
||||
staged.slot(),
|
||||
PidfsProcessIdentityV1::new(
|
||||
PidfsProcessAuthorityV1 {
|
||||
pidfs_st_ino: PidfsInodeId::new(identity.authority().pidfs_st_ino.get() + 1),
|
||||
..identity.authority()
|
||||
},
|
||||
identity.host_pid(),
|
||||
identity.start_time_ticks(),
|
||||
),
|
||||
),
|
||||
StagedProcessOwnership::v1(
|
||||
staged.domain_id(),
|
||||
staged.generation_id(),
|
||||
staged.coordinator_id(),
|
||||
staged.slot(),
|
||||
same_authority_different_locator,
|
||||
),
|
||||
StagedProcessOwnership::v1(
|
||||
staged.domain_id(),
|
||||
staged.generation_id(),
|
||||
staged.coordinator_id(),
|
||||
staged.slot(),
|
||||
PidfsProcessIdentityV1::new(
|
||||
identity.authority(),
|
||||
identity.host_pid(),
|
||||
ProcessStartTimeTicks::new(identity.start_time_ticks().get() + 1),
|
||||
),
|
||||
),
|
||||
];
|
||||
for mismatch in mismatches {
|
||||
assert!(!acknowledgment.acknowledges(&mismatch));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue