Add typed schema records and strict version checks
This commit is contained in:
parent
b438dd5201
commit
16defc768a
10 changed files with 3164 additions and 1 deletions
122
Cargo.lock
generated
122
Cargo.lock
generated
|
|
@ -2,6 +2,27 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cobs"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-io"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-io"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazaret-client"
|
name = "lazaret-client"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|
@ -12,6 +33,10 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazaret-core"
|
name = "lazaret-core"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"postcard",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazaret-escalation-client"
|
name = "lazaret-escalation-client"
|
||||||
|
|
@ -40,3 +65,100 @@ version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazaret-core",
|
"lazaret-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "postcard"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||||
|
dependencies = [
|
||||||
|
"cobs",
|
||||||
|
"embedded-io 0.4.0",
|
||||||
|
"embedded-io 0.6.1",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ edition = "2024"
|
||||||
rust-version = "1.90"
|
rust-version = "1.90"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
postcard = { version = "=1.1.3", default-features = false, features = ["use-std"] }
|
||||||
|
serde = { version = "=1.0.228", features = ["derive"] }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
unused = { level = "deny", priority = -1 }
|
unused = { level = "deny", priority = -1 }
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,9 @@ edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
postcard.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
85
crates/lazaret-core/src/codec.rs
Normal file
85
crates/lazaret-core/src/codec.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
//! Strict bounded Postcard structure checks shared by schema-record persistence and tests.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
pub const MAX_SCHEMA_RECORD_BYTES: usize = 1024 * 1024;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CodecError {
|
||||||
|
RecordTooLarge { length: usize, maximum: usize },
|
||||||
|
TrailingPayload,
|
||||||
|
NonMinimalPayload,
|
||||||
|
MalformedPayload(postcard::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CodecError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::RecordTooLarge { length, maximum } => {
|
||||||
|
write!(formatter, "schema record length {length} exceeds {maximum}")
|
||||||
|
}
|
||||||
|
Self::TrailingPayload => {
|
||||||
|
formatter.write_str("schema record has trailing payload bytes")
|
||||||
|
}
|
||||||
|
Self::NonMinimalPayload => formatter
|
||||||
|
.write_str("schema record does not use the minimal Postcard representation"),
|
||||||
|
Self::MalformedPayload(error) => write!(formatter, "malformed schema payload: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CodecError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::MalformedPayload(error) => Some(error),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes one schema value using Postcard's minimal representation.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when serialization fails or the encoded value exceeds the generic limit.
|
||||||
|
pub fn encode<T: Serialize>(record: &T) -> Result<Vec<u8>, CodecError> {
|
||||||
|
let encoded = postcard::to_stdvec(record).map_err(CodecError::MalformedPayload)?;
|
||||||
|
if encoded.len() > MAX_SCHEMA_RECORD_BYTES {
|
||||||
|
return Err(CodecError::RecordTooLarge {
|
||||||
|
length: encoded.len(),
|
||||||
|
maximum: MAX_SCHEMA_RECORD_BYTES,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes one complete, minimally encoded schema value after enforcing the generic size limit.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error for oversized, malformed, trailing, or non-minimal Postcard input.
|
||||||
|
pub fn decode<T>(encoded: &[u8]) -> Result<T, CodecError>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Serialize,
|
||||||
|
{
|
||||||
|
if encoded.len() > MAX_SCHEMA_RECORD_BYTES {
|
||||||
|
return Err(CodecError::RecordTooLarge {
|
||||||
|
length: encoded.len(),
|
||||||
|
maximum: MAX_SCHEMA_RECORD_BYTES,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (record, remainder) =
|
||||||
|
postcard::take_from_bytes(encoded).map_err(CodecError::MalformedPayload)?;
|
||||||
|
if !remainder.is_empty() {
|
||||||
|
return Err(CodecError::TrailingPayload);
|
||||||
|
}
|
||||||
|
let minimal = postcard::to_stdvec(&record).map_err(CodecError::MalformedPayload)?;
|
||||||
|
if minimal != encoded {
|
||||||
|
return Err(CodecError::NonMinimalPayload);
|
||||||
|
}
|
||||||
|
Ok(record)
|
||||||
|
}
|
||||||
191
crates/lazaret-core/src/identity.rs
Normal file
191
crates/lazaret-core/src/identity.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub const OPAQUE_ID_BYTES: usize = 32;
|
||||||
|
pub const SHA256_BYTES: usize = 32;
|
||||||
|
|
||||||
|
macro_rules! opaque_identity {
|
||||||
|
($($name:ident),+ $(,)?) => {
|
||||||
|
$(
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
|
pub struct $name([u8; OPAQUE_ID_BYTES]);
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(bytes: [u8; OPAQUE_ID_BYTES]) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_bytes(&self) -> &[u8; OPAQUE_ID_BYTES] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! digest_identity {
|
||||||
|
($($name:ident),+ $(,)?) => {
|
||||||
|
$(
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
|
pub struct $name([u8; SHA256_BYTES]);
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(bytes: [u8; SHA256_BYTES]) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_bytes(&self) -> &[u8; SHA256_BYTES] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! numeric_identity {
|
||||||
|
($($name:ident($inner:ty)),+ $(,)?) => {
|
||||||
|
$(
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
|
pub struct $name($inner);
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(value: $inner) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn get(self) -> $inner {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
opaque_identity!(
|
||||||
|
DomainId,
|
||||||
|
CoordinatorId,
|
||||||
|
NetworkSidecarId,
|
||||||
|
SandboxId,
|
||||||
|
ClientId,
|
||||||
|
SessionId,
|
||||||
|
SessionAnchorId,
|
||||||
|
RequestId,
|
||||||
|
PreparedArtifactId,
|
||||||
|
PrepareOperationId,
|
||||||
|
ProtocolCredentialId,
|
||||||
|
CapabilityId,
|
||||||
|
ContainerId,
|
||||||
|
PolicyId,
|
||||||
|
);
|
||||||
|
|
||||||
|
digest_identity!(
|
||||||
|
ConfigurationFingerprint,
|
||||||
|
DerivedPolicyDigest,
|
||||||
|
ReadinessFingerprint,
|
||||||
|
BundleDigest,
|
||||||
|
SelectedManifestDigest,
|
||||||
|
IdMapDigest,
|
||||||
|
CertificateDigest,
|
||||||
|
TrustManifestDigest,
|
||||||
|
RequestDigest,
|
||||||
|
StreamDigest,
|
||||||
|
);
|
||||||
|
|
||||||
|
numeric_identity!(
|
||||||
|
GenerationId(u64),
|
||||||
|
ProcessId(u32),
|
||||||
|
ProcessStartTime(u64),
|
||||||
|
GuestProcessGroupId(u32),
|
||||||
|
FileDescriptor(i32),
|
||||||
|
DeviceId(u64),
|
||||||
|
InodeId(u64),
|
||||||
|
MountId(u64),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
|
pub struct HostPath(Vec<u8>);
|
||||||
|
|
||||||
|
impl HostPath {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(bytes: Vec<u8>) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
|
pub struct SourceIdentity(Vec<u8>);
|
||||||
|
|
||||||
|
impl SourceIdentity {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(bytes: Vec<u8>) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub struct ProcessIdentity {
|
||||||
|
pub pid: ProcessId,
|
||||||
|
pub start_time: ProcessStartTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub struct NamespaceIdentity {
|
||||||
|
pub device: DeviceId,
|
||||||
|
pub inode: InodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub struct MountIdentity {
|
||||||
|
pub mount_id: MountId,
|
||||||
|
pub device: DeviceId,
|
||||||
|
pub inode: InodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::any::TypeId;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
ContainerId, CoordinatorId, DomainId, GenerationId, MountIdentity, NamespaceIdentity,
|
||||||
|
NetworkSidecarId, PreparedArtifactId, ProcessIdentity, RequestId, SandboxId, SessionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_categories_are_nominally_distinct() {
|
||||||
|
let identities = [
|
||||||
|
TypeId::of::<DomainId>(),
|
||||||
|
TypeId::of::<GenerationId>(),
|
||||||
|
TypeId::of::<CoordinatorId>(),
|
||||||
|
TypeId::of::<NetworkSidecarId>(),
|
||||||
|
TypeId::of::<SandboxId>(),
|
||||||
|
TypeId::of::<SessionId>(),
|
||||||
|
TypeId::of::<RequestId>(),
|
||||||
|
TypeId::of::<PreparedArtifactId>(),
|
||||||
|
TypeId::of::<ContainerId>(),
|
||||||
|
TypeId::of::<ProcessIdentity>(),
|
||||||
|
TypeId::of::<NamespaceIdentity>(),
|
||||||
|
TypeId::of::<MountIdentity>(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
identities.into_iter().collect::<HashSet<_>>().len(),
|
||||||
|
identities.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
#![doc = "Shared production interfaces for Lazaret processes."]
|
#![doc = "Shared production interfaces for Lazaret processes."]
|
||||||
|
|
||||||
|
pub mod codec;
|
||||||
|
pub mod identity;
|
||||||
|
pub mod records;
|
||||||
|
|
||||||
pub const PROTOCOL_NAME: &str = "lazaret";
|
pub const PROTOCOL_NAME: &str = "lazaret";
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
1493
crates/lazaret-core/src/records.rs
Normal file
1493
crates/lazaret-core/src/records.rs
Normal file
File diff suppressed because it is too large
Load diff
1247
crates/lazaret-core/tests/schema_records.rs
Normal file
1247
crates/lazaret-core/tests/schema_records.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -768,3 +768,16 @@ identity with a schema-specific domain separator. Process identities and network
|
||||||
observations are attestation evidence but are not part of that fingerprint. The
|
observations are attestation evidence but are not part of that fingerprint. The
|
||||||
Host Coordinator binds the accepted attestation and generation-bound reachability
|
Host Coordinator binds the accepted attestation and generation-bound reachability
|
||||||
probe into Network Readiness Evidence before publication.
|
probe into Network Readiness Evidence before publication.
|
||||||
|
|
||||||
|
`NetworkReadinessAttestation.schema_id` is exactly
|
||||||
|
`lazaret-network-attestation-v1`.
|
||||||
|
`NetworkReadinessIdentity.schema_id` is exactly
|
||||||
|
`lazaret-network-readiness-identity-v1`. They are independent version-dispatch
|
||||||
|
domains: the v1 attestation projects only to the v1 readiness identity, and any
|
||||||
|
other identifier or version is refused without fallback or cross-domain
|
||||||
|
interpretation.
|
||||||
|
|
||||||
|
The readiness-fingerprint domain separator is the UTF-8 bytes of
|
||||||
|
`lazaret-network-readiness-identity-v1` followed by exactly one zero byte. This
|
||||||
|
fixes the domain-separation context only; it does not select the byte encoding of
|
||||||
|
the canonical readiness-identity fields.
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ kind = "invariant"
|
||||||
symbols = ["TypeOK", "DomainGenerationBounds", "RuntimeRoleIdentitySeparation", "GenerationScopedIdentitiesDistinct"]
|
symbols = ["TypeOK", "DomainGenerationBounds", "RuntimeRoleIdentitySeparation", "GenerationScopedIdentitiesDistinct"]
|
||||||
owners = ["IP-01", "IP-10", "IP-14", "IP-15", "IP-30"]
|
owners = ["IP-01", "IP-10", "IP-14", "IP-15", "IP-30"]
|
||||||
levels = ["V1"]
|
levels = ["V1"]
|
||||||
verifier = "Type/property tests and lifecycle state assertions"
|
verifier = "IP-01 strict schema-codec, malformed-version, record-invariant, and nominal identity tests plus later lifecycle state assertions"
|
||||||
|
|
||||||
[[coverage]]
|
[[coverage]]
|
||||||
model = "Lazaret"
|
model = "Lazaret"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue