Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
80fef6f
feat(host): list every expected artifact as not installed when no pla…
crenshawdev Oct 8, 2026
8329e12
feat(ledger): print the host section after every line the doctor alre…
crenshawdev Oct 8, 2026
a7a700d
feat(host): read the placed files and the executable and name what is…
crenshawdev Oct 8, 2026
5dfe87b
feat(host): compare each placed stub byte for byte and the registrati…
crenshawdev Oct 8, 2026
f8d3e77
feat(host): report each tool's coverage as configuration of the docum…
crenshawdev Oct 8, 2026
16f3215
feat(host): name every tool in the hook matcher that no item runs the…
crenshawdev Oct 8, 2026
c8c36f9
feat(host): find the sandbox programs on PATH by platform and fail th…
crenshawdev Oct 8, 2026
4211cde
fix(host): stop crediting the guard for nine tools while disableAllHo…
crenshawdev Oct 8, 2026
4f7d958
fix(host): name an executable with no execute bit as a gap
crenshawdev Oct 8, 2026
f4eab16
feat(host): report the server's project and working directory from th…
crenshawdev Oct 8, 2026
7a02132
feat(host): tell the owner when a newer Baley wrote the ledger and wh…
crenshawdev Oct 8, 2026
bd12b61
feat(host): tell the owner to rebuild the guard's records that were b…
crenshawdev Oct 8, 2026
3923312
docs(design): describe the runtime doctor in the guard and host designs
crenshawdev Oct 8, 2026
2650317
docs(design): show T13 in progress and the doctor's host checks in th…
crenshawdev Oct 8, 2026
3074c3c
test(host): catch a host check that changes an existing ledger row
crenshawdev Oct 8, 2026
7c60abe
test(host): catch a kept server call that a later page never replaces
crenshawdev Oct 8, 2026
be56150
docs(design): scope the doctor's no-write claim to its host checks
crenshawdev Oct 8, 2026
682c12c
test(host): leave the WAL index out of the ledger-home byte snapshot
crenshawdev Oct 8, 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
5 changes: 4 additions & 1 deletion crates/baley/src/host_artifacts/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ fn merge(target: &mut Value, ours: &Value, key: &str, conflicts: &mut Vec<Confli
}
}

