diff --git a/internal/multiagent/eino_adk_run_loop.go b/internal/multiagent/eino_adk_run_loop.go index 9848bc05..68152f98 100644 --- a/internal/multiagent/eino_adk_run_loop.go +++ b/internal/multiagent/eino_adk_run_loop.go @@ -200,6 +200,7 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs markPendingWithMonitor := func(tc toolCallPendingInfo) { markPending(tc) beginEinoADKFilesystemToolMonitor( + ctx, args.FilesystemMonitorAgent, args.FilesystemMonitorRecord, args.MCPExecutionBinder, @@ -342,7 +343,7 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs toolCallID = tid } recordPendingExecuteStdoutDup(toolName, content, isErr) - recordEinoADKFilesystemToolMonitor(args.FilesystemMonitorAgent, args.FilesystemMonitorRecord, args.MCPExecutionBinder, toolName, toolCallID, runAccumulatedMsgs, content, isErr) + recordEinoADKFilesystemToolMonitor(ctx, args.FilesystemMonitorAgent, args.FilesystemMonitorRecord, args.MCPExecutionBinder, toolName, toolCallID, runAccumulatedMsgs, content, isErr) if args.FilesystemMonitorAgent != nil && args.MCPExecutionBinder != nil { if execID := args.MCPExecutionBinder.ExecutionID(toolCallID); execID != "" { args.FilesystemMonitorAgent.UpdateMCPExecutionDisplayResult(execID, content) diff --git a/internal/multiagent/eino_execute_monitor.go b/internal/multiagent/eino_execute_monitor.go index ecc919ee..7450a344 100644 --- a/internal/multiagent/eino_execute_monitor.go +++ b/internal/multiagent/eino_execute_monitor.go @@ -1,6 +1,7 @@ package multiagent import ( + "context" "fmt" "cyberstrike-ai/internal/agent" @@ -9,7 +10,7 @@ import ( // newEinoExecuteMonitorCallbacks 在 Eino filesystem execute 开始/结束时写入 MCP 监控库并 recorder(executionId), // 与 CallTool 路径一致,使监控页能展示「执行中」状态。 -func newEinoExecuteMonitorCallbacks(ag *agent.Agent, recorder einomcp.ExecutionRecorder) ( +func newEinoExecuteMonitorCallbacks(ctx context.Context, ag *agent.Agent, recorder einomcp.ExecutionRecorder) ( begin func(toolCallID, command string) string, finish func(executionID, toolCallID, command, stdout string, success bool, invokeErr error), ) { @@ -18,7 +19,7 @@ func newEinoExecuteMonitorCallbacks(ag *agent.Agent, recorder einomcp.ExecutionR return "" } args := map[string]interface{}{"command": command} - id := ag.BeginLocalToolExecution("execute", args) + id := ag.BeginLocalToolExecution(ctx, "execute", args) if id != "" && recorder != nil { recorder(id, toolCallID) } @@ -37,7 +38,7 @@ func newEinoExecuteMonitorCallbacks(ag *agent.Agent, recorder einomcp.ExecutionR } } args := map[string]interface{}{"command": command} - id := ag.FinishLocalToolExecution(executionID, "execute", args, stdout, err) + id := ag.FinishLocalToolExecution(ctx, executionID, "execute", args, stdout, err) if id != "" && recorder != nil && executionID == "" { recorder(id, toolCallID) } diff --git a/internal/multiagent/eino_filesystem_tool_monitor.go b/internal/multiagent/eino_filesystem_tool_monitor.go index 3cbeddd6..d69fcc5c 100644 --- a/internal/multiagent/eino_filesystem_tool_monitor.go +++ b/internal/multiagent/eino_filesystem_tool_monitor.go @@ -1,6 +1,7 @@ package multiagent import ( + "context" "encoding/json" "errors" "strings" @@ -65,6 +66,7 @@ func toolCallArgsFromAccumulated(msgs []adk.Message, toolCallID, expectToolName // beginEinoADKFilesystemToolMonitor 在 Eino ADK filesystem 工具开始调用时写入 running 状态。 func beginEinoADKFilesystemToolMonitor( + ctx context.Context, ag *agent.Agent, rec einomcp.ExecutionRecorder, binder *MCPExecutionBinder, @@ -85,7 +87,7 @@ func beginEinoADKFilesystemToolMonitor( return } storedName := "eino_fs::" + strings.ToLower(name) - id := ag.BeginLocalToolExecution(storedName, map[string]interface{}{}) + id := ag.BeginLocalToolExecution(ctx, storedName, map[string]interface{}{}) if id == "" { return } @@ -97,6 +99,7 @@ func beginEinoADKFilesystemToolMonitor( // recordEinoADKFilesystemToolMonitor 将 Eino ADK filesystem 中间件工具结果写入 MCP 监控(与 execute / MCP 桥芯片一致)。 func recordEinoADKFilesystemToolMonitor( + ctx context.Context, ag *agent.Agent, rec einomcp.ExecutionRecorder, binder *MCPExecutionBinder, @@ -131,7 +134,7 @@ func recordEinoADKFilesystemToolMonitor( if binder != nil { execID = binder.ExecutionID(toolCallID) } - id := ag.FinishLocalToolExecution(execID, storedName, args, resultText, invErr) + id := ag.FinishLocalToolExecution(ctx, execID, storedName, args, resultText, invErr) if id != "" && execID == "" { rec(id, toolCallID) } diff --git a/internal/multiagent/eino_single_runner.go b/internal/multiagent/eino_single_runner.go index 6489dd86..893d4ea7 100644 --- a/internal/multiagent/eino_single_runner.go +++ b/internal/multiagent/eino_single_runner.go @@ -82,7 +82,7 @@ func RunEinoSingleChatModelAgent( } toolInvokeNotify := einomcp.NewToolInvokeNotifyHolder() - einoExecBegin, einoExecFinish := newEinoExecuteMonitorCallbacks(ag, recorder) + einoExecBegin, einoExecFinish := newEinoExecuteMonitorCallbacks(ctx, ag, recorder) mainDefs := ag.ToolsForRole(roleTools) mainTools, err := einomcp.ToolsFromDefinitions(ag, holder, mainDefs, recorder, nil, toolInvokeNotify, einoSingleAgentName) if err != nil { @@ -162,6 +162,7 @@ func RunEinoSingleChatModelAgent( Tools: mainToolsForCfg, UnknownToolsHandler: einomcp.UnknownToolReminderHandler(), ToolCallMiddlewares: []compose.ToolMiddleware{ + localToolRBACMiddleware(), hitlToolCallMiddleware(), softRecoveryToolMiddleware(), }, diff --git a/internal/multiagent/rbac_tool_middleware.go b/internal/multiagent/rbac_tool_middleware.go new file mode 100644 index 00000000..9114153d --- /dev/null +++ b/internal/multiagent/rbac_tool_middleware.go @@ -0,0 +1,50 @@ +package multiagent + +import ( + "context" + "strings" + + "cyberstrike-ai/internal/authctx" + + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +func isLocalPrivilegeTool(name string) bool { + switch strings.ToLower(strings.TrimSpace(name)) { + case "execute", "ls", "read_file", "write_file", "edit_file", "glob", "grep": + return true + default: + return false + } +} + +func localToolPermissionDenied(ctx context.Context, name string) bool { + if !isLocalPrivilegeTool(name) { + return false + } + principal, ok := authctx.PrincipalFromContext(ctx) + return !ok || !principal.HasPermission("agent:local-execute") +} + +func localToolRBACMiddleware() compose.ToolMiddleware { + denied := "Permission denied: agent:local-execute is required for local filesystem and shell tools." + return compose.ToolMiddleware{ + Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) { + if input != nil && localToolPermissionDenied(ctx, input.Name) { + return &compose.ToolOutput{Result: denied}, nil + } + return next(ctx, input) + } + }, + Streamable: func(next compose.StreamableToolEndpoint) compose.StreamableToolEndpoint { + return func(ctx context.Context, input *compose.ToolInput) (*compose.StreamToolOutput, error) { + if input != nil && localToolPermissionDenied(ctx, input.Name) { + return &compose.StreamToolOutput{Result: schema.StreamReaderFromArray([]string{denied})}, nil + } + return next(ctx, input) + } + }, + } +} diff --git a/internal/multiagent/rbac_tool_middleware_test.go b/internal/multiagent/rbac_tool_middleware_test.go new file mode 100644 index 00000000..874127ce --- /dev/null +++ b/internal/multiagent/rbac_tool_middleware_test.go @@ -0,0 +1,22 @@ +package multiagent + +import ( + "context" + "testing" + + "cyberstrike-ai/internal/authctx" +) + +func TestLocalToolPermissionIsSeparateFromAgentExecution(t *testing.T) { + agentOnly := authctx.WithPrincipal(context.Background(), authctx.NewPrincipal("robot:u1", "robot", "own", map[string]bool{"agent:execute": true})) + if !localToolPermissionDenied(agentOnly, "execute") || !localToolPermissionDenied(agentOnly, "read_file") { + t.Fatal("agent:execute alone authorized local privileged tools") + } + local := authctx.WithPrincipal(context.Background(), authctx.NewPrincipal("u1", "user", "assigned", map[string]bool{"agent:local-execute": true})) + if localToolPermissionDenied(local, "execute") || localToolPermissionDenied(local, "write_file") { + t.Fatal("agent:local-execute was not honored") + } + if localToolPermissionDenied(agentOnly, "record_vulnerability") { + t.Fatal("non-local tool was incorrectly denied") + } +} diff --git a/internal/multiagent/runner.go b/internal/multiagent/runner.go index f29c7f80..fbcb9831 100644 --- a/internal/multiagent/runner.go +++ b/internal/multiagent/runner.go @@ -135,7 +135,7 @@ func RunDeepAgent( mcpIDs = append(mcpIDs, id) mcpIDsMu.Unlock() } - einoExecBegin, einoExecFinish := newEinoExecuteMonitorCallbacks(ag, recorder) + einoExecBegin, einoExecFinish := newEinoExecuteMonitorCallbacks(ctx, ag, recorder) // 与单代理流式一致:在 response_start / response_delta 的 data 中带当前 mcpExecutionIds,供主聊天绑定复制与展示。 snapshotMCPIDs := func() []string { @@ -279,6 +279,7 @@ func RunDeepAgent( Tools: subToolsForCfg, UnknownToolsHandler: einomcp.UnknownToolReminderHandler(), ToolCallMiddlewares: []compose.ToolMiddleware{ + localToolRBACMiddleware(), hitlToolCallMiddleware(), softRecoveryToolMiddleware(), }, @@ -438,6 +439,7 @@ func RunDeepAgent( Tools: mainToolsForCfg, UnknownToolsHandler: einomcp.UnknownToolReminderHandler(), ToolCallMiddlewares: []compose.ToolMiddleware{ + localToolRBACMiddleware(), hitlToolCallMiddleware(), softRecoveryToolMiddleware(), },