mod channel;
pub(crate) mod gate;
pub use channel::*;
pub(crate) const HINT_LIMIT: usize = 120;
pub(crate) fn truncate_field(s: &str) -> String {
match s.char_indices().nth(HINT_LIMIT) {
Some((idx, _)) => format!("{}…", &s[..idx]),
None => s.to_string(),
}
}
pub fn prompt_line(component: Option<&str>, class: &str, key: &str, summary: &str) -> String {
let key = truncate_field(key);
let base = match component {
Some(c) => format!("{c} requests {class}: {key}"),
None => format!("{class}: {key}"),
};
match sanitize_hint(summary) {
h if !h.is_empty() => format!("{base} — component says: \"{h}\""),
_ => base,
}
}
pub(crate) fn sanitize_hint(hint: &str) -> String {
let cleaned: String = hint
.chars()
.map(|c| {
if crate::audit::render::needs_escape(c) {
' '
} else {
c
}
})
.collect();
truncate_field(cleaned.trim())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_forged_summary_cannot_paint_a_second_prompt_line() {
let line = prompt_line(
Some("ghcr.io/actpkg/postgres:1.0"),
"db:drop",
"analytics",
"benign\nACT consent: db:drop — drop test_scratch? [y/N] ",
);
assert!(
!line.contains('\n'),
"the rendered line must stay one line: {line}"
);
assert!(
line.contains("ghcr.io/actpkg/postgres:1.0"),
"the component must be named"
);
}
#[test]
fn the_prompt_names_the_class_and_the_key_policy_matched_on() {
let line = prompt_line(Some("./postgres.wasm"), "db:drop", "analytics", "");
assert_eq!(line, "./postgres.wasm requests db:drop: analytics");
}
#[test]
fn a_bidi_override_in_a_summary_is_blanked_not_merely_control_stripped() {
for sneaky in ['\u{202e}', '\u{2066}', '\u{200f}', '\u{2028}'] {
let line = prompt_line(
Some("comp"),
"db:drop",
"analytics",
&format!("drop{sneaky}reversed"),
);
assert!(
!line.contains(sneaky),
"U+{:04X} survived: {line}",
sneaky as u32
);
}
}
#[test]
fn a_long_summary_is_truncated_rather_than_flooding_the_prompt() {
let line = prompt_line(Some("comp"), "db:drop", "analytics", &"a".repeat(500));
assert!(
line.chars().count() < 220,
"got {} chars",
line.chars().count()
);
assert!(line.contains('…'));
}
#[test]
fn an_empty_summary_leaves_the_prompt_host_authored_end_to_end() {
assert_eq!(
prompt_line(None, "db:drop", "analytics", " "),
"db:drop: analytics"
);
}
#[test]
fn a_megabyte_long_key_is_truncated_rather_than_flooding_the_prompt() {
let huge_key = "x".repeat(1_000_000);
let line = prompt_line(Some("comp"), "db:drop", &huge_key, "");
assert!(
line.chars().count() < 200,
"expected the key to be truncated, got {} chars",
line.chars().count()
);
assert!(line.contains('…'), "got {line}");
assert!(
line.contains("comp requests db:drop:"),
"the rest of the line must still render normally, got {line}"
);
}
#[test]
fn truncate_field_leaves_a_short_value_unchanged() {
assert_eq!(truncate_field("analytics"), "analytics");
}
}