fix: harden streaming transport and ownership checks
This commit is contained in:
@@ -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(®istry, "sales-json-accept", vec![]).await;
|
||||
let api_key =
|
||||
create_platform_api_key(®istry, "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(®istry, &operation_a, "sales-session-a").await;
|
||||
publish_agent_for_operation(®istry, &operation_b, "sales-session-b").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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(®istry, &operation_a, "sales-async-a").await;
|
||||
publish_agent_for_operation(®istry, &operation_b, "sales-async-b").await;
|
||||
let api_key = create_platform_api_key(
|
||||
®istry,
|
||||
"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;
|
||||
|
||||
Reference in New Issue
Block a user