Retire the mutable design schema
Make Rust records and fixed codec tests the implementation-shape owner, remove the Mermaid category registry, and state the ownership boundary in the authoritative specification.
This commit is contained in:
parent
a0055f67f1
commit
03bfe92ca0
5 changed files with 17 additions and 1679 deletions
|
|
@ -31,18 +31,14 @@ pub trait SchemaType {
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! schema_types {
|
macro_rules! schema_types {
|
||||||
($($name:literal => $record:ty),+; $($declared:literal),* $(,)?) => {
|
($($name:literal => $record:ty),+ $(,)?) => {
|
||||||
$(
|
$(
|
||||||
impl SchemaType for $record {
|
impl SchemaType for $record {
|
||||||
const NAME: &'static str = $name;
|
const NAME: &'static str = $name;
|
||||||
}
|
}
|
||||||
)+
|
)+
|
||||||
|
|
||||||
pub const IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES: &[&str] = &[$($name),+];
|
pub const SERIALIZED_RECORD_TYPE_NAMES: &[&str] = &[$($name),+];
|
||||||
pub const DECLARED_SERIALIZED_SCHEMA_TYPE_NAMES: &[&str] = &[
|
|
||||||
$($name),+,
|
|
||||||
$($declared),*
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,13 +145,5 @@ schema_types!(
|
||||||
"ResolvedNetworkReadinessV1" => ResolvedNetworkReadinessV1,
|
"ResolvedNetworkReadinessV1" => ResolvedNetworkReadinessV1,
|
||||||
"DomainStore" => DomainStore,
|
"DomainStore" => DomainStore,
|
||||||
"StorageCatalog" => StorageCatalog,
|
"StorageCatalog" => StorageCatalog,
|
||||||
"PrepareStageState" => PrepareStageState;
|
"PrepareStageState" => PrepareStageState,
|
||||||
"StateResetIntentV1",
|
|
||||||
"StateResetScopeV1",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
pub const DECLARED_RUNTIME_SCHEMA_TYPE_NAMES: &[&str] = &[
|
|
||||||
"ResolvedDomainConfiguration",
|
|
||||||
"SandboxLayoutV1",
|
|
||||||
"ConfinedPathPlan",
|
|
||||||
];
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::num::NonZeroU16;
|
use std::num::NonZeroU16;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -670,165 +670,14 @@ image = "example.invalid/agent:v1"
|
||||||
&mut seen,
|
&mut seen,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(seen.len(), IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES.len());
|
assert_eq!(seen.len(), SERIALIZED_RECORD_TYPE_NAMES.len());
|
||||||
assert!(
|
assert!(
|
||||||
IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES
|
SERIALIZED_RECORD_TYPE_NAMES
|
||||||
.iter()
|
.iter()
|
||||||
.all(|name| seen.contains(name))
|
.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)
|
fn assert_unknown_protocol_version<T>(record: &T)
|
||||||
where
|
where
|
||||||
T: DeserializeOwned + Serialize,
|
T: DeserializeOwned + Serialize,
|
||||||
|
|
|
||||||
1499
docs/schema.md
1499
docs/schema.md
File diff suppressed because it is too large
Load diff
|
|
@ -184,8 +184,8 @@ initiated by a host Lazaret CLI invocation, not by a resident scheduler.
|
||||||
|
|
||||||
### 3.2 Trusted Host Configuration Refinement
|
### 3.2 Trusted Host Configuration Refinement
|
||||||
|
|
||||||
The non-authoritative repository [design schemas](schema.md) mirror the closed
|
Rust configuration records and their codec tests own the closed v1 document
|
||||||
v1 trusted-host configuration described by this refinement. Lazaret chooses its
|
shape described by this refinement. Lazaret chooses its
|
||||||
configuration file from an explicit host CLI option or, when absent, from
|
configuration file from an explicit host CLI option or, when absent, from
|
||||||
`$XDG_CONFIG_HOME/lazaret/config.toml`, with
|
`$XDG_CONFIG_HOME/lazaret/config.toml`, with
|
||||||
`$HOME/.config/lazaret/config.toml` as the XDG fallback. It derives the Storage
|
`$HOME/.config/lazaret/config.toml` as the XDG fallback. It derives the Storage
|
||||||
|
|
@ -562,7 +562,7 @@ Disconnected Client Sessions close rather than reattach to an unbounded scrollba
|
||||||
owned by the coordinator.
|
owned by the coordinator.
|
||||||
|
|
||||||
The session connection carries the terminal events and messages described by
|
The session connection carries the terminal events and messages described by
|
||||||
this refinement. The non-authoritative repository schema mirrors their v1
|
this refinement. Rust protocol records and fixed codec tests own their v1
|
||||||
encoding. One Host Coordinator event loop serializes input, resize, signal,
|
encoding. One Host Coordinator event loop serializes input, resize, signal,
|
||||||
close, prompt-routing, PTY output, and exec-client completion for each Session
|
close, prompt-routing, PTY output, and exec-client completion for each Session
|
||||||
ID. It uses bounded queues and PTY
|
ID. It uses bounded queues and PTY
|
||||||
|
|
@ -818,10 +818,10 @@ before network ownership records readiness. Sessions and the generation mailbox
|
||||||
are separate generation records. Deterministic paths are lookup hints for
|
are separate generation records. Deterministic paths are lookup hints for
|
||||||
physical recovery, not ownership proofs.
|
physical recovery, not ownership proofs.
|
||||||
|
|
||||||
The non-authoritative repository [design schemas](schema.md) mirror the logical
|
Rust durable records and their codec tests own the store row shapes described
|
||||||
records and relationships described by these refinements. Implementations must
|
by these refinements. Implementations must preserve the distinct Domain Store
|
||||||
preserve the distinct Domain Store and Storage Catalog authority boundaries and
|
and Storage Catalog authority boundaries and the independently recoverable
|
||||||
the independently recoverable ownership described above.
|
ownership described above.
|
||||||
|
|
||||||
Lazaret v1 stores the catalog in redb. Each catalog operation holds the
|
Lazaret v1 stores the catalog in redb. Each catalog operation holds the
|
||||||
host-wide Storage Catalog lock for one bounded open, transaction, and close
|
host-wide Storage Catalog lock for one bounded open, transaction, and close
|
||||||
|
|
@ -915,9 +915,9 @@ and publication gates follow the protocol model.
|
||||||
|
|
||||||
#### Derived policy and readiness encoding
|
#### Derived policy and readiness encoding
|
||||||
|
|
||||||
The non-authoritative repository [design schemas](schema.md) mirror the v1
|
Rust network records and fixed codec tests own the v1 policy, attestation,
|
||||||
policy, attestation, readiness-identity projection, and fingerprint inputs
|
readiness-identity projection, and fingerprint encodings described by this
|
||||||
described by this refinement.
|
refinement.
|
||||||
|
|
||||||
The v1 policy transport passes canonical bytes through a write-sealed inherited
|
The v1 policy transport passes canonical bytes through a write-sealed inherited
|
||||||
memfd. The Runtime Supervisor verifies the seals and digest, installs the policy,
|
memfd. The Runtime Supervisor verifies the seals and digest, installs the policy,
|
||||||
|
|
@ -970,7 +970,7 @@ handles. Host-only CLI/coordinator control may use host-private Unix sockets tha
|
||||||
are never mounted into the Agent Sandbox.
|
are never mounted into the Agent Sandbox.
|
||||||
|
|
||||||
Mailbox files carry the signed protocol records described by this refinement;
|
Mailbox files carry the signed protocol records described by this refinement;
|
||||||
the non-authoritative repository schema mirrors their v1 encoding. TLS is not
|
Rust protocol records and fixed codec tests own their v1 encoding. TLS is not
|
||||||
layered over the regular-file transport. HC opens each ready entry relative to a
|
layered over the regular-file transport. HC opens each ready entry relative to a
|
||||||
confined directory handle,
|
confined directory handle,
|
||||||
rejects links and non-regular files, bounds the complete file before allocation,
|
rejects links and non-regular files, bounds the complete file before allocation,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue