huskies: merge 1206 story fleet_identity MCP tool: read sled pins vs live signed identity, and re-pin via TOFU
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
//! transports (Matrix, Slack). Uses `service::pipeline::aggregate_pipeline_counts`
|
||||
//! for per-project parsing.
|
||||
|
||||
use super::identity::SledIdentityReport;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -125,6 +126,54 @@ pub fn format_overview_compact(items_by_project: &BTreeMap<String, Value>) -> St
|
||||
format!("**Overview: Active Work**\n\n{}", sections.join("\n\n"))
|
||||
}
|
||||
|
||||
/// Format `fleet_identity` read-mode reports as Markdown, one line per sled.
|
||||
///
|
||||
/// Matches, first-contacts (no pin recorded yet), and unreachable sleds get a
|
||||
/// plain status line. A verified mismatch is called out with the same
|
||||
/// wording as the `upgrade` command's identity check
|
||||
/// (`chat::transport::matrix::sled_upgrade::verify_sled_identity`) so an
|
||||
/// operator sees one consistent message for "wrong container answered"
|
||||
/// regardless of which command surfaced it — including naming the container
|
||||
/// as `huskies-{project}`.
|
||||
pub fn format_identity_reports(reports: &[SledIdentityReport]) -> String {
|
||||
if reports.is_empty() {
|
||||
return "No projects registered.".to_string();
|
||||
}
|
||||
|
||||
let lines: Vec<String> = reports
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let container_name = format!("huskies-{}", r.project);
|
||||
let url = r.url.as_deref().unwrap_or("(no url configured)");
|
||||
|
||||
if !r.connected {
|
||||
return format!("\u{1F534} **{}** — unreachable at `{url}`", r.project);
|
||||
}
|
||||
|
||||
match (&r.expected_pin, &r.live_node_id, r.matched) {
|
||||
(_, _, true) => format!("\u{1F7E2} **{}** — matches pin `{url}`", r.project),
|
||||
(Some(expected), Some(live), false) => format!(
|
||||
"\u{1F7E0} **identity mismatch** for `{container_name}` at `{url}`: expected \
|
||||
node_id `{expected}`, but the container that answered identified as \
|
||||
`{live}`. Refusing to proceed — this may not be the sled you expect."
|
||||
),
|
||||
(None, Some(live), false) => format!(
|
||||
"\u{1F7E1} **{}** — no pin recorded yet at `{url}`; live node_id `{live}` \
|
||||
(re-pin to trust it)",
|
||||
r.project
|
||||
),
|
||||
(_, None, false) => format!(
|
||||
"\u{1F534} **identity verification failed** for `{container_name}` at `{url}`: \
|
||||
the `/identity` response's signature is missing or did not verify. \
|
||||
Refusing to proceed — the container's identity cannot be trusted."
|
||||
),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
format!("**Fleet Identity**\n\n{}", lines.join("\n"))
|
||||
}
|
||||
|
||||
/// Find the registered project whose pipeline contains a story with the
|
||||
/// given numeric ID prefix, searching `active`, `backlog`, and `archived`
|
||||
/// alike so gateway `status <n>` resolves regardless of which project is
|
||||
@@ -313,6 +362,95 @@ mod tests {
|
||||
assert!(output.find("alpha").unwrap() < output.find("beta").unwrap());
|
||||
}
|
||||
|
||||
// ── format_identity_reports (story 1206 AC3) ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn identity_report_mismatch_names_container_consistent_with_upgrade_sweep() {
|
||||
let reports = vec![SledIdentityReport {
|
||||
project: "myapp".to_string(),
|
||||
url: Some("http://sled:3001".to_string()),
|
||||
connected: true,
|
||||
expected_pin: Some("expected-id".to_string()),
|
||||
live_node_id: Some("different-id".to_string()),
|
||||
matched: false,
|
||||
}];
|
||||
let output = format_identity_reports(&reports);
|
||||
assert!(output.contains("**identity mismatch**"));
|
||||
assert!(
|
||||
output.contains("`huskies-myapp`"),
|
||||
"must name the container as huskies-<project>: {output}"
|
||||
);
|
||||
assert!(output.contains("expected node_id `expected-id`"));
|
||||
assert!(output.contains("identified as `different-id`"));
|
||||
assert!(output.contains("Refusing to proceed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_report_match_is_a_plain_status_line() {
|
||||
let reports = vec![SledIdentityReport {
|
||||
project: "myapp".to_string(),
|
||||
url: Some("http://sled:3001".to_string()),
|
||||
connected: true,
|
||||
expected_pin: Some("abc".to_string()),
|
||||
live_node_id: Some("abc".to_string()),
|
||||
matched: true,
|
||||
}];
|
||||
let output = format_identity_reports(&reports);
|
||||
assert!(output.contains("myapp"));
|
||||
assert!(output.contains("matches pin"));
|
||||
assert!(!output.contains("mismatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_report_unreachable_sled_is_marked() {
|
||||
let reports = vec![SledIdentityReport {
|
||||
project: "myapp".to_string(),
|
||||
url: Some("http://sled:3001".to_string()),
|
||||
connected: false,
|
||||
expected_pin: Some("abc".to_string()),
|
||||
live_node_id: None,
|
||||
matched: false,
|
||||
}];
|
||||
let output = format_identity_reports(&reports);
|
||||
assert!(output.contains("unreachable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_report_no_pin_recorded_invites_a_repin() {
|
||||
let reports = vec![SledIdentityReport {
|
||||
project: "myapp".to_string(),
|
||||
url: Some("http://sled:3001".to_string()),
|
||||
connected: true,
|
||||
expected_pin: None,
|
||||
live_node_id: Some("some-id".to_string()),
|
||||
matched: false,
|
||||
}];
|
||||
let output = format_identity_reports(&reports);
|
||||
assert!(output.contains("no pin recorded"));
|
||||
assert!(output.contains("some-id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_report_invalid_signature_is_flagged() {
|
||||
let reports = vec![SledIdentityReport {
|
||||
project: "myapp".to_string(),
|
||||
url: Some("http://sled:3001".to_string()),
|
||||
connected: true,
|
||||
expected_pin: Some("abc".to_string()),
|
||||
live_node_id: None,
|
||||
matched: false,
|
||||
}];
|
||||
let output = format_identity_reports(&reports);
|
||||
assert!(output.contains("**identity verification failed**"));
|
||||
assert!(output.contains("`huskies-myapp`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_report_empty_list() {
|
||||
let output = format_identity_reports(&[]);
|
||||
assert_eq!(output, "No projects registered.");
|
||||
}
|
||||
|
||||
// ── find_project_containing_story ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user