mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 08:18:56 +00:00
Phase 07 runtime migration. Post-Wave-1 Standard deployments that pre-dated the edition gate may still carry enabled command-type hook rows in the DB; those keep firing via the dispatcher after the UI stops letting users create new ones. This helper flips them off at startup so live traffic never fires a disabled-by-policy handler again. - internal/hooks/migration_command_autodisable.go: DisableLegacyCommandHooks(ctx, hookStore, edition). Standard only; Lite returns early. Master-scope context + List(Enabled=true) → skip non-command + source='builtin' rows → Update enabled=false. Per-row WARN log with hook_id + tenant_id for audit; INFO summary when n>0. Idempotent: second boot finds nothing. - cmd/gateway_managed.go: invoke after builtin.Seed, BEFORE handler registration so no HTTP/WS traffic races the migration. - Tests (fake store, 5 cases): * Standard with mixed rows → only command+source=ui gets disabled * http + script/builtin + command/builtin rows untouched * Idempotent second run → n=0 * Lite edition → n=0, nothing touched * nil store → n=0, no error * List error → propagated up Note: Phase 04/05 never seed command-typed builtins, but the carve-out is kept defensively so a future accidental builtin command row can't be disabled by this migration.
73 lines
2.3 KiB
Go
73 lines
2.3 KiB
Go
package hooks
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// DisableLegacyCommandHooks turns off any enabled command-typed hook rows
|
|
// when running on Standard edition. No-op on Lite. Idempotent: second call
|
|
// finds nothing to disable.
|
|
//
|
|
// Context: Phase 01 removed the UI affordance for creating `command` hooks on
|
|
// Standard edition; Phase 03's edition gate rejects new writes at the WS RPC
|
|
// layer. But Standard deployments that existed BEFORE Wave 1 already have
|
|
// command-hook rows in the DB, and those rows keep firing via the dispatcher.
|
|
// This helper is the one-time-per-boot runtime migration that flips them off.
|
|
//
|
|
// Why not a SQL migration?
|
|
// - Lite runs on SQLite and must not be affected even when sharing code
|
|
// - Edition is a runtime flag, not a schema property
|
|
// - Fresh Standard DBs that restore a Lite backup may reintroduce command
|
|
// rows; re-running on each boot handles that safely
|
|
//
|
|
// Called once at startup from cmd/gateway_managed.go after migrations and the
|
|
// builtin seed, BEFORE HTTP/WS listeners accept traffic.
|
|
func DisableLegacyCommandHooks(ctx context.Context, hs HookStore, ed edition.Edition) (int, error) {
|
|
if ed.Name != edition.Standard.Name {
|
|
return 0, nil
|
|
}
|
|
if hs == nil {
|
|
return 0, nil
|
|
}
|
|
|
|
// Reach all tenants' rows. store.WithOwnerRole does NOT exist — see
|
|
// phase-04 red-team notes. Use WithRole explicitly.
|
|
ctx = store.WithRole(ctx, store.RoleOwner)
|
|
|
|
enabled := true
|
|
rows, err := hs.List(ctx, ListFilter{Enabled: &enabled})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
n := 0
|
|
for _, r := range rows {
|
|
if r.HandlerType != HandlerCommand {
|
|
continue
|
|
}
|
|
if r.Source == SourceBuiltin {
|
|
// Defensive: Phase 04/05 only seed HandlerScript, but guard anyway
|
|
// so a future accidental builtin command row wouldn't get disabled.
|
|
continue
|
|
}
|
|
if err := hs.Update(ctx, r.ID, map[string]any{"enabled": false}); err != nil {
|
|
slog.Warn("hooks.command_hook_auto_disable_failed",
|
|
"hook_id", r.ID, "tenant_id", r.TenantID, "err", err)
|
|
continue
|
|
}
|
|
slog.Warn("hooks.command_hook_auto_disabled",
|
|
"hook_id", r.ID,
|
|
"tenant_id", r.TenantID,
|
|
"reason", "command_handler_disabled_on_standard_edition")
|
|
n++
|
|
}
|
|
if n > 0 {
|
|
slog.Info("hooks.command_migration_complete", "disabled_count", n)
|
|
}
|
|
return n, nil
|
|
}
|