refactor(hive-c0re): group src-root files into submodules
stores/ (sqlite-backed host stores + db helper), stats/, agent_config/, workers/ — pure git-mv moves; crate-root re-exports keep every crate::<module> path compiling. flake_check stays at root (synchronous approval-flow validation, not a background worker)
This commit is contained in:
parent
b489454dc2
commit
0e4b5a1120
29 changed files with 68 additions and 24 deletions
|
|
@ -1,176 +0,0 @@
|
|||
//! Wire-protocol size limits shared across the agent + manager
|
||||
//! sockets. Caps on inline message bodies stop a single chatty agent
|
||||
//! (or a misbehaving extra-MCP server) from flooding the broker
|
||||
//! sqlite with megabyte-sized rows that then bloat every recipient's
|
||||
//! wake-prompt context. Anything genuinely larger should be written
|
||||
//! to a state file and the path sent as the body.
|
||||
//!
|
||||
//! Reminders get a separate auto-file escape hatch (see
|
||||
//! `socket_server::handle_remind`) so callers don't have to think
|
||||
//! about it — oversized reminder bodies get persisted to disk
|
||||
//! transparently and the inbox sees a pointer.
|
||||
|
||||
/// Per-message body cap. Applies to `send`, `ask` question text,
|
||||
/// `answer` body, and the stored inline form of a reminder. 4 KiB
|
||||
/// catches the bulk of conversational overflow (status reports,
|
||||
/// bullet-list summaries, short proposals) while staying small
|
||||
/// enough that a backed-up inbox of ~10 unread messages only adds
|
||||
/// ~40 KiB to the recipient's wake-prompt context. Genuinely
|
||||
/// long-form artifacts (audit reports, full diffs, transcripts)
|
||||
/// still belong in a state file — the error message on overflow
|
||||
/// points callers at that escape hatch.
|
||||
pub const MESSAGE_MAX_BYTES: usize = 4096;
|
||||
|
||||
/// Validate that `body` fits under [`MESSAGE_MAX_BYTES`]. Returns a
|
||||
/// caller-ready error string (caller wraps in
|
||||
/// `AgentResponse::Err`/`ManagerResponse::Err`) on failure.
|
||||
///
|
||||
/// `label` shows up in the error message verbatim — pass a short
|
||||
/// noun like `"send"`, `"question"`, `"broadcast"` so the model can
|
||||
/// tell which call got rejected.
|
||||
pub fn check_size(label: &str, body: &str) -> Result<(), String> {
|
||||
if body.len() > MESSAGE_MAX_BYTES {
|
||||
Err(format!(
|
||||
"{label} body too long ({} bytes, max {MESSAGE_MAX_BYTES}); write the \
|
||||
payload to a file under your `/agents/<you>/state/` dir and send the \
|
||||
path as the body instead",
|
||||
body.len()
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-status soft cap. `set_status` renders as a short chip on the
|
||||
/// dashboard agent card — the front-end truncates long strings to
|
||||
/// keep the row layout intact, so a multi-paragraph "session report"
|
||||
/// is wasted bytes that just bloat the rescan emit + container view
|
||||
/// payload. Cap at 200 chars to fit the chip plus a little
|
||||
/// descriptive padding without forcing the operator to read a
|
||||
/// scrolling chunk.
|
||||
/// NOTE: `hive-ag3nt/src/mcp.rs::write_status_file` mirrors this constant
|
||||
/// client-side so invalid text is caught before the file is written.
|
||||
/// Keep in sync if this value changes.
|
||||
pub const STATUS_MAX_CHARS: usize = 200;
|
||||
|
||||
/// Validate a `set_status` payload. Single-line + bounded so
|
||||
/// callers can't dump multi-paragraph session reports into the
|
||||
/// dashboard chip. Whitespace trim is done by the caller before the
|
||||
/// store-to-disk step — we run validation on the trimmed form so
|
||||
/// surrounding whitespace doesn't push a borderline-legal status
|
||||
/// past the cap.
|
||||
///
|
||||
/// Empty / all-whitespace input is accepted: the call site treats
|
||||
/// that as "clear the status" and removes the on-disk sentinel. Tests
|
||||
/// + caller cover both directions.
|
||||
///
|
||||
/// Returns a caller-ready error string suitable for surfacing in the
|
||||
/// `*Response::Err` shape.
|
||||
pub fn check_status_text(text: &str) -> Result<(), String> {
|
||||
let trimmed = text.trim();
|
||||
if trimmed.is_empty() {
|
||||
// Empty = clear-status sentinel; nothing to validate.
|
||||
return Ok(());
|
||||
}
|
||||
// Newline / carriage-return: status is a single-line chip on the
|
||||
// dashboard. Multi-line session reports should go to a state file.
|
||||
if trimmed.contains('\n') || trimmed.contains('\r') {
|
||||
return Err(
|
||||
"set_status text must be a single line — write multi-line context to \
|
||||
a file under your `/agents/<you>/state/` dir and reference that path \
|
||||
from the chip instead"
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
let len = trimmed.chars().count();
|
||||
if len > STATUS_MAX_CHARS {
|
||||
return Err(format!(
|
||||
"set_status text too long ({len} chars, max {STATUS_MAX_CHARS}); the \
|
||||
dashboard chip truncates anything longer, so trim to a short summary \
|
||||
and write the detail to `/agents/<you>/state/<file>` instead"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn accepts_short_body() {
|
||||
assert!(check_size("send", "hello").is_ok());
|
||||
assert!(check_size("send", &"x".repeat(MESSAGE_MAX_BYTES)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_oversize_body() {
|
||||
let err = check_size("send", &"x".repeat(MESSAGE_MAX_BYTES + 1)).unwrap_err();
|
||||
assert!(err.contains("send body too long"));
|
||||
assert!(err.contains(&format!("max {MESSAGE_MAX_BYTES}")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn label_threads_through() {
|
||||
let err = check_size("question", &"x".repeat(MESSAGE_MAX_BYTES + 1)).unwrap_err();
|
||||
assert!(err.starts_with("question body too long"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_accepts_short_single_line() {
|
||||
assert!(check_status_text("idle").is_ok());
|
||||
assert!(check_status_text("processing matrix messages").is_ok());
|
||||
// Boundary: exactly STATUS_MAX_CHARS chars trimmed is still
|
||||
// accepted; one more rejects.
|
||||
let max = "a".repeat(STATUS_MAX_CHARS);
|
||||
assert!(check_status_text(&max).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_accepts_empty_and_whitespace() {
|
||||
// Empty + whitespace-only are the "clear status" sentinel and
|
||||
// bypass the rest of the checks.
|
||||
assert!(check_status_text("").is_ok());
|
||||
assert!(check_status_text(" ").is_ok());
|
||||
assert!(check_status_text("\n\t ").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_rejects_multi_line() {
|
||||
let err = check_status_text("line one\nline two").unwrap_err();
|
||||
assert!(err.contains("single line"), "err = {err}");
|
||||
// Carriage return alone also rejects (windows linebreak / CR-only).
|
||||
assert!(check_status_text("a\rb").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_rejects_oversize() {
|
||||
let too_long = "a".repeat(STATUS_MAX_CHARS + 1);
|
||||
let err = check_status_text(&too_long).unwrap_err();
|
||||
assert!(err.contains("too long"), "err = {err}");
|
||||
assert!(err.contains(&format!("max {STATUS_MAX_CHARS}")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_counts_chars_not_bytes() {
|
||||
// Multi-byte chars (emoji, accented letters) count once each
|
||||
// per char — chars().count() not byte len. STATUS_MAX_CHARS
|
||||
// worth of 4-byte chars is still legal.
|
||||
let emoji = "💜".repeat(STATUS_MAX_CHARS);
|
||||
assert!(
|
||||
check_status_text(&emoji).is_ok(),
|
||||
"{STATUS_MAX_CHARS} emoji should fit"
|
||||
);
|
||||
let too_many = "💜".repeat(STATUS_MAX_CHARS + 1);
|
||||
assert!(check_status_text(&too_many).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_status_validates_post_trim() {
|
||||
// Leading/trailing whitespace is trimmed before the length
|
||||
// check — a borderline-legal payload with spaces around it
|
||||
// still passes.
|
||||
let padded = format!(" {} ", "a".repeat(STATUS_MAX_CHARS));
|
||||
assert!(check_status_text(&padded).is_ok());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue