Split record modules without changing wire behavior
This commit is contained in:
parent
ccf23ccac1
commit
6301eeba3e
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