fix: harden streaming transport and ownership checks

This commit is contained in:
a.tolmachev
2026-04-11 01:29:01 +03:00
parent 1a4f0ea6f3
commit 2770c5935f
3 changed files with 348 additions and 7 deletions
+229
View File
@@ -568,6 +568,47 @@ mod tests {
assert_eq!(after_delete.status(), reqwest::StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn initialize_accepts_json_only_response_negotiation() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-json-accept", vec![]).await;
let api_key =
create_platform_api_key(&registry, "mcp-json-accept", &[PlatformApiKeyScope::Read])
.await;
let base_url = spawn_mcp_server(build_test_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let response = client
.post(agent_mcp_url(&base_url, "sales-json-accept"))
.header(header::ACCEPT, "application/json")
.header(header::AUTHORIZATION, format!("Bearer {api_key}"))
.json(&json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25"
}
}))
.send()
.await
.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::OK);
assert_eq!(
response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap(),
"application/json"
);
}
#[tokio::test]
async fn rejects_get_with_protocol_version_mismatch() {
let registry = test_registry().await;
@@ -989,6 +1030,194 @@ mod tests {
);
}
#[tokio::test]
async fn rejects_cross_agent_session_poll() {
let registry = test_registry().await;
let server_addr = grpc_test_support::spawn_unary_echo_server().await;
let operation_a = test_grpc_session_operation(&server_addr, "echo_stream_session_a");
let operation_b = test_grpc_session_operation(&server_addr, "echo_stream_session_b");
for operation in [&operation_a, &operation_b] {
registry
.create_operation(&test_workspace_id(), operation, Some("alice"))
.await
.unwrap();
registry
.publish_operation(PublishRequest {
workspace_id: &test_workspace_id(),
operation_id: &operation.id,
version: 1,
published_at: "2026-03-26T10:00:00Z",
published_by: Some("alice"),
})
.await
.unwrap();
}
publish_agent_for_operation(&registry, &operation_a, "sales-session-a").await;
publish_agent_for_operation(&registry, &operation_b, "sales-session-b").await;
let api_key = create_platform_api_key(
&registry,
"mcp-cross-session",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let base_url = spawn_mcp_server(build_test_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let agent_a_url = agent_mcp_url(&base_url, "sales-session-a");
let agent_b_url = agent_mcp_url(&base_url, "sales-session-b");
let session_a = initialize_session(&client, &agent_a_url, &api_key).await;
let session_b = initialize_session(&client, &agent_b_url, &api_key).await;
let start_response = post_jsonrpc(
&client,
&agent_a_url,
&api_key,
Some(&session_a),
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "echo_stream_session_a_start",
"arguments": { "message": "hello" }
}
}),
)
.await;
let foreign_session_id = start_response["result"]["structuredContent"]["session_id"]
.as_str()
.unwrap()
.to_owned();
let poll_response = post_jsonrpc(
&client,
&agent_b_url,
&api_key,
Some(&session_b),
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "echo_stream_session_b_poll",
"arguments": { "session_id": foreign_session_id }
}
}),
)
.await;
assert_eq!(
poll_response["result"]["structuredContent"]["error"]["code"],
json!("stream_session_not_found")
);
assert!(
poll_response["result"]["structuredContent"]["error"]["message"]
.as_str()
.is_some_and(|message| message.starts_with("stream session "))
);
}
#[tokio::test]
async fn rejects_cross_agent_async_job_access() {
let registry = test_registry().await;
let upstream_base_url = spawn_upstream_server().await;
let operation_a = test_rest_async_job_operation(&upstream_base_url, "crm_async_a");
let operation_b = test_rest_async_job_operation(&upstream_base_url, "crm_async_b");
for operation in [&operation_a, &operation_b] {
registry
.create_operation(&test_workspace_id(), operation, Some("alice"))
.await
.unwrap();
registry
.publish_operation(PublishRequest {
workspace_id: &test_workspace_id(),
operation_id: &operation.id,
version: 1,
published_at: "2026-03-26T10:00:00Z",
published_by: Some("alice"),
})
.await
.unwrap();
}
publish_agent_for_operation(&registry, &operation_a, "sales-async-a").await;
publish_agent_for_operation(&registry, &operation_b, "sales-async-b").await;
let api_key = create_platform_api_key(
&registry,
"mcp-cross-async",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let base_url = spawn_mcp_server(build_test_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let agent_a_url = agent_mcp_url(&base_url, "sales-async-a");
let agent_b_url = agent_mcp_url(&base_url, "sales-async-b");
let session_a = initialize_session(&client, &agent_a_url, &api_key).await;
let session_b = initialize_session(&client, &agent_b_url, &api_key).await;
let start_response = post_jsonrpc(
&client,
&agent_a_url,
&api_key,
Some(&session_a),
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "crm_async_a_start",
"arguments": { "email": "user@example.com" }
}
}),
)
.await;
let foreign_job_id = start_response["result"]["structuredContent"]["job_id"]
.as_str()
.unwrap()
.to_owned();
let status_response = post_jsonrpc(
&client,
&agent_b_url,
&api_key,
Some(&session_b),
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "crm_async_b_status",
"arguments": { "job_id": foreign_job_id }
}
}),
)
.await;
assert_eq!(
status_response["result"]["structuredContent"]["error"]["code"],
json!("async_job_not_found")
);
assert!(
status_response["result"]["structuredContent"]["error"]["message"]
.as_str()
.is_some_and(|message| message.starts_with("async job "))
);
}
#[tokio::test]
async fn exposes_and_runs_async_job_tool_family_via_mcp() {
let registry = test_registry().await;