use plecto_host::LoadError;
use crate::ControlError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Diagnostic {
pub code: &'static str,
pub cause: &'static str,
pub suggestion: &'static str,
pub docs: &'static str,
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {}\n suggestion: {}\n docs: {}",
self.code, self.cause, self.suggestion, self.docs
)
}
}
pub const SIGNATURE_VERIFICATION_FAILED: Diagnostic = Diagnostic {
code: "PLECTO-E0001",
cause: "the component or SBOM signature does not verify against any key in the manifest's [trust]",
suggestion: "sign with a key listed under [trust] (cosign sign-blob or your CI's signer); for \
local dev, run `plecto dev`, which signs with your project's .plecto/dev-key automatically",
docs: "docs/ADR/000006.md",
};
pub const QUOTA_EXCEEDED: Diagnostic = Diagnostic {
code: "PLECTO-E0002",
cause: "the request exceeded the filter's host-native rate-limit bucket",
suggestion: "raise [filter.ratelimit] capacity/refill_per_sec in the manifest, or have the \
client back off using the retry-after-ms header on the 429",
docs: "docs/ADR/000026.md",
};
pub const PATH_NORMALIZATION_REJECTED: Diagnostic = Diagnostic {
code: "PLECTO-E0003",
cause: "the request path failed normalization (e.g. `..` traversal, invalid percent-encoding, \
or a raw control byte)",
suggestion: "this rejects the client's request, not the manifest; if a legitimate client hits \
this, check what it is sending for the path",
docs: "docs/ADR/000013.md",
};
pub const DEV_KEY_IN_TRUST: Diagnostic = Diagnostic {
code: "PLECTO-E0004",
cause: "a [trust] key file carries the plecto-dev-key marker — it was generated by `plecto \
dev`/`plecto new-filter`, not a production signing key",
suggestion: "if this manifest is meant for production, replace the key with one from your \
real signing pipeline; if it's a dev manifest, this warning is expected",
docs: "docs/ADR/000065.md",
};
pub fn diagnose(err: &ControlError) -> Option<Diagnostic> {
match err {
ControlError::Load { err, .. } => match err.downcast_ref::<LoadError>() {
Some(LoadError::UnverifiedComponentSignature | LoadError::UnverifiedSbomSignature) => {
Some(SIGNATURE_VERIFICATION_FAILED)
}
_ => None,
},
_ => None,
}
}
pub fn diagnosed_message(err: &ControlError) -> String {
match diagnose(err) {
Some(diagnostic) => format!("{err}\n{diagnostic}"),
None => err.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unverified_component_signature_maps_to_e0001() {
let err = ControlError::Load {
id: "f".to_string(),
err: anyhow::Error::from(LoadError::UnverifiedComponentSignature),
};
assert_eq!(diagnose(&err), Some(SIGNATURE_VERIFICATION_FAILED));
}
#[test]
fn unrelated_control_errors_get_no_diagnostic() {
let err = ControlError::DuplicateFilterId("f".to_string());
assert_eq!(diagnose(&err), None);
}
#[test]
fn display_renders_all_four_parts() {
let rendered = SIGNATURE_VERIFICATION_FAILED.to_string();
assert!(rendered.contains("PLECTO-E0001"));
assert!(rendered.contains("suggestion:"));
assert!(rendered.contains("docs:"));
}
#[test]
fn diagnosed_message_appends_the_diagnostic_only_when_one_is_registered() {
let signature = ControlError::Load {
id: "f".to_string(),
err: anyhow::Error::from(LoadError::UnverifiedComponentSignature),
};
let rendered = diagnosed_message(&signature);
assert!(rendered.contains("failed the load gate"));
assert!(rendered.contains("PLECTO-E0001"));
assert!(rendered.contains("suggestion:"));
let plain = ControlError::DuplicateFilterId("f".to_string());
assert_eq!(diagnosed_message(&plain), plain.to_string());
}
}