mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-17 10:14:23 +00:00
feat(hooks): WS RPC surface + tenant-scope guards + shared test runner
Exposes agent hooks over WebSocket (hooks.list/create/update/delete/ toggle/test/history) behind existing auth layer. Additions: - Gateway wiring: buildHookHandlers() factory reused by dispatcher and hooks.test runner, so UI test panel exercises production code paths - GetByID tenant-scope guard: non-master callers only see own tenant + global rows (matches List() behavior on both PG and SQLite) - i18n keys + catalogs (en/vi/zh) for hooks error messages - Protocol method constants for hooks.* and config.defaults
This commit is contained in:
1 parent
553340b3a9
commit
aa1b2f026a
13 files changed
+774
-23
No files matched your search
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway/methods"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/hooks"
|
||||
httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
|
||||
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/media"
|
||||
@@ -378,6 +379,18 @@ func runGateway() {
|
||||
server.SetLogTee(logTee)
|
||||
pairingMethods, heartbeatMethods, chatMethods := registerAllMethods(server, agentRouter, pgStores.Sessions, pgStores.Cron, pgStores.Pairing, cfg, cfgPath, workspace, dataDir, msgBus, execApprovalMgr, pgStores.Agents, pgStores.Skills, pgStores.ConfigSecrets, pgStores.Teams, contextFileInterceptor, logTee, pgStores.Heartbeats, pgStores.ConfigPermissions, pgStores.SystemConfigs, pgStores.Tenants, pgStores.SkillTenantCfgs)
|
||||
|
||||
// Phase 3: Agent hooks RPC methods (hooks.list/create/update/delete/toggle/test/history).
|
||||
if hs, ok := pgStores.Hooks.(hooks.HookStore); ok && hs != nil {
|
||||
hm := methods.NewHookMethods(hs, edition.Current())
|
||||
// Reuse dispatcher handlers for dry-run test runner so UI test panel
|
||||
// exercises the exact code that will run in production.
|
||||
if sharedHookHandlers != nil {
|
||||
hm.SetTestRunner(methods.NewDispatcherTestRunner(sharedHookHandlers))
|
||||
}
|
||||
hm.Register(server.Router())
|
||||
slog.Info("registered hooks RPC methods")
|
||||
}
|
||||
|
||||
// Wire post-turn processor for team task dispatch (WS chat.send + HTTP API paths).
|
||||
if postTurn != nil {
|
||||
chatMethods.SetPostTurnProcessor(postTurn)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/hooks"
|
||||
hookhandlers "github.com/nextlevelbuilder/goclaw/internal/hooks/handlers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/hooks/budget"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/security"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
||||
)
|
||||
|
||||
// sharedHookHandlers is populated by wireExtras so the gateway.go router
|
||||
// wiring can reuse the same handler instances for the `hooks.test` runner.
|
||||
// nil when hook store is absent (hooks disabled).
|
||||
var sharedHookHandlers map[hooks.HandlerType]hooks.Handler
|
||||
|
||||
// buildHookHandlers constructs the production handler map used by both the
|
||||
// dispatcher (sync + async chain) and the `hooks.test` test runner. Keeping
|
||||
// this factory single-source ensures test-panel behavior mirrors production.
|
||||
//
|
||||
// Budget wiring (C1 fix): the PromptHandler receives a budget.Store bound
|
||||
// to pg.NewPGHookBudget so token spend is atomically deducted per tenant.
|
||||
// When the DB handle is unavailable, budget falls back to nil (Lite desktop).
|
||||
func buildHookHandlers(stores *store.Stores, providerReg *providers.Registry) map[hooks.HandlerType]hooks.Handler {
|
||||
encryptKey := os.Getenv("GOCLAW_ENCRYPTION_KEY")
|
||||
|
||||
var budgetStore *budget.Store
|
||||
if stores != nil && stores.DB != nil {
|
||||
budgetStore = budget.New(pg.NewPGHookBudget(stores.DB), nil)
|
||||
}
|
||||
|
||||
promptHandler := &hookhandlers.PromptHandler{
|
||||
Resolver: hookhandlers.NewRegistryResolver(providerReg, stores.SystemConfigs),
|
||||
Budget: budgetStore,
|
||||
DefaultModel: "haiku",
|
||||
}
|
||||
|
||||
return map[hooks.HandlerType]hooks.Handler{
|
||||
hooks.HandlerCommand: &hookhandlers.CommandHandler{Edition: edition.Current()},
|
||||
hooks.HandlerHTTP: &hookhandlers.HTTPHandler{
|
||||
EncryptKey: encryptKey,
|
||||
Client: security.NewSafeClient(10 * time.Second),
|
||||
},
|
||||
hooks.HandlerPrompt: promptHandler,
|
||||
}
|
||||
}
|
||||
+7
-15
@@ -5,9 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
@@ -19,7 +17,6 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/eventbus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/hooks"
|
||||
hookhandlers "github.com/nextlevelbuilder/goclaw/internal/hooks/handlers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/orchestration"
|
||||
httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
|
||||
kg "github.com/nextlevelbuilder/goclaw/internal/knowledgegraph"
|
||||
@@ -28,7 +25,6 @@ import (
|
||||
memorypkg "github.com/nextlevelbuilder/goclaw/internal/memory"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/sandbox"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/security"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/skills"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
||||
@@ -152,21 +148,17 @@ func wireExtras(
|
||||
// Agent Hooks (Issue #875) — lifecycle dispatcher + handlers.
|
||||
var hookDispatcher hooks.Dispatcher = hooks.NewNoopDispatcher()
|
||||
if hs, ok := stores.Hooks.(hooks.HookStore); ok && hs != nil {
|
||||
encryptKey := os.Getenv("GOCLAW_ENCRYPTION_KEY")
|
||||
handlers := buildHookHandlers(stores, providerReg)
|
||||
stdOpts := hooks.StdDispatcherOpts{
|
||||
Store: hs,
|
||||
Audit: hooks.NewAuditWriter(hs, ""),
|
||||
Handlers: map[hooks.HandlerType]hooks.Handler{
|
||||
hooks.HandlerCommand: &hookhandlers.CommandHandler{Edition: edition.Current()},
|
||||
hooks.HandlerHTTP: &hookhandlers.HTTPHandler{
|
||||
EncryptKey: encryptKey,
|
||||
Client: security.NewSafeClient(10 * time.Second),
|
||||
},
|
||||
},
|
||||
Store: hs,
|
||||
Audit: hooks.NewAuditWriter(hs, ""),
|
||||
Handlers: handlers,
|
||||
}
|
||||
hookDispatcher = hooks.NewStdDispatcher(stdOpts)
|
||||
hooks.SubscribeDelegateEvents(domainBus, hookDispatcher)
|
||||
slog.Info("agent hooks dispatcher wired", "handlers", "command,http")
|
||||
// Stash handlers for later gateway.go wiring (test runner).
|
||||
sharedHookHandlers = handlers
|
||||
slog.Info("agent hooks dispatcher wired", "handlers", "command,http,prompt")
|
||||
}
|
||||
|
||||
resolver := agent.NewManagedResolver(agent.ResolverDeps{
|
||||
|
||||
Reference in new issue
Block a user