mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-26 19:21:52 +02:00
Add files via upload
This commit is contained in:
@@ -1424,7 +1424,8 @@ func (db *DB) GetProcessDetailsSummary(messageID string) (*ProcessDetailsSummary
|
|||||||
seenExecIDs := make(map[string]bool)
|
seenExecIDs := make(map[string]bool)
|
||||||
// A provider may reuse a fallback toolCallId across streaming rounds. Keep a
|
// A provider may reuse a fallback toolCallId across streaming rounds. Keep a
|
||||||
// FIFO per ID instead of a single index so every persisted call gets at most
|
// FIFO per ID instead of a single index so every persisted call gets at most
|
||||||
// one result. Results without an ID fall back to the oldest unmatched call.
|
// one result. Results without a stable ID are kept separate instead of being
|
||||||
|
// guessed by order; showing no link is safer than linking to the wrong tool.
|
||||||
toolIndexesByCallID := make(map[string][]int)
|
toolIndexesByCallID := make(map[string][]int)
|
||||||
lastMatchedToolIndexByCallID := make(map[string]int)
|
lastMatchedToolIndexByCallID := make(map[string]int)
|
||||||
matchedToolIndexes := make([]bool, 0)
|
matchedToolIndexes := make([]bool, 0)
|
||||||
@@ -1499,7 +1500,7 @@ func (db *DB) GetProcessDetailsSummary(messageID string) (*ProcessDetailsSummary
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if idx < 0 {
|
if idx < 0 && toolCallID != "" {
|
||||||
for nextUnmatchedToolIdx < len(matchedToolIndexes) && matchedToolIndexes[nextUnmatchedToolIdx] {
|
for nextUnmatchedToolIdx < len(matchedToolIndexes) && matchedToolIndexes[nextUnmatchedToolIdx] {
|
||||||
nextUnmatchedToolIdx++
|
nextUnmatchedToolIdx++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProcessDetailsSummaryPairsMixedIdentifiedAndIDLessResults(t *testing.T) {
|
func TestProcessDetailsSummaryDoesNotGuessIDLessResultsByOrder(t *testing.T) {
|
||||||
db, conversationID, messageID := setupProcessDetailsSummaryTest(t)
|
db, conversationID, messageID := setupProcessDetailsSummaryTest(t)
|
||||||
for _, id := range []string{"call-1", "call-2", "call-3", "call-4"} {
|
for _, id := range []string{"call-1", "call-2", "call-3", "call-4"} {
|
||||||
if err := db.AddProcessDetail(messageID, conversationID, "tool_call", "call", map[string]interface{}{
|
if err := db.AddProcessDetail(messageID, conversationID, "tool_call", "call", map[string]interface{}{
|
||||||
@@ -32,14 +32,24 @@ func TestProcessDetailsSummaryPairsMixedIdentifiedAndIDLessResults(t *testing.T)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetProcessDetailsSummary: %v", err)
|
t.Fatalf("GetProcessDetailsSummary: %v", err)
|
||||||
}
|
}
|
||||||
if len(summary.ToolExecutions) != 4 {
|
if len(summary.ToolExecutions) != 6 {
|
||||||
t.Fatalf("tool executions = %d, want 4", len(summary.ToolExecutions))
|
t.Fatalf("tool executions = %d, want 6", len(summary.ToolExecutions))
|
||||||
}
|
}
|
||||||
for i, execution := range summary.ToolExecutions {
|
for i, execution := range summary.ToolExecutions[:2] {
|
||||||
if execution.Status != "completed" {
|
if execution.Status != "completed" {
|
||||||
t.Fatalf("execution %d status = %q, want completed", i, execution.Status)
|
t.Fatalf("execution %d status = %q, want completed", i, execution.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for i, execution := range summary.ToolExecutions[2:4] {
|
||||||
|
if execution.Status != "result_missing" {
|
||||||
|
t.Fatalf("unmatched call %d status = %q, want result_missing", i, execution.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, execution := range summary.ToolExecutions[4:] {
|
||||||
|
if execution.Status != "completed" || execution.ToolCallID != "" {
|
||||||
|
t.Fatalf("idless result %d = %#v, want separate completed result without toolCallId", i, execution)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessDetailsSummaryPairsRepeatedToolCallIDsFIFO(t *testing.T) {
|
func TestProcessDetailsSummaryPairsRepeatedToolCallIDsFIFO(t *testing.T) {
|
||||||
|
|||||||
@@ -674,48 +674,6 @@ func (m *ExternalMCPManager) updateToolCache(name string, tools []Tool) {
|
|||||||
|
|
||||||
// CallTool 调用外部MCP工具(返回执行ID)
|
// CallTool 调用外部MCP工具(返回执行ID)
|
||||||
func (m *ExternalMCPManager) CallTool(ctx context.Context, toolName string, args map[string]interface{}) (*ToolResult, string, error) {
|
func (m *ExternalMCPManager) CallTool(ctx context.Context, toolName string, args map[string]interface{}) (*ToolResult, string, error) {
|
||||||
_, authenticated := authctx.PrincipalFromContext(ctx)
|
|
||||||
m.mu.RLock()
|
|
||||||
authorizer := m.toolAuthorizer
|
|
||||||
m.mu.RUnlock()
|
|
||||||
if authorizer != nil {
|
|
||||||
if err := authorizer(ctx, toolName, args); err != nil {
|
|
||||||
return nil, "", fmt.Errorf("external tool authorization denied: %w", err)
|
|
||||||
}
|
|
||||||
} else if authenticated {
|
|
||||||
return nil, "", fmt.Errorf("external tool authorization policy is not configured")
|
|
||||||
}
|
|
||||||
// 解析工具名称:name::toolName
|
|
||||||
var mcpName, actualToolName string
|
|
||||||
if idx := findSubstring(toolName, "::"); idx > 0 {
|
|
||||||
mcpName = toolName[:idx]
|
|
||||||
actualToolName = toolName[idx+2:]
|
|
||||||
} else {
|
|
||||||
return nil, "", fmt.Errorf("无效的工具名称格式: %s", toolName)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, exists := m.GetClient(mcpName)
|
|
||||||
if !exists {
|
|
||||||
return nil, "", fmt.Errorf("外部MCP客户端不存在: %s", mcpName)
|
|
||||||
}
|
|
||||||
if err := m.checkExternalMCPCircuit(mcpName); err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查连接状态,如果未连接或状态为error,不允许调用
|
|
||||||
if !client.IsConnected() {
|
|
||||||
status := client.GetStatus()
|
|
||||||
if status == "error" {
|
|
||||||
// 获取错误信息(如果有)
|
|
||||||
errorMsg := m.GetError(mcpName)
|
|
||||||
if errorMsg != "" {
|
|
||||||
return nil, "", fmt.Errorf("外部MCP连接失败: %s (错误: %s)", mcpName, errorMsg)
|
|
||||||
}
|
|
||||||
return nil, "", fmt.Errorf("外部MCP连接失败: %s", mcpName)
|
|
||||||
}
|
|
||||||
return nil, "", fmt.Errorf("外部MCP客户端未连接: %s (状态: %s)", mcpName, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.executionService == nil {
|
if m.executionService == nil {
|
||||||
m.executionService = NewExecutionService(m.storage, m.logger)
|
m.executionService = NewExecutionService(m.storage, m.logger)
|
||||||
m.executionService.ConfigureToolResultMaxBytes(m.toolResultMaxBytes)
|
m.executionService.ConfigureToolResultMaxBytes(m.toolResultMaxBytes)
|
||||||
@@ -725,12 +683,57 @@ func (m *ExternalMCPManager) CallTool(ctx context.Context, toolName string, args
|
|||||||
if principal, ok := authctx.PrincipalFromContext(ctx); ok {
|
if principal, ok := authctx.PrincipalFromContext(ctx); ok {
|
||||||
ownerUserID = principal.UserID
|
ownerUserID = principal.UserID
|
||||||
}
|
}
|
||||||
|
var mcpName, actualToolName string
|
||||||
|
var client ExternalMCPClient
|
||||||
handle, err := m.executionService.Submit(ctx, ExecutionRequest{
|
handle, err := m.executionService.Submit(ctx, ExecutionRequest{
|
||||||
ToolName: toolName,
|
ToolName: toolName,
|
||||||
Arguments: args,
|
Arguments: args,
|
||||||
ConversationID: MCPConversationIDFromContext(ctx),
|
ConversationID: MCPConversationIDFromContext(ctx),
|
||||||
OwnerUserID: ownerUserID,
|
OwnerUserID: ownerUserID,
|
||||||
PreRun: func(runCtx context.Context, exec *ToolExecution) (func(), error) {
|
PreRun: func(runCtx context.Context, exec *ToolExecution) (func(), error) {
|
||||||
|
_, authenticated := authctx.PrincipalFromContext(runCtx)
|
||||||
|
m.mu.RLock()
|
||||||
|
authorizer := m.toolAuthorizer
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if authorizer != nil {
|
||||||
|
if err := authorizer(runCtx, toolName, args); err != nil {
|
||||||
|
return nil, fmt.Errorf("external tool authorization denied: %w", err)
|
||||||
|
}
|
||||||
|
} else if authenticated {
|
||||||
|
return nil, fmt.Errorf("external tool authorization policy is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析工具名称:name::toolName
|
||||||
|
if idx := findSubstring(toolName, "::"); idx > 0 {
|
||||||
|
mcpName = toolName[:idx]
|
||||||
|
actualToolName = toolName[idx+2:]
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("无效的工具名称格式: %s", toolName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
client, exists = m.GetClient(mcpName)
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("外部MCP客户端不存在: %s", mcpName)
|
||||||
|
}
|
||||||
|
if err := m.checkExternalMCPCircuit(mcpName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查连接状态,如果未连接或状态为error,不允许调用
|
||||||
|
if !client.IsConnected() {
|
||||||
|
status := client.GetStatus()
|
||||||
|
if status == "error" {
|
||||||
|
// 获取错误信息(如果有)
|
||||||
|
errorMsg := m.GetError(mcpName)
|
||||||
|
if errorMsg != "" {
|
||||||
|
return nil, fmt.Errorf("外部MCP连接失败: %s (错误: %s)", mcpName, errorMsg)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("外部MCP连接失败: %s", mcpName)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("外部MCP客户端未连接: %s (状态: %s)", mcpName, status)
|
||||||
|
}
|
||||||
|
|
||||||
release, acquireErr := m.acquireExternalMCPCallSlot(runCtx, mcpName)
|
release, acquireErr := m.acquireExternalMCPCallSlot(runCtx, mcpName)
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return nil, acquireErr
|
return nil, acquireErr
|
||||||
@@ -746,7 +749,9 @@ func (m *ExternalMCPManager) CallTool(ctx context.Context, toolName string, args
|
|||||||
},
|
},
|
||||||
OnDone: func(exec *ToolExecution) {
|
OnDone: func(exec *ToolExecution) {
|
||||||
failed := exec != nil && exec.Status != ToolExecutionStatusCompleted && exec.Status != ToolExecutionStatusCancelled
|
failed := exec != nil && exec.Status != ToolExecutionStatusCompleted && exec.Status != ToolExecutionStatusCancelled
|
||||||
m.recordExternalMCPResult(mcpName, failed)
|
if mcpName != "" {
|
||||||
|
m.recordExternalMCPResult(mcpName, failed)
|
||||||
|
}
|
||||||
m.updateStats(toolName, failed)
|
m.updateStats(toolName, failed)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,10 +20,20 @@ func TestExternalManagerEnforcesConfiguredAuthorizer(t *testing.T) {
|
|||||||
return errors.New("denied by policy")
|
return errors.New("denied by policy")
|
||||||
})
|
})
|
||||||
ctx := authctx.WithPrincipal(context.Background(), authctx.NewPrincipal("u1", "user", "assigned", map[string]bool{"agent:execute": true}))
|
ctx := authctx.WithPrincipal(context.Background(), authctx.NewPrincipal("u1", "user", "assigned", map[string]bool{"agent:execute": true}))
|
||||||
_, _, err := manager.CallTool(ctx, "server::tool", map[string]interface{}{})
|
_, executionID, err := manager.CallTool(ctx, "server::tool", map[string]interface{}{})
|
||||||
if err == nil || !strings.Contains(err.Error(), "authorization denied") {
|
if err == nil || !strings.Contains(err.Error(), "authorization denied") {
|
||||||
t.Fatalf("external call bypassed authorizer: %v", err)
|
t.Fatalf("external call bypassed authorizer: %v", err)
|
||||||
}
|
}
|
||||||
|
if executionID == "" {
|
||||||
|
t.Fatal("denied external call should still return an execution id")
|
||||||
|
}
|
||||||
|
execution, ok := manager.GetExecution(executionID)
|
||||||
|
if !ok || execution == nil {
|
||||||
|
t.Fatalf("missing denied external execution %q", executionID)
|
||||||
|
}
|
||||||
|
if execution.Status != ToolExecutionStatusFailed || !strings.Contains(execution.Error, "denied by policy") {
|
||||||
|
t.Fatalf("denied external execution = %#v, want failed with policy error", execution)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExternalMCPManager_AddOrUpdateConfig(t *testing.T) {
|
func TestExternalMCPManager_AddOrUpdateConfig(t *testing.T) {
|
||||||
|
|||||||
+15
-19
@@ -895,25 +895,6 @@ func (s *Server) GetAllTools() []Tool {
|
|||||||
|
|
||||||
// CallTool 直接调用工具(用于内部调用)
|
// CallTool 直接调用工具(用于内部调用)
|
||||||
func (s *Server) CallTool(ctx context.Context, toolName string, args map[string]interface{}) (*ToolResult, string, error) {
|
func (s *Server) CallTool(ctx context.Context, toolName string, args map[string]interface{}) (*ToolResult, string, error) {
|
||||||
_, authenticated := authctx.PrincipalFromContext(ctx)
|
|
||||||
s.mu.RLock()
|
|
||||||
authorizer := s.toolAuthorizer
|
|
||||||
s.mu.RUnlock()
|
|
||||||
if authorizer != nil {
|
|
||||||
if err := authorizer(ctx, toolName, args); err != nil {
|
|
||||||
return nil, "", fmt.Errorf("tool authorization denied: %w", err)
|
|
||||||
}
|
|
||||||
} else if authenticated {
|
|
||||||
return nil, "", errors.New("tool authorization policy is not configured")
|
|
||||||
}
|
|
||||||
s.mu.RLock()
|
|
||||||
handler, exists := s.tools[toolName]
|
|
||||||
s.mu.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return nil, "", fmt.Errorf("工具 %s 未找到", toolName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.executionService == nil {
|
if s.executionService == nil {
|
||||||
s.executionService = NewExecutionService(s.storage, s.logger)
|
s.executionService = NewExecutionService(s.storage, s.logger)
|
||||||
s.executionService.ConfigureToolResultMaxBytes(s.toolResultMaxBytes)
|
s.executionService.ConfigureToolResultMaxBytes(s.toolResultMaxBytes)
|
||||||
@@ -929,6 +910,21 @@ func (s *Server) CallTool(ctx context.Context, toolName string, args map[string]
|
|||||||
ConversationID: MCPConversationIDFromContext(ctx),
|
ConversationID: MCPConversationIDFromContext(ctx),
|
||||||
OwnerUserID: ownerUserID,
|
OwnerUserID: ownerUserID,
|
||||||
Run: func(runCtx context.Context) (*ToolResult, error) {
|
Run: func(runCtx context.Context) (*ToolResult, error) {
|
||||||
|
_, authenticated := authctx.PrincipalFromContext(runCtx)
|
||||||
|
s.mu.RLock()
|
||||||
|
authorizer := s.toolAuthorizer
|
||||||
|
handler, exists := s.tools[toolName]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
if authorizer != nil {
|
||||||
|
if err := authorizer(runCtx, toolName, args); err != nil {
|
||||||
|
return nil, fmt.Errorf("tool authorization denied: %w", err)
|
||||||
|
}
|
||||||
|
} else if authenticated {
|
||||||
|
return nil, errors.New("tool authorization policy is not configured")
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("工具 %s 未找到", toolName)
|
||||||
|
}
|
||||||
return handler(runCtx, args)
|
return handler(runCtx, args)
|
||||||
},
|
},
|
||||||
OnDone: func(exec *ToolExecution) {
|
OnDone: func(exec *ToolExecution) {
|
||||||
|
|||||||
@@ -23,9 +23,20 @@ func TestToolAuthorizerIsUniversalAndExecutionKeepsOwner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if _, _, err := server.CallTool(context.Background(), "echo", nil); err == nil {
|
_, deniedExecutionID, err := server.CallTool(context.Background(), "echo", nil)
|
||||||
|
if err == nil {
|
||||||
t.Fatal("tool call without principal was allowed")
|
t.Fatal("tool call without principal was allowed")
|
||||||
}
|
}
|
||||||
|
if deniedExecutionID == "" {
|
||||||
|
t.Fatal("denied tool call should still return an execution id")
|
||||||
|
}
|
||||||
|
deniedExecution, ok := server.GetExecution(deniedExecutionID)
|
||||||
|
if !ok || deniedExecution == nil {
|
||||||
|
t.Fatalf("missing denied execution %q", deniedExecutionID)
|
||||||
|
}
|
||||||
|
if deniedExecution.Status != ToolExecutionStatusFailed || !strings.Contains(deniedExecution.Error, "principal required") {
|
||||||
|
t.Fatalf("denied execution = %#v, want failed with authorization error", deniedExecution)
|
||||||
|
}
|
||||||
ctx := authctx.WithPrincipal(context.Background(), authctx.NewPrincipal("u1", "user", "assigned", map[string]bool{"mcp:execute": true}))
|
ctx := authctx.WithPrincipal(context.Background(), authctx.NewPrincipal("u1", "user", "assigned", map[string]bool{"mcp:execute": true}))
|
||||||
_, executionID, err := server.CallTool(ctx, "echo", nil)
|
_, executionID, err := server.CallTool(ctx, "echo", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user