fn same_server(existing: &Value, ours: &Value) -> bool {
/// Whether an existing `mcpServers` entry runs the same command with the
/// same arguments as ours. `alwaysLoad` and every other key are ignored, so
/// the doctor judges a registration by composition's own rule.
pub(crate) fn same_server(existing: &Value, ours: &Value) -> bool {
existing.get("command") == ours.get("command") && existing.get("args") == ours.get("args")
}

Expand Down
4 changes: 2 additions & 2 deletions crates/baley/src/host_artifacts/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ fn guard(inputs: &Inputs<'_>, tool: Tool) -> Vec<Cause> {
/// absent, empty or `*` matcher, and reads one holding only letters, digits,
/// `_`, `-`, spaces, `,` and `|` as a list of exact names. Anything else is
/// a regular expression, which this judge does not evaluate.
fn matcher_names(matcher: Option<&Value>, tool: Tool) -> bool {
pub(crate) fn matcher_names(matcher: Option<&Value>, tool: Tool) -> bool {
let text = match matcher {
None => return true,
Some(Value::String(text)) => text.as_str(),
Expand All @@ -526,7 +526,7 @@ fn matcher_names(matcher: Option<&Value>, tool: Tool) -> bool {
/// through bash, with no field that narrows it (`if`), detaches it
/// (`async`), reshapes it (`args`) or is unknown, and no timeout below the
/// guard's budget, since a cut-off hook lets the call run.
fn is_guard(handler: &Value, command: &str) -> bool {
pub(crate) fn is_guard(handler: &Value, command: &str) -> bool {
let Some(fields) = handler.as_object() else {
return false;
};
Expand Down
137 changes: 137 additions & 0 deletions crates/baley/src/host_doctor/guard_records.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//! Whether the guard's per-user records were behind this binary when the
//! doctor started.
//!
//! The guard keeps its decisions in the per-user project `user`. When that
//! project's views are behind this binary's, the guard answers needs-rebuild
//! and records nothing until `baley rebuild user` runs. The store's doctor
//! reads each project's raw view stamps before it verifies the views, and
//! verifying brings an old project current, so the stamps in the [`Health`]
//! it returned show the state the doctor started with.
//!
//! The judgement mirrors the store adapter's rule for a project's live views
//! as far as the raw stamps show it. A stored view this binary no longer
//! declares is not in the stamps, so a project that holds only such a view
//! is not seen here.

use baley_core::catalog::USER_PROJECT;
use baley_store::Health;

/// What the `user` project's views are to this binary.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Judged {
/// The ledger holds no `user` project yet.
NoUserProject,
/// The views are this binary's, or the project is empty and unstamped.
Current,
/// The views were built by an older binary: the guard needs a rebuild.
Behind,
/// A newer binary built the views. A rebuild would take them backward, so
/// there is no rebuild finding; the doctor's other lines report it.
Newer,
/// The stamps or the chain could not be read, which the doctor's other
/// lines report.
NotJudged,
}

/// Judges the `user` project's raw views from the store's health, and no
/// other project's.
pub fn judge(health: &Health) -> Judged {
let Some(user) = health
.projects
.iter()
.find(|project| project.project.0 == USER_PROJECT)
else {
return Judged::NoUserProject;
};
let Ok(raw) = &user.raw_views else {
return Judged::NotJudged;
};
let (live_set, binary_set) = raw.view_set;
if live_set.is_some_and(|set| set > binary_set)
|| raw.views.iter().any(|view| {
view.live_version
.is_some_and(|live| live > view.binary_version)
})
{
return Judged::Newer;
}
let stamped = live_set.is_some() || raw.views.iter().any(|view| view.live_version.is_some());
let behind = if stamped {
live_set.is_some_and(|set| set != binary_set)
|| raw
.views
.iter()
.any(|view| view.live_version != Some(view.binary_version))
} else {
// Nothing is stamped. An empty project needs no stamps from a rebuild,
// the guard stamps it itself, and one with events needs a rebuild.
match &user.verify {
Ok(report) => report.chain.head.is_some(),
Err(_) => return Judged::NotJudged,
}
};
if behind {
Judged::Behind
} else {
Judged::Current
}
}

#[cfg(test)]
mod tests {
use baley_store::StoreError;

use super::super::fixtures::{health_with, project_health, raw_views};
use super::*;

fn behind_views() -> baley_store::RawViewHealth {
raw_views(Some(6), 7, &[(Some(1), 1), (Some(3), 3)])
}

fn current_views() -> baley_store::RawViewHealth {
raw_views(Some(7), 7, &[(Some(1), 1), (Some(3), 3)])
}

#[test]
fn a_project_other_than_user_judged_for_the_guard_is_caught() {
let other_behind = health_with(vec![
project_health("P", Ok(behind_views()), Some(5)),
project_health(USER_PROJECT, Ok(current_views()), Some(5)),
]);
assert_eq!(judge(&other_behind), Judged::Current);

let user_behind = health_with(vec![
project_health("P", Ok(current_views()), Some(5)),
project_health(USER_PROJECT, Ok(behind_views()), Some(5)),
]);
assert_eq!(judge(&user_behind), Judged::Behind);

let no_user = health_with(vec![project_health("P", Ok(behind_views()), Some(5))]);
assert_eq!(judge(&no_user), Judged::NoUserProject);
}

#[test]
fn an_empty_user_project_called_behind_or_a_used_one_without_stamps_called_current_is_caught() {
let unstamped = || raw_views(None, 7, &[(None, 1), (None, 3)]);

let empty = health_with(vec![project_health(USER_PROJECT, Ok(unstamped()), None)]);
assert_eq!(judge(&empty), Judged::Current);

let used = health_with(vec![project_health(USER_PROJECT, Ok(unstamped()), Some(3))]);
assert_eq!(judge(&used), Judged::Behind);

let mut failed = project_health(USER_PROJECT, Ok(unstamped()), Some(3));
failed.verify = Err(StoreError::Busy);
assert_eq!(judge(&health_with(vec![failed])), Judged::NotJudged);
}

#[test]
fn newer_user_views_called_needs_rebuild_is_caught() {
let newer_view = raw_views(Some(7), 7, &[(Some(2), 1), (Some(3), 3)]);
let newer_set = raw_views(Some(8), 7, &[(Some(1), 1), (Some(3), 3)]);
for raw in [newer_view, newer_set] {
let health = health_with(vec![project_health(USER_PROJECT, Ok(raw), Some(5))]);
assert_eq!(judge(&health), Judged::Newer);
}
}
}
Loading
Loading