package app import ( "context" "net/http" "strings" "github.com/yeomyeonggeori/blueclaw/internal/adminapi" "github.com/yeomyeonggeori/blueclaw/internal/agentruntime" "github.com/yeomyeonggeori/blueclaw/internal/config" "github.com/yeomyeonggeori/blueclaw/internal/httpserver" "github.com/yeomyeonggeori/blueclaw/internal/policy" "github.com/yeomyeonggeori/blueclaw/internal/runtimecontrol" "github.com/yeomyeonggeori/blueclaw/internal/sessionquery" "github.com/yeomyeonggeori/blueclaw/internal/store/postgres" "github.com/yeomyeonggeori/blueclaw/internal/userapi" ) func newRouterDependencies(components applicationComponents) httpserver.RouterDependencies { runtimeConfiguration := components.runtimeConfiguration directory := components.directory services := components.services kernel := components.kernel return httpserver.RouterDependencies{ ServiceTokenPath: runtimeConfiguration.ServiceTokenPath, HealthHandler: newHealthHandler(components), WorkspaceFilesHandler: newWorkspaceFilesHandler(runtimeConfiguration, kernel, directory), PersonaHandler: newPersonaHandler(runtimeConfiguration, kernel, directory), ToolCatalogHandler: kernel.toolCatalog.handler, ScriptHostHandler: kernel.toolCatalog.scriptHost.Handler(), PolicyHandler: newPolicyHandler(components), IdentityResolve: adminapi.IdentityResolveHandler{PolicyWatcher: directory.policyWatcher, PlatformAccountLister: directory.platformAccountLister}, AuditHandler: services.auditHandler, AttentionHandler: adminapi.AttentionHandler{LanguageModel: kernel.taskTierLanguageModels.High}, TaskMonitorHandler: newTaskMonitorHandler(services, directory, components.connectorRuntime), TaskSearchHandler: adminapi.TaskSearchHandler{SessionQuery: sessionquery.New(services.taskRunService)}, TaskRunHandler: newTaskRunHandler(runtimeConfiguration, services, directory, components.taskLauncher, components.taskIntakeController), HarnessStatusHandler: newHarnessStatusHandler(runtimeConfiguration, kernel.harnessName), SkillInventoryHandler: newSkillInventoryHandler(runtimeConfiguration, kernel.capabilityRegistry), LearningHandler: newLearningHandler(components), ToolInventoryHandler: adminapi.ToolInventoryHandler{ToolCatalogBuilder: components.toolCatalogBuilder}, TaskApprovalHandler: newTaskApprovalHandler(services, directory, components.taskLauncher, components.connectorRuntime), HeldCallRedemption: adminapi.HeldCallRedemptionHandler{TaskRunService: services.taskRunService}, QuiesceHandler: adminapi.QuiesceHandler{ Controller: components.taskIntakeController, TaskRunService: services.taskRunService, }, ScheduleHandler: newScheduleHandler(runtimeConfiguration, services, directory), ConnectorDiagnostics: adminapi.ConnectorEventDiagnosticHandler{Repository: services.repositories.connectorEventDiagnostic}, LLMCallExchange: llmCallExchangeHandler(services.repositories.llmCall), TurnInput: turnInputHandler(services.repositories.taskEvent), ConversationReset: adminapi.ConversationResetHandler{Repository: services.repositories.conversationReset}, MemoryHandler: adminapi.MemoryHandler{Stores: components.memory.stores, IdentityService: directory.identityService}, BackupHandler: adminapi.BackupHandler{Coordinator: components.backupCoordinator}, TaskInboxHandler: userapi.TaskInboxHandler{TaskRunService: services.taskRunService, TaskStepService: services.taskStepService, TaskAuthService: services.taskAuthService}, TaskActionHandler: userapi.TaskActionHandler{TaskRunService: services.taskRunService, TaskAuthService: services.taskAuthService}, SSEHandler: httpserver.SSEHandler{TaskEventService: services.taskEventService}, ConnectorEventHandler: components.connectorEventHandler, AgentReplyHandler: httpserver.AgentReplyHandler{ReplyStore: components.agentReplyStore}, } } func newHealthHandler(components applicationComponents) httpserver.HealthHandler { return httpserver.HealthHandler{ LanguageModel: languageModelHealth(components.kernel), Database: components.foundation.database, ConnectorRuntime: components.connectorRuntime, MaximumBacklog: 1000, ProtocolIdentity: components.protocolIdentity.status, ProtocolIdentityChecker: &components.protocolIdentity.checker, ProtocolIdentityExpected: components.protocolIdentity.expected, } } func languageModelHealth(kernel agentKernel) httpserver.LanguageModelHealth { if kernel.languageModelError != nil { return httpserver.LanguageModelHealth{Error: kernel.languageModelError.Error()} } return httpserver.LanguageModelHealth{Configured: kernel.intakeLanguageModelProvider != nil || kernel.taskTierLanguageModels.High != nil} } func newWorkspaceFilesHandler(runtimeConfiguration config.RuntimeConfiguration, kernel agentKernel, directory identityDirectory) httpserver.WorkspaceFilesHandler { return httpserver.WorkspaceFilesHandler{ WorkspaceRootPath: runtimeConfiguration.Terminal.WorkspaceRootPath, WorkspaceActorFactory: kernel.terminalService.WorkspaceActorFactory(), PersonAccessResolver: directory.identityService, } } func newPersonaHandler(runtimeConfiguration config.RuntimeConfiguration, kernel agentKernel, directory identityDirectory) httpserver.PersonaHandler { return httpserver.PersonaHandler{ WorkspaceRootPath: runtimeConfiguration.Terminal.WorkspaceRootPath, WorkspaceActorFactory: kernel.terminalService.WorkspaceActorFactory(), PersonAccessResolver: directory.identityService, AuthorizeRequest: personaRequestAuthorizer(runtimeConfiguration.Memory.AdminAssertionKeyPath), } } func personaRequestAuthorizer(keyPath string) func(*http.Request) bool { reader := signedPersonaReader(keyPath) return func(request *http.Request) bool { principal := reader(request) switch { case request.Method == http.MethodGet && request.URL.Path == "/admin/api/persona/user": return principal != "" && principal == request.URL.Query().Get("personID") case request.Method == http.MethodPut && request.URL.Path == "/admin/api/persona/user": return principal != "" && principal == request.URL.Query().Get("personID") case request.Method == http.MethodPost && request.URL.Path == "/admin/api/persona/user": return principal == "internkim-persona-seed" case request.Method == http.MethodGet && request.URL.Path == "/admin/api/persona/agent": return principal == "internkim-persona-service" case request.Method == http.MethodPost && request.URL.Path == "/admin/api/persona/agent": return principal == "internkim-persona-service" case request.Method == http.MethodPut && request.URL.Path == "/admin/api/persona/agent": return principal == "internkim-persona-service" default: return false } } } func newPolicyHandler(components applicationComponents) adminapi.PolicyHandler { return adminapi.PolicyHandler{ PolicyPath: components.policyPath, PolicyLoader: components.foundation.policyLoader, PolicySaver: policy.PolicySaver{}, PolicyWatcher: components.directory.policyWatcher, Validator: policy.PolicyValidator{}, AuditHandler: components.services.auditHandler, PersonReferenceCanonicalizer: components.services.repositories.personReferenceCanonicalizer, PlatformAccountLinker: components.directory.identityService, OnPolicyReload: newPolicyReloadHandler(components), } } func newPolicyReloadHandler(components applicationComponents) func(policy.PolicyDocument) { database := components.foundation.database personRepository := components.services.repositories.person identityService := components.directory.identityService policyProjectionService := components.directory.policyProjectionService posixSynchronizer := components.foundation.posixSynchronizer logger := components.foundation.logger return func(policyDocument policy.PolicyDocument) { if database.SQL != nil { if errorValue := personRepository.ReplacePeople(policyDocument); errorValue != nil { logger.Error("policy.people_projection_failed", "error", errorValue) } } identityService.ReloadPolicyProjection(policyProjectionService.ReplacePolicyProjectionTransactionally(policyDocument)) _ = posixSynchronizer.Synchronize(context.Background()) } } func newTaskMonitorHandler(services taskServices, directory identityDirectory, retryTaskRun adminapi.TaskRunRetryer) adminapi.TaskMonitorHandler { return adminapi.TaskMonitorHandler{ TaskRunService: services.taskRunService, TaskStepService: services.taskStepService, TaskEventService: services.taskEventService, IdentityService: directory.identityService, RetryTaskRun: retryTaskRun, } } func newTaskRunHandler(runtimeConfiguration config.RuntimeConfiguration, services taskServices, directory identityDirectory, taskLauncher *agentruntime.TaskLauncher, taskIntakeController *runtimecontrol.TaskIntakeController) adminapi.TaskRunHandler { return adminapi.TaskRunHandler{ TaskLauncher: taskLauncher, IdentityService: directory.identityService, TaskRunService: services.taskRunService, TaskIntakeGate: taskIntakeController, AllowTaskDecisionPreset: runtimeConfiguration.Agent.AllowAdminTaskDiagnostic, } } func newHarnessStatusHandler(runtimeConfiguration config.RuntimeConfiguration, harnessName string) adminapi.HarnessStatusHandler { return adminapi.HarnessStatusHandler{Status: adminapi.HarnessStatus{ Name: harnessName, AgentCommandPath: runtimeConfiguration.Agent.Harness.AgentCommandPath, RunsAsRequesterIdentity: strings.TrimSpace(runtimeConfiguration.Terminal.POSIXHelperPath) != "", ToolCatalogURL: toolCatalogURL(runtimeConfiguration), }} } func newSkillInventoryHandler(runtimeConfiguration config.RuntimeConfiguration, capabilityRegistry *agentruntime.CapabilityRegistry) adminapi.SkillInventoryHandler { return adminapi.SkillInventoryHandler{InventoryLoader: func() adminapi.SkillInventory { instructions := loadAgentInstructions(runtimeConfiguration, capabilityRegistry) return adminapi.SkillInventory{Loaded: instructions.Bundle.Skills, Unavailable: instructions.UnavailableSkills} }} } func newTaskApprovalHandler(services taskServices, directory identityDirectory, taskLauncher *agentruntime.TaskLauncher, liveHolds adminapi.LiveHolds) adminapi.TaskApprovalHandler { return adminapi.TaskApprovalHandler{ LiveHolds: liveHolds, TaskLauncher: taskLauncher, TaskRunService: services.taskRunService, IdentityService: directory.identityService, } } func newScheduleHandler(runtimeConfiguration config.RuntimeConfiguration, services taskServices, directory identityDirectory) adminapi.ScheduleHandler { return adminapi.ScheduleHandler{ CompanyProvider: directory.companyProvider, SummaryRepository: services.repositories.scheduleSummary, ListRepository: services.repositories.scheduleList, RepairRepository: services.repositories.scheduleCreatorRepair, ReaderPersonID: signedReader(runtimeConfiguration.Memory.AdminAssertionKeyPath, true), } } func llmCallExchangeHandler(repository *postgres.LLMCallRepository) adminapi.LLMCallExchangeHandler { if repository == nil { return adminapi.LLMCallExchangeHandler{} } return adminapi.LLMCallExchangeHandler{Repository: repository} } func turnInputHandler(repository *postgres.TaskEventRepository) adminapi.TurnInputHandler { if repository == nil { return adminapi.TurnInputHandler{} } return adminapi.TurnInputHandler{Repository: repository} }