Files
viettranx ee328d4266 refactor: consolidate hooks table migration and drop deprecated agent_id column
- Consolidated PG migration 054: agent_hooks rename + junction table + deprecated column drop
- Updated SQLite schema v19 with final consolidated migration
- Removed obsolete schema rebuild files (v21, v23)
- Updated Go store layer (pg/hooks.go, sqlitestore/hooks.go)
- Updated integration tests to use new table names (hooks, hook_agents)
- Updated TypeScript protocol, UI components, and i18n strings
- Updated gateway methods to reflect schema changes

Tables: agent_hooks → hooks, agent_hook_agents → hook_agents
2026-04-16 14:17:48 +07:00

86 lines
3.5 KiB
Go

package hooks
import (
"context"
"errors"
"github.com/google/uuid"
)
// ErrBuiltinReadOnly is returned by Update/Delete when a caller attempts to
// modify a row with source='builtin' beyond toggling `enabled`. Builtin rows
// are canonical — content comes from embedded JS at every startup (Phase 04
// seed). UI should render them read-only except for the enabled toggle.
// Message is surfaced via i18n key hook.builtin_readonly.
var ErrBuiltinReadOnly = errors.New("hook: builtin row is read-only except enabled toggle")
// seedBypassKey is the private context key used by Phase 04 builtin seeder to
// bypass the Update/Delete builtin-readonly guard. Process-private: only the
// seeder sets it. Ensures user-facing writes still fail ErrBuiltinReadOnly.
type seedBypassKey struct{}
// WithSeedBypass marks ctx as originating from the builtin seeder. Store
// implementations MUST consult IsSeedBypass(ctx) before rejecting writes on
// builtin rows. Any other caller is denied.
func WithSeedBypass(ctx context.Context) context.Context {
return context.WithValue(ctx, seedBypassKey{}, true)
}
// IsSeedBypass reports whether ctx carries the builtin-seeder bypass marker.
func IsSeedBypass(ctx context.Context) bool {
v, _ := ctx.Value(seedBypassKey{}).(bool)
return v
}
// ListFilter narrows results from HookStore.List.
type ListFilter struct {
TenantID *uuid.UUID
AgentID *uuid.UUID
Event *HookEvent
Scope *Scope
Enabled *bool
}
// HookStore is the data-access contract for agent hooks.
// PG impl lives in internal/store/pg/hooks.go;
// SQLite impl lives in internal/store/sqlitestore/hooks.go.
//
// All write operations must enforce tenant isolation: reads outside master scope
// must include WHERE tenant_id = $N. See store.IsMasterScope(ctx).
type HookStore interface {
// Create inserts a new hook config and returns the generated UUID.
Create(ctx context.Context, cfg HookConfig) (uuid.UUID, error)
// GetByID returns a single hook config by primary key.
// Returns nil, nil when not found (no sentinel error).
GetByID(ctx context.Context, id uuid.UUID) (*HookConfig, error)
// List returns hooks matching the filter. Caller applies ordering/limit.
List(ctx context.Context, filter ListFilter) ([]HookConfig, error)
// Update applies the map of column→value patches and bumps the version field.
// Callers must not include "version" in updates — the store increments it
// atomically to bust the TTL cache entry (H1 mitigation).
Update(ctx context.Context, id uuid.UUID, updates map[string]any) error
// Delete removes a hook config. hook_executions rows are preserved via
// ON DELETE SET NULL on the hook_id FK column.
Delete(ctx context.Context, id uuid.UUID) error
// ResolveForEvent returns the ordered list of enabled hooks that match
// (tenant_id, agent_id, event). Implementations should cache this result
// with a short TTL (5s) keyed by (tenant_id, agent_id, event, maxVersion)
// and short-circuit via COUNT(*) = 0 cache for the zero-hooks hot path.
ResolveForEvent(ctx context.Context, event Event) ([]HookConfig, error)
// WriteExecution appends an immutable execution audit row.
// Caller must pre-truncate Error to 256 chars and encrypt ErrorDetail.
WriteExecution(ctx context.Context, exec HookExecution) error
// SetHookAgents replaces all junction rows for hookID with the given agentIDs.
SetHookAgents(ctx context.Context, hookID uuid.UUID, agentIDs []uuid.UUID) error
// GetHookAgents returns the agent UUIDs linked to hookID via the junction table.
GetHookAgents(ctx context.Context, hookID uuid.UUID) ([]uuid.UUID, error)
}