mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 12:18:59 +00:00
* feat(hooks/observe): add structured output validation hook (ObserveHook) - hooks/types.go: Add ObserveHook interface + BuiltInHookType enum - hooks/dispatcher.go: HookDispatcher emits ObserveHook with PhaseResult payload - pipeline/observe_stage.go: ObserveStage emits hook after ObserveResult built - pipeline/substates.go: ObserveStageResult carries hook results + validation errors - hooks/config.go: BuiltInHookTypeObserve added to BuiltInHookType enum PhaseResult carries structured output, token usage, tool calls + validation errors emitted post-ObserveStage. ObserveHook implementations can validate structured output against schemas, detect tool-call loops, enforce token budgets, etc. Hook fires after ObserveStage produces ObserveResult, before results propagate to next stage. ValidationError returned by hook halts pipeline and propagates error to caller without further stage execution. Co-Authored-By: Claude <noreply@anthropic.com> * ui(hooks): add post_model_response event to web UI - Add event to Zod schema, filter dropdown, and form dialog - Implement conditional test panel UI for model response payload - Add translations (en/zh/vi) for new test panel fields - Updated beta description to reference the new event Co-Authored-By: Claude <noreply@anthropic.com> * feat(mcp): add MCP CRUD server exposing goclaw resources at /api/mcp/ with Bearer token auth and X-GoClaw-Tenant-Id header, default to master tenant * feat(mcp): add goclaw_skills_write_file tool to edit skill files on disk The CRUD MCP server's goclaw_skills_update only touched skill DB metadata, with no way to edit a skill's SKILL.md/file content on the filesystem. Extract the versioned write logic from the web UI's skill file editor (SkillsHandler.handleWriteFile) into skills.WriteVersionedFile so both surfaces share identical validation and versioning, and expose it as a new MCP tool. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
404 lines
16 KiB
Go
404 lines
16 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/audio"
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
|
"github.com/nextlevelbuilder/goclaw/internal/gateway/methods"
|
|
httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
|
|
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
|
|
"github.com/nextlevelbuilder/goclaw/internal/media"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
"github.com/nextlevelbuilder/goclaw/internal/webhooks"
|
|
)
|
|
|
|
// httpHandlers bundles the results of wireHTTP() for passing to wireHTTPHandlersOnServer.
|
|
type httpHandlers struct {
|
|
agents *httpapi.AgentsHandler
|
|
skills *httpapi.SkillsHandler
|
|
traces *httpapi.TracesHandler
|
|
mcp *httpapi.MCPHandler
|
|
channelInstances *httpapi.ChannelInstancesHandler
|
|
providers *httpapi.ProvidersHandler
|
|
builtinTools *httpapi.BuiltinToolsHandler
|
|
pendingMessages *httpapi.PendingMessagesHandler
|
|
teamEvents *httpapi.TeamEventsHandler
|
|
secureCLI *httpapi.SecureCLIHandler
|
|
secureCLIGrant *httpapi.SecureCLIGrantHandler
|
|
mcpUserCreds *httpapi.MCPUserCredentialsHandler
|
|
mcpOAuth *httpapi.MCPOAuthHandler
|
|
}
|
|
|
|
// wireHTTPHandlersOnServer registers all HTTP handler objects onto the gateway server.
|
|
// Called after wireHTTP() and wireExtras() have returned their results.
|
|
func (d *gatewayDeps) wireHTTPHandlersOnServer(
|
|
h httpHandlers,
|
|
wakeH *httpapi.WakeHandler,
|
|
mcpPool *mcpbridge.Pool,
|
|
postTurn tools.PostTurnProcessor,
|
|
mediaStore *media.Store,
|
|
) {
|
|
if h.providers != nil {
|
|
h.providers.SetAPIBaseFallback(d.cfg.Providers.APIBaseForType)
|
|
}
|
|
if h.agents != nil {
|
|
d.server.SetAgentsHandler(h.agents)
|
|
}
|
|
if h.skills != nil {
|
|
d.server.SetSkillsHandler(h.skills)
|
|
}
|
|
if h.traces != nil {
|
|
d.server.SetTracesHandler(h.traces)
|
|
}
|
|
// External wake/trigger API — wakeH was created by caller before invoking this method.
|
|
d.server.SetWakeHandler(wakeH)
|
|
if h.mcp != nil {
|
|
if mcpPool != nil {
|
|
h.mcp.SetPoolEvictor(mcpPool)
|
|
}
|
|
d.server.SetMCPHandler(h.mcp)
|
|
}
|
|
if h.mcpUserCreds != nil {
|
|
d.server.SetMCPUserCredentialsHandler(h.mcpUserCreds)
|
|
}
|
|
if h.mcpOAuth != nil {
|
|
if mcpPool != nil {
|
|
h.mcpOAuth.SetEvictor(mcpPool)
|
|
}
|
|
d.server.SetMCPOAuthHandler(h.mcpOAuth)
|
|
}
|
|
if h.channelInstances != nil {
|
|
d.server.SetChannelInstancesHandler(h.channelInstances)
|
|
}
|
|
if h.providers != nil {
|
|
d.server.SetProvidersHandler(h.providers)
|
|
}
|
|
if h.teamEvents != nil {
|
|
d.server.SetTeamEventsHandler(h.teamEvents)
|
|
}
|
|
if d.pgStores != nil && d.pgStores.Teams != nil {
|
|
d.server.SetTeamAttachmentsHandler(httpapi.NewTeamAttachmentsHandler(d.pgStores.Teams, d.workspace))
|
|
d.server.SetWorkspaceUploadHandler(httpapi.NewWorkspaceUploadHandler(d.pgStores.Teams, d.workspace, d.msgBus))
|
|
}
|
|
if h.builtinTools != nil {
|
|
d.server.SetBuiltinToolsHandler(h.builtinTools)
|
|
}
|
|
if h.pendingMessages != nil {
|
|
if pc := d.cfg.Channels.PendingCompaction; pc != nil {
|
|
h.pendingMessages.SetKeepRecent(pc.KeepRecent)
|
|
h.pendingMessages.SetMaxTokens(pc.MaxTokens)
|
|
h.pendingMessages.SetProviderModel(pc.Provider, pc.Model)
|
|
}
|
|
d.server.SetPendingMessagesHandler(h.pendingMessages)
|
|
}
|
|
if h.secureCLI != nil {
|
|
d.server.SetSecureCLIHandler(h.secureCLI)
|
|
}
|
|
if h.secureCLIGrant != nil {
|
|
d.server.SetSecureCLIGrantHandler(h.secureCLIGrant)
|
|
}
|
|
if d.pgStores != nil && d.pgStores.BrowserCookies != nil {
|
|
d.server.SetBrowserCookiesHandler(httpapi.NewBrowserCookiesHandler(d.pgStores.BrowserCookies))
|
|
}
|
|
|
|
// Activity audit log API
|
|
if d.pgStores.Activity != nil {
|
|
d.server.SetActivityHandler(httpapi.NewActivityHandler(d.pgStores.Activity))
|
|
}
|
|
|
|
// System configs API
|
|
if d.pgStores.SystemConfigs != nil {
|
|
d.server.SetSystemConfigsHandler(httpapi.NewSystemConfigsHandler(d.pgStores.SystemConfigs, d.msgBus))
|
|
|
|
// Refresh in-memory config when system_configs change via HTTP API
|
|
d.msgBus.Subscribe(bus.TopicSystemConfigChanged, func(evt bus.Event) {
|
|
// Use tenant context from the request that triggered the change
|
|
ctx := context.Background()
|
|
if reqCtx, ok := evt.Payload.(context.Context); ok {
|
|
ctx = reqCtx
|
|
} else {
|
|
ctx = store.WithTenantID(ctx, store.MasterTenantID)
|
|
}
|
|
if sysConfigs, err := d.pgStores.SystemConfigs.List(ctx); err == nil && len(sysConfigs) > 0 {
|
|
d.cfg.ApplySystemConfigs(sysConfigs)
|
|
// Update PGMemoryStore chunk config so new documents use updated settings
|
|
if mem := d.cfg.Agents.Defaults.Memory; mem != nil {
|
|
if pgMem, ok := d.pgStores.Memory.(*pg.PGMemoryStore); ok {
|
|
pgMem.UpdateChunkConfig(mem.MaxChunkLen, mem.ChunkOverlap)
|
|
}
|
|
}
|
|
// Note: vault enrichment provider is resolved per-tenant at runtime,
|
|
// no hot-reload needed here
|
|
slog.Debug("system_configs refreshed to in-memory config", "keys", len(sysConfigs))
|
|
}
|
|
})
|
|
}
|
|
|
|
d.server.SetBrandingAssetsHandler(httpapi.NewBrandingAssetsHandler(d.dataDir))
|
|
|
|
// Usage analytics API
|
|
if d.pgStores.Snapshots != nil {
|
|
d.server.SetUsageHandler(httpapi.NewUsageHandler(d.pgStores.Snapshots, d.pgStores.UsageEvents, d.pgStores.DB))
|
|
}
|
|
if d.pgStores.UsageCaps != nil {
|
|
d.server.SetUsageCapsHandler(httpapi.NewUsageCapsHandler(d.pgStores.UsageCaps, d.pgStores.Tenants))
|
|
}
|
|
|
|
// Runtime package management (install/uninstall system/pip/npm/github packages)
|
|
// Wire the update registry AFTER initGitHubInstaller so DefaultGitHubInstaller() is set.
|
|
initGitHubInstaller()
|
|
pkgHandler := wirePackagesHandler(d)
|
|
d.server.SetPackagesHandler(pkgHandler)
|
|
d.server.SetGatewayUpgradeHandler(httpapi.NewGatewayUpgradeHandlerFromEnv())
|
|
|
|
// API documentation (OpenAPI spec + Swagger UI at /docs)
|
|
d.server.SetDocsHandler(httpapi.NewDocsHandler())
|
|
|
|
// Edition info (public, no auth — used by desktop UI comparison modal)
|
|
d.server.SetEditionHandler(httpapi.NewEditionHandler())
|
|
|
|
if d.pgStores != nil && d.pgStores.APIKeys != nil {
|
|
d.server.SetAPIKeysHandler(httpapi.NewAPIKeysHandler(d.pgStores.APIKeys, d.msgBus))
|
|
d.server.SetAPIKeyStore(d.pgStores.APIKeys)
|
|
httpapi.InitAPIKeyCache(d.pgStores.APIKeys, d.msgBus)
|
|
}
|
|
|
|
// K10: single shared webhookLimiter — one per process enforces per-tenant RPM cap across
|
|
// both LLM and message endpoints. Two separate instances would double the effective cap.
|
|
webhookEncKey := os.Getenv("GOCLAW_ENCRYPTION_KEY")
|
|
|
|
// K6: refuse to mount any webhook handler when GOCLAW_ENCRYPTION_KEY is unset.
|
|
// crypto.Encrypt("", "") returns plaintext unchanged, so an empty key would silently
|
|
// persist raw secrets to the database — defeating the stated DB-leak protection.
|
|
// Skip-mount approach: process still starts (all other subsystems work), but
|
|
// /v1/webhooks/* returns 404. Set GOCLAW_ENCRYPTION_KEY to re-enable webhooks.
|
|
if webhookEncKey == "" {
|
|
slog.Error("webhook subsystem disabled: GOCLAW_ENCRYPTION_KEY not set. Set the env var to enable /v1/webhooks/* endpoints.")
|
|
} else {
|
|
sharedWebhookLimiter := httpapi.NewWebhookLimiter()
|
|
|
|
// Webhook admin CRUD — available in all editions (Standard + Lite).
|
|
// Runtime routes (/v1/webhooks/message, /v1/webhooks/llm) are mounted by phases 05/06.
|
|
// adminH is captured so the test endpoint (POST /v1/webhooks/{id}/test) can be wired
|
|
// with the runtime invokers (llm/message handlers) once they are constructed below.
|
|
var adminH *httpapi.WebhooksAdminHandler
|
|
if d.pgStores != nil && d.pgStores.Webhooks != nil {
|
|
adminH = httpapi.NewWebhooksAdminHandler(
|
|
d.pgStores.Webhooks,
|
|
d.pgStores.WebhookCalls,
|
|
d.pgStores.Tenants,
|
|
d.msgBus,
|
|
)
|
|
adminH.SetEncKey(webhookEncKey)
|
|
d.server.SetWebhooksAdminHandler(adminH)
|
|
}
|
|
|
|
// Webhook message endpoint — Standard edition only (channels required).
|
|
// Phase 05b: POST /v1/webhooks/message → sync channel send (text + optional media).
|
|
var msgH *httpapi.WebhookMessageHandler
|
|
if edition.Current().AllowsChannels() &&
|
|
d.pgStores != nil &&
|
|
d.pgStores.Webhooks != nil &&
|
|
d.pgStores.WebhookCalls != nil &&
|
|
d.pgStores.ChannelInstances != nil &&
|
|
d.channelMgr != nil {
|
|
msgH = httpapi.NewWebhookMessageHandler(
|
|
d.channelMgr,
|
|
d.pgStores.ChannelInstances,
|
|
d.pgStores.WebhookCalls,
|
|
d.pgStores.Webhooks,
|
|
sharedWebhookLimiter, // K10: shared limiter
|
|
)
|
|
msgH.SetEncKey(webhookEncKey) // K6: decrypt secret at HMAC verify time
|
|
d.server.SetWebhookMessageHandler(msgH)
|
|
}
|
|
|
|
// Webhook LLM endpoint — all editions (Standard + Lite).
|
|
// Phase 06: POST /v1/webhooks/llm → sync agent run (≤30s) or async enqueue.
|
|
// LocalhostOnly enforcement is handled by WebhookAuthMiddleware at request time.
|
|
// lane=nil → handler self-creates internal default lane (4-slot).
|
|
var llmH *httpapi.WebhookLLMHandler
|
|
if d.pgStores != nil &&
|
|
d.pgStores.Webhooks != nil &&
|
|
d.pgStores.WebhookCalls != nil &&
|
|
d.agentRouter != nil {
|
|
llmH = httpapi.NewWebhookLLMHandler(
|
|
d.agentRouter,
|
|
d.pgStores.WebhookCalls,
|
|
d.pgStores.Webhooks,
|
|
sharedWebhookLimiter, // K10: shared limiter
|
|
nil, // lane: nil → internal default (4-slot); configurable in future via cfg
|
|
webhooks.ResolveTimeoutSec(d.cfg.Gateway.WebhookSyncTimeoutSec),
|
|
webhooks.ResolveStream(d.cfg.Gateway.WebhookStream),
|
|
)
|
|
llmH.SetEncKey(webhookEncKey) // K6: decrypt secret at HMAC verify time
|
|
d.server.SetWebhookLLMHandler(llmH)
|
|
}
|
|
|
|
// Wire the admin test endpoint with runtime invokers. msgH is nil on Lite — the
|
|
// admin handler guards on nil and rejects message tests there.
|
|
if adminH != nil {
|
|
adminH.SetTesters(llmH, msgH)
|
|
}
|
|
}
|
|
|
|
// Allow browser-paired users to access HTTP APIs
|
|
if d.pgStores.Pairing != nil {
|
|
httpapi.InitPairingAuth(d.pgStores.Pairing)
|
|
}
|
|
|
|
// Memory management API
|
|
if d.pgStores != nil && d.pgStores.Memory != nil {
|
|
d.server.SetMemoryHandler(httpapi.NewMemoryHandler(d.pgStores.Memory, d.pgStores.Agents))
|
|
}
|
|
|
|
// Knowledge graph API
|
|
if d.pgStores != nil && d.pgStores.KnowledgeGraph != nil {
|
|
kgHandler := httpapi.NewKnowledgeGraphHandler(d.pgStores.KnowledgeGraph, d.providerRegistry)
|
|
kgHandler.SetUsageCapService(d.usageCapSvc)
|
|
d.server.SetKnowledgeGraphHandler(kgHandler)
|
|
}
|
|
|
|
// V3: Evolution metrics + suggestions API
|
|
if d.pgStores != nil && d.pgStores.EvolutionMetrics != nil && d.pgStores.EvolutionSuggestions != nil {
|
|
var evoOpts []httpapi.EvolutionHandlerOpt
|
|
if manageStore, ok := d.pgStores.Skills.(store.SkillManageStore); ok && d.skillsLoader != nil {
|
|
evoOpts = append(evoOpts, httpapi.WithSkillCreation(manageStore, d.skillsLoader, d.dataDir))
|
|
}
|
|
if d.pgStores.Agents != nil {
|
|
evoOpts = append(evoOpts, httpapi.WithAgentStore(d.pgStores.Agents))
|
|
}
|
|
if d.pgStores.BuiltinToolTenantCfgs != nil {
|
|
evoOpts = append(evoOpts, httpapi.WithToolTenantCfgs(d.pgStores.BuiltinToolTenantCfgs))
|
|
}
|
|
d.server.SetEvolutionHandler(httpapi.NewEvolutionHandler(d.pgStores.EvolutionMetrics, d.pgStores.EvolutionSuggestions, evoOpts...))
|
|
}
|
|
|
|
// V3: Knowledge Vault document API
|
|
if d.pgStores != nil && d.pgStores.Vault != nil {
|
|
vh := httpapi.NewVaultHandler(d.pgStores.Vault, d.pgStores.Teams, d.workspace, d.domainBus, d.pgStores.Agents, d.pgStores.Teams)
|
|
vh.SetEnrichProgress(d.enrichProgress)
|
|
vh.SetEnrichWorker(d.enrichWorker)
|
|
d.server.SetVaultHandler(vh)
|
|
|
|
// Lightweight graph visualization endpoints (vault + KG).
|
|
var kgGraph store.KGGraphStore
|
|
if d.pgStores.KnowledgeGraph != nil {
|
|
kgGraph = newKGGraphStore(d.pgStores.DB)
|
|
}
|
|
vgHandler := httpapi.NewVaultGraphHandler(
|
|
newVaultGraphStore(d.pgStores.DB), kgGraph, d.pgStores.Teams,
|
|
)
|
|
d.server.SetVaultGraphHandler(vgHandler)
|
|
}
|
|
|
|
// V3: Episodic memory summaries API
|
|
if d.pgStores != nil && d.pgStores.Episodic != nil {
|
|
d.server.SetEpisodicHandler(httpapi.NewEpisodicHandler(d.pgStores.Episodic))
|
|
}
|
|
|
|
// V3: Orchestration mode API (read-only)
|
|
if d.pgStores != nil && d.pgStores.Agents != nil {
|
|
d.server.SetOrchestrationHandler(httpapi.NewOrchestrationHandler(d.pgStores.Agents, d.pgStores.Teams, d.pgStores.AgentLinks))
|
|
}
|
|
|
|
// V3: Per-agent v3 feature flags API
|
|
if d.pgStores != nil && d.pgStores.Agents != nil {
|
|
d.server.SetV3FlagsHandler(httpapi.NewV3FlagsHandler(d.pgStores.Agents))
|
|
}
|
|
|
|
// Workspace file serving endpoint — serves files by absolute path, auth-token protected.
|
|
d.server.SetFilesHandler(httpapi.NewFilesHandler(d.workspace, d.dataDir))
|
|
|
|
// Storage file management — browse/delete files under the resolved workspace directory.
|
|
d.server.SetStorageHandler(httpapi.NewStorageHandler(d.workspace, d.pgStores.Tenants))
|
|
|
|
// Media upload endpoint — accepts multipart file uploads, returns temp path + MIME type.
|
|
d.server.SetMediaUploadHandler(httpapi.NewMediaUploadHandler())
|
|
|
|
// Media serve endpoint — serves persisted media files by ID for WS/web clients.
|
|
if mediaStore != nil {
|
|
d.server.SetMediaServeHandler(httpapi.NewMediaServeHandler(mediaStore))
|
|
}
|
|
|
|
// ElevenLabs voice list + refresh endpoints (GET /v1/voices, POST /v1/voices/refresh).
|
|
// VoiceCache is shared between the HTTP handler and the WS voices.list method.
|
|
// TTL 1h + LRU cap 1000 tenants.
|
|
{
|
|
voiceCache := audio.NewVoiceCache(1*time.Hour, 1000)
|
|
var secretStore store.ConfigSecretsStore
|
|
if d.pgStores != nil && d.pgStores.ConfigSecrets != nil {
|
|
secretStore = d.pgStores.ConfigSecrets
|
|
}
|
|
var tenantStore store.TenantStore
|
|
if d.pgStores != nil && d.pgStores.Tenants != nil {
|
|
tenantStore = d.pgStores.Tenants
|
|
}
|
|
voicesH := httpapi.NewVoicesHandler(voiceCache, secretStore, tenantStore)
|
|
d.server.SetVoicesHandler(voicesH)
|
|
// Wire WS method — provider nil means each request resolves key via secretStore at HTTP layer.
|
|
// For WS, use same cache. Provider is resolved via secretStore at WS level in a future phase.
|
|
methods.NewVoicesMethods(voiceCache, nil).Register(d.server.Router())
|
|
// Wire the same cache + secret store into the CRUD MCP server (see
|
|
// internal/mcp/crud_server.go, mounted at /api/mcp/ in BuildMux()).
|
|
d.server.SetVoiceCache(voiceCache, secretStore)
|
|
}
|
|
|
|
// TTS synthesize endpoint — shares audio.Manager with setupTTS.
|
|
if d.audioMgr != nil {
|
|
ttsH := httpapi.NewTTSHandler(d.audioMgr)
|
|
// Reuse the server's rate limiter (per-IP/token; NOT per-user).
|
|
// Server.RateLimiter() is non-nil by construction (server.go:104).
|
|
if rl := d.server.RateLimiter(); rl != nil && rl.Enabled() {
|
|
ttsH.SetRateLimiter(rl.Allow)
|
|
}
|
|
// Wire stores for per-tenant TTS config lookup at synthesis time.
|
|
if d.pgStores.SystemConfigs != nil && d.pgStores.ConfigSecrets != nil {
|
|
ttsH.SetStores(d.pgStores.SystemConfigs, d.pgStores.ConfigSecrets)
|
|
// Wire tenant resolver for channels TTS auto-apply
|
|
d.audioMgr.SetTenantResolver(httpapi.NewTenantTTSResolver(d.pgStores.SystemConfigs, d.pgStores.ConfigSecrets))
|
|
}
|
|
d.server.SetTTSHandler(ttsH)
|
|
d.ttsHandler = ttsH // store for hot-reload
|
|
}
|
|
|
|
// Per-tenant TTS config endpoint — allows tenant admins to configure TTS.
|
|
if d.pgStores.SystemConfigs != nil && d.pgStores.ConfigSecrets != nil {
|
|
d.server.SetTTSConfigHandler(httpapi.NewTTSConfigHandler(d.pgStores.SystemConfigs, d.pgStores.ConfigSecrets, d.pgStores.Tenants))
|
|
}
|
|
|
|
// Workstations API — Standard edition only.
|
|
// Lite edition MUST NOT expose these routes (silent orphan data + contract violation).
|
|
if edition.Current().Name != "lite" {
|
|
if d.pgStores != nil && d.pgStores.Workstations != nil && d.pgStores.WorkstationLinks != nil {
|
|
wsH := httpapi.NewWorkstationsHandler(
|
|
d.pgStores.Workstations,
|
|
d.pgStores.WorkstationLinks,
|
|
d.pgStores.Tenants,
|
|
)
|
|
if d.pgStores.WorkstationPermissions != nil {
|
|
wsH.SetPermStore(d.pgStores.WorkstationPermissions)
|
|
}
|
|
if d.pgStores.WorkstationActivity != nil {
|
|
wsH.SetActivityStore(d.pgStores.WorkstationActivity)
|
|
}
|
|
d.server.SetWorkstationsHandler(wsH)
|
|
}
|
|
}
|
|
|
|
// Seed + apply builtin tool disables
|
|
if d.pgStores.BuiltinTools != nil {
|
|
seedBuiltinTools(context.Background(), d.pgStores.BuiltinTools)
|
|
migrateBuiltinToolSettings(context.Background(), d.pgStores.BuiltinTools)
|
|
backfillWebFetchSettings(context.Background(), d.pgStores.BuiltinTools)
|
|
applyBuiltinToolDisables(context.Background(), d.pgStores.BuiltinTools, d.toolsReg)
|
|
}
|
|
}
|