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" }