Integrate core record modularization

This commit is contained in:
ms2k 2026-07-15 02:16:30 +09:00
commit c1b40f923a
15 changed files with 5674 additions and 5591 deletions

File diff suppressed because it is too large Load diff

View 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>,
}

View 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>,
}

File diff suppressed because it is too large Load diff

View 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
}
}

View 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",
];

View 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>,
}

View 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

View 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(_))
));
}

View 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]);
}

View 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(_))
));
}

View file

@ -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(),
&current
);
}
#[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)
);
}

View 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(),
&current
);
assert_eq!(
PersistedHostProcessOwnership::staged(current).serviceable_pidfs(),
Err(ProcessOwnershipRefusal::Staged)
);
assert_eq!(
PersistedHostProcessOwnership::staged(current)
.authoritative_pidfs()
.unwrap(),
&current
);
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}"
);
}
}

View 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));
}
}