meerkat-contracts 0.5.2

Wire format contracts and capability registry for Meerkat
Documentation
use serde::Serialize;

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RestOperationDescriptor {
    pub method: &'static str,
    pub summary: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<&'static str>,
}

impl RestOperationDescriptor {
    const fn new(method: &'static str, summary: &'static str) -> Self {
        Self {
            method,
            summary,
            description: None,
        }
    }

    const fn with_description(
        method: &'static str,
        summary: &'static str,
        description: &'static str,
    ) -> Self {
        Self {
            method,
            summary,
            description: Some(description),
        }
    }
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RestPathDescriptor {
    pub path: &'static str,
    pub operations: Vec<RestOperationDescriptor>,
}

impl RestPathDescriptor {
    fn new(path: &'static str, operations: Vec<RestOperationDescriptor>) -> Self {
        Self { path, operations }
    }
}

pub fn rest_path_catalog() -> Vec<RestPathDescriptor> {
    vec![
        RestPathDescriptor::new(
            "/sessions",
            vec![
                RestOperationDescriptor::new("get", "List sessions"),
                RestOperationDescriptor::new("post", "Create and run a new session"),
            ],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}",
            vec![
                RestOperationDescriptor::new("get", "Get session details"),
                RestOperationDescriptor::new("delete", "Archive a session"),
            ],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/history",
            vec![RestOperationDescriptor::new(
                "get",
                "Get full session history",
            )],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/interrupt",
            vec![RestOperationDescriptor::new(
                "post",
                "Interrupt a running session",
            )],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/system_context",
            vec![RestOperationDescriptor::new(
                "post",
                "Append system context to a session",
            )],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/messages",
            vec![RestOperationDescriptor::new(
                "post",
                "Continue session with new message",
            )],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/external-events",
            vec![RestOperationDescriptor::new(
                "post",
                "Queue a runtime-backed external event",
            )],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/events",
            vec![RestOperationDescriptor::new("get", "SSE event stream")],
        ),
        RestPathDescriptor::new(
            "/schedule/tools",
            vec![RestOperationDescriptor::new("get", "List schedule tools")],
        ),
        RestPathDescriptor::new(
            "/schedule/call",
            vec![RestOperationDescriptor::new(
                "post",
                "Invoke a schedule tool",
            )],
        ),
        RestPathDescriptor::new(
            "/schedules",
            vec![
                RestOperationDescriptor::new("get", "List schedules"),
                RestOperationDescriptor::new("post", "Create schedule"),
            ],
        ),
        RestPathDescriptor::new(
            "/schedules/{id}",
            vec![
                RestOperationDescriptor::new("get", "Get schedule"),
                RestOperationDescriptor::new("patch", "Update schedule"),
                RestOperationDescriptor::new("delete", "Delete schedule"),
            ],
        ),
        RestPathDescriptor::new(
            "/schedules/{id}/pause",
            vec![RestOperationDescriptor::new("post", "Pause schedule")],
        ),
        RestPathDescriptor::new(
            "/schedules/{id}/resume",
            vec![RestOperationDescriptor::new("post", "Resume schedule")],
        ),
        RestPathDescriptor::new(
            "/schedules/{id}/occurrences",
            vec![RestOperationDescriptor::new(
                "get",
                "List schedule occurrences",
            )],
        ),
        RestPathDescriptor::new(
            "/comms/send",
            vec![RestOperationDescriptor::new("post", "Send a comms message")],
        ),
        RestPathDescriptor::new(
            "/comms/peers",
            vec![RestOperationDescriptor::new(
                "get",
                "List resolved comms peers",
            )],
        ),
        RestPathDescriptor::new(
            "/config",
            vec![
                RestOperationDescriptor::new("get", "Read config"),
                RestOperationDescriptor::new("put", "Replace config"),
                RestOperationDescriptor::new("patch", "Merge-patch config"),
            ],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/mcp/add",
            vec![RestOperationDescriptor::with_description(
                "post",
                "Stage live MCP server addition",
                "Requires mcp_live capability. Check GET /capabilities.",
            )],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/mcp/remove",
            vec![RestOperationDescriptor::with_description(
                "post",
                "Stage live MCP server removal",
                "Requires mcp_live capability. Check GET /capabilities.",
            )],
        ),
        RestPathDescriptor::new(
            "/sessions/{id}/mcp/reload",
            vec![RestOperationDescriptor::with_description(
                "post",
                "Stage live MCP server reload",
                "Requires mcp_live capability. Check GET /capabilities.",
            )],
        ),
        RestPathDescriptor::new(
            "/skills",
            vec![RestOperationDescriptor::new("get", "List available skills")],
        ),
        RestPathDescriptor::new(
            "/skills/{id}",
            vec![RestOperationDescriptor::new("get", "Inspect a skill")],
        ),
        RestPathDescriptor::new(
            "/capabilities",
            vec![RestOperationDescriptor::new(
                "get",
                "Get runtime capabilities",
            )],
        ),
        RestPathDescriptor::new(
            "/models/catalog",
            vec![RestOperationDescriptor::new(
                "get",
                "Get the compiled-in model catalog",
            )],
        ),
        RestPathDescriptor::new(
            "/runtime/{id}/state",
            vec![RestOperationDescriptor::new("get", "Get runtime state")],
        ),
        RestPathDescriptor::new(
            "/runtime/{id}/accept",
            vec![RestOperationDescriptor::new(
                "post",
                "Accept a runtime input",
            )],
        ),
        RestPathDescriptor::new(
            "/runtime/{id}/retire",
            vec![RestOperationDescriptor::new("post", "Retire a runtime")],
        ),
        RestPathDescriptor::new(
            "/runtime/{id}/reset",
            vec![RestOperationDescriptor::new("post", "Reset a runtime")],
        ),
        RestPathDescriptor::new(
            "/input/{id}/list",
            vec![RestOperationDescriptor::new(
                "get",
                "List runtime inputs for a session",
            )],
        ),
        RestPathDescriptor::new(
            "/input/{session_id}/{input_id}",
            vec![RestOperationDescriptor::new(
                "get",
                "Get a runtime input state",
            )],
        ),
        RestPathDescriptor::new(
            "/mob/{id}/events",
            vec![RestOperationDescriptor::new("get", "SSE mob event stream")],
        ),
        RestPathDescriptor::new(
            "/mob/{id}/spawn-helper",
            vec![RestOperationDescriptor::new(
                "post",
                "Spawn a helper member in a mob",
            )],
        ),
        RestPathDescriptor::new(
            "/mob/{id}/fork-helper",
            vec![RestOperationDescriptor::new(
                "post",
                "Fork a helper member in a mob",
            )],
        ),
        RestPathDescriptor::new(
            "/mob/{id}/members/{meerkat_id}/status",
            vec![RestOperationDescriptor::new(
                "get",
                "Get live status for a mob member",
            )],
        ),
        RestPathDescriptor::new(
            "/mob/{id}/members/{meerkat_id}/cancel",
            vec![RestOperationDescriptor::new(
                "post",
                "Force-cancel a mob member",
            )],
        ),
        RestPathDescriptor::new(
            "/mob/{id}/members/{meerkat_id}/respawn",
            vec![RestOperationDescriptor::new(
                "post",
                "Respawn a mob member with topology restore",
            )],
        ),
        RestPathDescriptor::new(
            "/health",
            vec![RestOperationDescriptor::new("get", "Health check")],
        ),
    ]
}

pub fn rest_documented_paths() -> Vec<&'static str> {
    rest_path_catalog()
        .into_iter()
        .map(|entry| entry.path)
        .collect()
}

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

    #[test]
    fn catalog_keeps_live_config_and_member_routes() {
        let paths = rest_documented_paths();
        for expected in [
            "/config",
            "/schedules",
            "/schedules/{id}/occurrences",
            "/mob/{id}/members/{meerkat_id}/status",
            "/mob/{id}/members/{meerkat_id}/cancel",
            "/mob/{id}/members/{meerkat_id}/respawn",
        ] {
            assert!(paths.iter().any(|path| path == &expected));
        }
    }

    #[test]
    #[allow(clippy::expect_used)]
    fn catalog_keeps_live_mcp_route_descriptions() {
        let catalog = rest_path_catalog();
        let mcp_add = catalog
            .iter()
            .find(|entry| entry.path == "/sessions/{id}/mcp/add")
            .expect("mcp/add path should remain documented");
        let post = mcp_add
            .operations
            .iter()
            .find(|operation| operation.method == "post")
            .expect("mcp/add POST should remain documented");
        assert_eq!(
            post.description,
            Some("Requires mcp_live capability. Check GET /capabilities.")
        );
    }
}