internal/app/tool_catalog.go
Rama main · réplica de lectura del repositorio original.
package app
import (
"context"
"crypto/rand"
"encoding/hex"
"log/slog"
"net/http"
"strings"
"time"
"github.com/yeomyeonggeori/blueclaw/internal/agentruntime"
"github.com/yeomyeonggeori/blueclaw/internal/approvalgate"
"github.com/yeomyeonggeori/blueclaw/internal/approvalreply"
"github.com/yeomyeonggeori/blueclaw/internal/capability"
"github.com/yeomyeonggeori/blueclaw/internal/config"
"github.com/yeomyeonggeori/blueclaw/internal/mcpserver"
"github.com/yeomyeonggeori/blueclaw/internal/task"
"github.com/yeomyeonggeori/blueprotocol/model"
"github.com/yeomyeonggeori/blueprotocol/toolcontract"
)
type toolCatalogEndpoint struct {
resolver *mcpserver.SessionTokenRequesterResolver
handler http.Handler
approvalGate *approvalgate.Gate
replyReader approvalreply.DecisionModelReader
scriptHost *agentruntime.ScriptHost
}
func newToolCatalogEndpoint(taskRunService *task.TaskRunService, newQuestionWorder QuestionWorderFactory, approvalLanguageModel model.LanguageModelProvider, decisionModel model.DecisionModel, capabilityClient capability.Client) toolCatalogEndpoint {
resolver := mcpserver.NewSessionTokenRequesterResolver(newToolCatalogSessionToken)
handler := mcpserver.NewToolCatalogHandler(resolver, "1")
approvalGate := approvalgate.New(taskRunService)
if newQuestionWorder != nil {
approvalGate.UseQuestionWorder(newQuestionWorder(approvalLanguageModel))
}
approvalGate.UseApprovalTargetResolver(agentruntime.NewCapabilityApprovalTargetResolver(capabilityClient))
return toolCatalogEndpoint{resolver: resolver, handler: handler, approvalGate: approvalGate, replyReader: approvalreply.NewDecisionModelReader(decisionModel), scriptHost: agentruntime.NewScriptHost()}
}
func newToolCatalogBuilder(runtimeConfiguration config.RuntimeConfiguration, kernel agentKernel, services taskServices, directory identityDirectory, memoryComponents memoryComponents, logger *slog.Logger) *agentruntime.ToolCatalogBuilder {
logger.Info("application.initializing", "stage", "tool_catalog")
toolCatalogBuilder := agentruntime.NewToolCatalogBuilder()
toolCatalogBuilder.UseCapabilityQuarantineReporter(func(quarantinedProvider toolcontract.QuarantinedToolProvider) {
logCapabilityProviderQuarantine(logger, quarantinedProvider)
})
toolCatalogBuilder.UseCapabilityToolShadowReporter(func(shadowedTool agentruntime.ShadowedCapabilityTool) {
logShadowedCapabilityTool(logger, shadowedTool)
})
toolCatalogBuilder.UseCapabilityRegistry(kernel.capabilityClient, kernel.capabilityRegistry)
toolCatalogBuilder.UseRecordCatalogDivergenceReporter(func(divergence agentruntime.RecordCatalogDivergence) {
logRecordCatalogDivergence(logger, divergence)
})
warmCapabilityRegistry(kernel.capabilityRegistry)
toolCatalogBuilder.UseAllowedToolNamesByProfile(deriveAllowedToolNamesByProfile(runtimeConfiguration), agentruntime.AlwaysAllowedToolNames())
toolCatalogBuilder.UseSkillSearch(kernel.skillRetriever, kernel.instructionBundleLoader)
toolCatalogBuilder.UseTerminalService(kernel.terminalService)
toolCatalogBuilder.UseTaskRunService(services.taskRunService)
toolCatalogBuilder.UseTaskArtifactService(services.taskArtifactService)
toolCatalogBuilder.UseWorkspaceRootPath(runtimeConfiguration.Terminal.WorkspaceRootPath)
toolCatalogBuilder.UseSkillChangeHandler(kernel.refreshSkillIndex)
toolCatalogBuilder.UseScriptHost(kernel.toolCatalog.scriptHost, scriptHostURL(runtimeConfiguration))
toolCatalogBuilder.UseScriptModels(kernel.decisionModel, newConfiguredVisualReviewModel(runtimeConfiguration, logger), kernel.taskTierLanguageModels.Medium)
if memoryComponents.stores != nil {
toolCatalogBuilder.UseMemoryStores(memoryComponents.stores, directory.identityService)
}
return toolCatalogBuilder
}
func logCapabilityProviderQuarantine(logger *slog.Logger, quarantinedProvider toolcontract.QuarantinedToolProvider) {
if logger == nil {
return
}
logger.Warn("capability.provider.quarantined", "providerID", quarantinedProvider.ProviderID, "reason", quarantinedProvider.Reason)
}
func logShadowedCapabilityTool(logger *slog.Logger, shadowedTool agentruntime.ShadowedCapabilityTool) {
if logger == nil {
return
}
logger.Warn("capability.tool.shadowed",
"canonicalName", shadowedTool.CanonicalName,
"modelName", shadowedTool.ModelName,
)
}
func logRecordCatalogDivergence(logger *slog.Logger, divergence agentruntime.RecordCatalogDivergence) {
if logger == nil {
return
}
logger.Warn("record.catalog.divergence",
"requesterEmail", divergence.RequesterEmail,
"discoveredOnly", divergence.DiscoveredOnly,
"stampedOnly", divergence.StampedOnly,
"discoveryFailed", divergence.DiscoveryFailed,
)
}
func warmCapabilityRegistry(capabilityRegistry *agentruntime.CapabilityRegistry) {
warmContext, cancelWarm := context.WithTimeout(context.Background(), 3*time.Second)
defer cancelWarm()
capabilityRegistry.Warm(warmContext)
}
func capabilityToolDescriptors(toolDescriptors []config.CapabilityToolDescriptor) []agentruntime.CapabilityToolDescriptor {
catalogToolDescriptors := []agentruntime.CapabilityToolDescriptor{}
for _, toolDescriptor := range toolDescriptors {
toolDescriptor.Name = strings.TrimSpace(toolDescriptor.Name)
toolDescriptor.Description = strings.TrimSpace(toolDescriptor.Description)
if toolDescriptor.Name == "" {
continue
}
catalogToolDescriptors = append(catalogToolDescriptors, toolDescriptor)
}
return catalogToolDescriptors
}
func capabilityCompletionEvidence(completionEvidence *config.CapabilityCompletionEvidence) *agentruntime.CapabilityCompletionEvidence {
if completionEvidence == nil {
return nil
}
return &agentruntime.CapabilityCompletionEvidence{
Mode: completionEvidence.Mode,
Action: completionEvidence.Action,
TargetKind: completionEvidence.TargetKind,
}
}
func deriveAllowedToolNamesByProfile(runtimeConfiguration config.RuntimeConfiguration) map[string][]string {
allowedToolNamesByProfile := map[string][]string{}
for _, agentProfile := range runtimeConfiguration.AgentProfiles {
profileName := strings.TrimSpace(agentProfile.Name)
if profileName == "" {
profileName = "default"
}
allowedToolNamesByProfile[profileName] = appendDefaultBuiltInToolNames(agentProfile.AllowedToolNames)
}
return allowedToolNamesByProfile
}
func appendDefaultBuiltInToolNames(toolNames []string) []string {
allowedToolNames := agentruntime.AlwaysAllowedToolNames()
for _, toolName := range toolNames {
trimmedToolName := strings.TrimSpace(toolName)
if trimmedToolName == "" || containsString(allowedToolNames, trimmedToolName) {
continue
}
allowedToolNames = append(allowedToolNames, trimmedToolName)
}
return allowedToolNames
}
func containsString(values []string, expectedValue string) bool {
for _, value := range values {
if strings.TrimSpace(value) == expectedValue {
return true
}
}
return false
}
func newToolCatalogSessionToken() string {
sessionToken := make([]byte, 32)
if _, errorValue := rand.Read(sessionToken); errorValue != nil {
return ""
}
return hex.EncodeToString(sessionToken)
}
func scriptHostURL(runtimeConfiguration config.RuntimeConfiguration) string {
return "http://" + deriveListenAddress(runtimeConfiguration.BaseURL) + agentruntime.ScriptHostPath
}
func toolCatalogURL(runtimeConfiguration config.RuntimeConfiguration) string {
if configuredURL := strings.TrimSpace(runtimeConfiguration.Agent.Harness.ToolCatalogURL); configuredURL != "" {
return configuredURL
}
return "http://" + deriveListenAddress(runtimeConfiguration.BaseURL) + "/harness/tool-catalog"
}