travelagent-core 1.11.1

Core library for travelagent code review tool
Documentation
//! Byte-size caps applied at the MCP ingress for agent-supplied payload bodies.
//!
//! These caps guard the TUI and standalone MCP server against runaway or
//! hostile agents shipping megabytes of text into a single tool call. They
//! are enforced in each server's tool handler before any side effect runs —
//! i.e. before a comment lands in `ReviewSession`, before a review is pushed
//! to the forge, etc.
//!
//! Limits are expressed in **bytes**, matching Rust's UTF-8 `String::len()`.
//! They apply to free-form human-readable bodies only; they intentionally do
//! **not** cover file paths, SHAs, thread IDs, commit messages, or other
//! values that are bounded by their data shape.
//!
//! The AI summary tool (`trv_set_ai_summary`) has its own 64 KB cap defined
//! alongside its handler and is deliberately not duplicated here.

/// Cap for line/file review comment bodies (`trv_add_comment`).
pub const MCP_COMMENT_BODY_MAX: usize = 32 * 1024;

/// Cap for discussion-thread reply bodies (`trv_post_reply`).
pub const MCP_REPLY_BODY_MAX: usize = 16 * 1024;

/// Cap for the top-level review body submitted with `trv_submit_review`.
pub const MCP_REVIEW_BODY_MAX: usize = 32 * 1024;

/// Cap for the generated-test body supplied to `trv_propose_accept_test`
/// (Phase I4c-2). Larger than the comment caps because a test file can
/// legitimately run into hundreds of lines; still bounded so a runaway
/// agent can't stash a multi-MB file in app state via the propose surface.
pub const MCP_GENERATED_TEST_BODY_MAX: usize = 256 * 1024;

/// Return an error string in the canonical "Input exceeds <name> size limit:
/// <limit> bytes (got <actual>)" shape, or `None` when the body fits.
///
/// The caller decides how to surface the error (JSON envelope vs. bare
/// string) — this helper only owns the message wording so both servers stay
/// in lockstep.
#[must_use]
pub fn check_body_size(name: &str, body: &str, limit: usize) -> Option<String> {
    let actual = body.len();
    if actual > limit {
        Some(format!(
            "Input exceeds {name} size limit: {limit} bytes (got {actual})"
        ))
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn check_body_size_accepts_exactly_at_limit() {
        let body = "x".repeat(MCP_COMMENT_BODY_MAX);
        assert!(check_body_size("comment body", &body, MCP_COMMENT_BODY_MAX).is_none());
    }

    #[test]
    fn check_body_size_rejects_one_byte_over() {
        let body = "x".repeat(MCP_COMMENT_BODY_MAX + 1);
        let msg = check_body_size("comment body", &body, MCP_COMMENT_BODY_MAX)
            .expect("one byte over must be rejected");
        assert!(msg.contains("comment body"));
        assert!(msg.contains(&MCP_COMMENT_BODY_MAX.to_string()));
        assert!(msg.contains(&(MCP_COMMENT_BODY_MAX + 1).to_string()));
    }

    #[test]
    fn check_body_size_accepts_empty_body() {
        // Empty payloads are below every cap — semantic validation (e.g.
        // "comment must not be empty") belongs to the caller, not this guard.
        assert!(check_body_size("whatever", "", MCP_COMMENT_BODY_MAX).is_none());
    }

    #[test]
    fn limits_have_expected_values() {
        // Pin the byte values so a future edit here is visible in review.
        assert_eq!(MCP_COMMENT_BODY_MAX, 32 * 1024);
        assert_eq!(MCP_REPLY_BODY_MAX, 16 * 1024);
        assert_eq!(MCP_REVIEW_BODY_MAX, 32 * 1024);
        assert_eq!(MCP_GENERATED_TEST_BODY_MAX, 256 * 1024);
    }
}