Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bf639b9
feat: plan racks, cabling and replacements
polaz Nov 1, 2026
3645079
fix(ui): expose site installation facts
polaz Nov 1, 2026
daa97d1
fix(store): validate allocated replacement space
polaz Nov 1, 2026
f9108f1
fix(store): account shared replacement loads
polaz Nov 1, 2026
119e7e3
fix(observe): report unavailable native sources
polaz Nov 1, 2026
20e74a1
fix(store): keep unpositioned holds unqualified
polaz Nov 1, 2026
e4c014b
fix(contracts): bound power path references
polaz Nov 1, 2026
c6b3601
fix(observe): label Windows identity parse errors
polaz Nov 1, 2026
593fa8a
test(observe): verify readable Windows tables
polaz Nov 1, 2026
d0a4ac1
test(store): complete ordered cable handoffs
polaz Nov 1, 2026
c664c73
test(store): preserve cable links during mapping
polaz Nov 1, 2026
905b4be
test(observe): reject unexpected native failures
polaz Nov 1, 2026
d57aa35
test(observe): reject failed table qualification
polaz Nov 1, 2026
cb9db19
fix(store): require meaningful physical evidence
polaz Nov 1, 2026
86daea1
fix(store): reject blank mounting profiles
polaz Nov 1, 2026
47522b6
fix(store): lock only reported physical work
polaz Nov 1, 2026
a183702
fix(store): preserve unstarted plan refreshes
polaz Nov 1, 2026
71fdd29
docs(contracts): describe postcondition refreshes
polaz Nov 1, 2026
5ac09d0
fix(store): reclaim corrected physical poses
polaz Nov 1, 2026
eac5b55
fix(web): separate cable save and reload errors
polaz Nov 1, 2026
0060422
fix(web): require a declared mating side
polaz Nov 1, 2026
80bde40
fix(web): bound converted equipment envelopes
polaz Nov 1, 2026
38286c5
fix(web): preserve required plan postconditions
polaz Nov 1, 2026
93a20d2
fix(web): reset replacement review navigation
polaz Nov 1, 2026
d411708
test(store): remove redundant completion defaults
polaz Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ including bounded quantities, model alternatives and supported exclusions,
then be refined without changing their identity.
The workspace can [check recorded requirements](docs/inventory.md#recorded-requirements)
against permitted current facts, preserving missing, stale and conflicting evidence.
It includes [physical planning](docs/inventory.md#physical-plans-and-cable-ends) with exact
rack elevations, cable ends, material requirements, reservations and reviewed
component replacement. Manual results remain separate from verified service health.

## License

Expand Down
32 changes: 32 additions & 0 deletions crates/omloom-contracts/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ fn main() -> Result<(), Box<dyn Error>> {
.boxed(".omloom.workspace.v1.Reply.result.detail")
.boxed(".omloom.workspace.v1.Reply.result.vm_placement_review")
.boxed(".omloom.workspace.v1.HostImport.report")
// Keep the request stack bound; allocate this volume only for an explicit coverage assertion.
.boxed(".omloom.workspace.v1.PhysicalDefinition.occupancy_coverage")
.boxed(".omloom.workspace.v1.PhysicalDefinition.rack_grid")
.boxed(".omloom.workspace.v1.PhysicalDefinition.clearance")
.boxed(".omloom.workspace.v1.PhysicalMaterial.physical")
.boxed(".omloom.workspace.v1.PhysicalReplacement.feedback")
.file_descriptor_set_path(&descriptor)
.compile_protos(&[workspace], &["../../proto"])?;
let set = FileDescriptorSet::decode(fs::read(descriptor)?.as_slice())?;
Expand Down Expand Up @@ -65,10 +71,36 @@ fn main() -> Result<(), Box<dyn Error>> {
&& matches!(field.name(), "candidates" | "excluded")
{
16
} else if message.name() == "PowerPathRequirement"
&& matches!(field.name(), "sources" | "boundaries")
{
8
} else if (message.name() == "EvaluateRequirements" && field.name() == "nodes")
|| (message.name() == "RequirementGroup" && field.name() == "members")
|| (message.name() == "PhysicalPlan" && field.name() == "steps")
|| (message.name() == "PhysicalView" && field.name() == "definitions")
|| (message.name() == "PhysicalCompletion"
&& matches!(field.name(), "completed_steps" | "not_performed_steps"))
|| (message.name() == "PhysicalPlan" && field.name() == "materials")
|| (message.name() == "PhysicalMaterial" && field.name() == "allocations")
|| (message.name() == "PhysicalReplacement" && field.name() == "endpoints")
|| (message.name() == "PhysicalEndpointMappings" && field.name() == "pairs")
|| (message.name() == "PhysicalCableEnd" && field.name() == "channels")
|| (message.name() == "PhysicalChannelSet" && field.name() == "channels")
|| (message.name() == "PhysicalDefinition" && field.name() == "channels")
{
64
} else if message.name() == "PhysicalMaterial" && field.name() == "specifications" {
12
} else if message.name() == "PhysicalView"
&& matches!(
field.name(),
"occupancy" | "plans" | "positions" | "replacement_quality"
)
{
25
} else if message.name() == "PhysicalView" && field.name() == "items" {
256
} else {
0
}
Expand Down
53 changes: 31 additions & 22 deletions crates/omloom-contracts/src/equipment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,29 +221,13 @@ fn identity(value: &Option<EquipmentReference>) -> (&str, &str, &str) {
None => ("", "", ""),
}
}
/// One bounded semantic encoding for retries, changed-only storage and CSV.
/// Candidate/exclusion order is irrelevant; source and interpretation basis are not.
pub fn encode(value: &EquipmentClaim) -> Result<Vec<u8>, Error> {
/// Normalize an owned bounded claim for changed-only storage and returned plans.
pub fn canonicalize(value: &mut EquipmentClaim) -> Result<(), Error> {
validate(value)?;
let canonical = match &value.assertion {
Some(equipment_claim::Assertion::Quantity(q)) => {
q.scale == 0
|| q.lower.is_some_and(|n| n % 10 != 0)
|| q.upper.is_some_and(|n| n % 10 != 0)
}
Some(equipment_claim::Assertion::Alternatives(v)) => {
v.candidates.is_sorted_by(|a, b| {
(&a.value, identity(&a.reference)) <= (&b.value, identity(&b.reference))
}) && v.excluded.is_sorted_by(|a, b| {
(&a.value, identity(&a.reference)) <= (&b.value, identity(&b.reference))
})
}
_ => true,
};
if canonical {
return Ok(value.encode_to_vec());
}
let mut value = value.clone();
canonicalize_validated(value);
Ok(())
}
fn canonicalize_validated(value: &mut EquipmentClaim) {
if let Some(equipment_claim::Assertion::Quantity(q)) = &mut value.assertion {
while q.scale > 0
&& q.lower.is_none_or(|n| n % 10 == 0)
Expand All @@ -266,6 +250,31 @@ pub fn encode(value: &EquipmentClaim) -> Result<Vec<u8>, Error> {
(&a.value, identity(&a.reference)).cmp(&(&b.value, identity(&b.reference)))
});
}
}
/// One bounded semantic encoding for retries, changed-only storage and CSV.
/// Candidate/exclusion order is irrelevant; source and interpretation basis are not.
pub fn encode(value: &EquipmentClaim) -> Result<Vec<u8>, Error> {
validate(value)?;
let canonical = match &value.assertion {
Some(equipment_claim::Assertion::Quantity(q)) => {
q.scale == 0
|| q.lower.is_some_and(|n| n % 10 != 0)
|| q.upper.is_some_and(|n| n % 10 != 0)
}
Some(equipment_claim::Assertion::Alternatives(v)) => {
v.candidates.is_sorted_by(|a, b| {
(&a.value, identity(&a.reference)) <= (&b.value, identity(&b.reference))
}) && v.excluded.is_sorted_by(|a, b| {
(&a.value, identity(&a.reference)) <= (&b.value, identity(&b.reference))
})
}
_ => true,
};
if canonical {
return Ok(value.encode_to_vec());
}
let mut value = value.clone();
canonicalize_validated(&mut value);
Ok(value.encode_to_vec())
}

Expand Down
24 changes: 24 additions & 0 deletions crates/omloom-contracts/src/equipment/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
use super::*;

// Owned plans and the persisted encoding must share normalization, including retry order and fixed-point spelling.
#[test]
fn owned_claim_canonicalization_matches_encoding_and_is_idempotent() {
let mut value = possible();
let encoded = encode(&value).expect("valid claim");
canonicalize(&mut value).expect("valid owned claim");
assert_eq!(value.encode_to_vec(), encoded);
let previous = value.clone();
canonicalize(&mut value).expect("already canonical claim");
assert_eq!(value, previous);
let mut quantity = EquipmentClaim {
basis: "Recorded physical measurement".into(),
assertion: Some(equipment_claim::Assertion::Quantity(QuantityRange {
lower: Some(1200),
upper: None,
scale: 2,
unit: "mm".into(),
})),
};
let encoded = encode(&quantity).expect("valid measurement");
canonicalize(&mut quantity).expect("owned measurement");
assert_eq!(quantity.encode_to_vec(), encoded);
}
fn possible() -> EquipmentClaim {
EquipmentClaim {
basis: "Front label and visible bays".into(),
Expand Down
1 change: 1 addition & 0 deletions crates/omloom-contracts/src/ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ id!(SourceId);
id!(AuthorityId);
id!(RelationId);
id!(OperationId);
id!(PlanId);
id!(ScanId);
id!(TrustId);
id!(CredentialBindingId);
Expand Down
85 changes: 85 additions & 0 deletions crates/omloom-contracts/tests/workspace.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,91 @@
use omloom_contracts::{Error, Limits, MemoryBudget, decode_workspace, workspace as api};
use prost::Message;

// The evaluator's eight-reference bound must reject oversized frames before decoded allocation.
#[test]
fn power_path_reference_counts_are_bounded_before_decode() {
for sources in [true, false] {
for count in [8, 9] {
let refs = vec![Default::default(); count];
let mut paths = api::PowerPathRequirement::default();
if sources {
paths.sources = refs;
} else {
paths.boundaries = refs;
}
let bytes = api::Request {
command: Some(api::request::Command::EvaluateRequirements(
api::EvaluateRequirements {
nodes: vec![api::RequirementNode {
expression: Some(api::requirement_node::Expression::PowerPaths(paths)),
}],
..Default::default()
},
)),
..Default::default()
}
.encode_to_vec();
let budget = MemoryBudget::new(16 * 1_048_576);
let decoded = decode_workspace(&bytes, Limits::default(), &budget);
if count == 9 {
assert!(matches!(decoded, Err(Error::Limit)));
} else {
assert!(decoded.is_ok());
}
drop(decoded);
assert_eq!(budget.used(), 0);
}
}
}

// Physical wire containers enforce repeated bounds before prost allocates their vectors.
#[test]
fn physical_channels_and_endpoint_maps_are_bounded_before_decode() {
let budget = MemoryBudget::new(16 * 1_048_576);
let channels = api::Request {
working: None,
command: Some(api::request::Command::Physical(api::Physical {
action: Some(api::physical::Action::Define(api::PhysicalDefinitionEdit {
definition: Some(api::PhysicalDefinition {
channels: vec![1; 65],
..Default::default()
}),
})),
})),
}
.encode_to_vec();
assert!(matches!(
decode_workspace(&channels, Limits::default(), &budget),
Err(Error::Limit)
));
assert_eq!(budget.used(), 0);
let mappings = api::Request {
working: None,
command: Some(api::request::Command::Physical(api::Physical {
action: Some(api::physical::Action::Save(api::PhysicalSave {
plan: Some(api::PhysicalPlan {
steps: vec![api::PhysicalStep {
action: Some(api::physical_step::Action::Replacement(
api::PhysicalReplacement {
endpoints: vec![Default::default(); 65],
..Default::default()
},
)),
..Default::default()
}],
..Default::default()
}),
})),
})),
}
.encode_to_vec();
assert!(matches!(
decode_workspace(&mappings, Limits::default(), &budget),
Err(Error::Limit)
));
assert_eq!(budget.used(), 0);
}

// Omitted wire input cannot implicitly request an exclusive allocation.
#[test]
fn network_sharing_requires_an_explicit_mechanism() {
Expand Down
2 changes: 1 addition & 1 deletion crates/omloom-observe/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ impl Runner {
};
let result = match timeout_at(deadline, work).await {
Ok(result) => result,
Err(_) => Err("Command deadline exceeded".into()),
Err(_) => Err(format!("Command deadline exceeded ({executable})")),
};
if result.is_err() && child.id().is_some() {
child
Expand Down
10 changes: 6 additions & 4 deletions crates/omloom-observe/src/command/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,11 @@ async fn deadline_terminates_the_owned_child() {
.await;
#[cfg(not(windows))]
let result = runner.run("/bin/sleep", &["5"]).await;
assert_eq!(
result.err().expect("deadline failure"),
"Command deadline exceeded"
);
let message = result.err().expect("deadline failure");
assert!(message.starts_with("Command deadline exceeded"));
#[cfg(windows)]
assert!(message.contains("cmd.exe"));
#[cfg(not(windows))]
assert!(message.contains("/bin/sleep"));
assert_eq!(runner.remaining, initial);
}
28 changes: 21 additions & 7 deletions crates/omloom-observe/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ fn add(report: &mut api::HostReport, value: api::HostFinding) -> Result<()> {

async fn collect() -> Result<api::HostReport> {
let mut runner = command::Runner::new();
let identity = native::identity(&mut runner).await?;
let identity = native::identity(&mut runner)
.await
.map_err(|error| format!("Initial host identity unavailable: {error}"))?;
let native::Identity {
platform,
source,
Expand Down Expand Up @@ -128,7 +130,9 @@ async fn collect() -> Result<api::HostReport> {
});
native::collect(&mut runner, &mut report).await?;
runtime::collect(&mut runner, &mut report).await?;
let after = native::identity(&mut runner).await?;
let after = native::identity(&mut runner)
.await
.map_err(|error| format!("Final host identity unavailable: {error}"))?;
check_identity(&source, &boot, &after.source, &after.boot)?;
if report.findings.first().and_then(|v| v.identity.first()) != after.hardware.as_ref() {
return Err("Hardware evidence changed during collection; output not written".into());
Expand Down Expand Up @@ -203,6 +207,20 @@ fn save(path: &Path, bytes: &[u8]) -> Result<()> {
.map_err(|e| format!("Report directory sync failed: {e}"))?;
Ok(())
}
fn finish_collection(report: Result<api::HostReport>, path: &Path) -> Result<()> {
let report = match report {
Ok(report) => report,
Err(reason) => {
println!("Collection unavailable: {reason}. No report was written.");
return Ok(());
}
};
let bytes = encode(report)?;
save(path, &bytes)?;
println!("Report written. Import it in Omloom to review selected current facts.");
Ok(())
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
Expand Down Expand Up @@ -245,9 +263,5 @@ async fn main() -> Result<()> {
}
return Ok(());
}
let report = collect().await?;
let bytes = encode(report)?;
save(Path::new(&args[1]), &bytes)?;
println!("Report written. Import it in Omloom to review selected current facts.");
Ok(())
finish_collection(collect().await, Path::new(&args[1]))
}
Loading
Loading