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:
viettranx committed 2026-04-16 14:17:47 +07:00
1 parent 553340b3a9
commit aa1b2f026a
13 files changed
+774 -23

No files matched your search

+15 -2
View File
@@ -103,12 +103,25 @@ func (s *SqliteHookStore) Create(ctx context.Context, cfg hooks.HookConfig) (uui
// ─── GetByID ─────────────────────────────────────────────────────────────────
func (s *SqliteHookStore) GetByID(ctx context.Context, id uuid.UUID) (*hooks.HookConfig, error) {
row := s.db.QueryRowContext(ctx, `
q := `
SELECT id, tenant_id, agent_id, scope, event, handler_type,
config, matcher, if_expr, timeout_ms, on_timeout,
priority, enabled, version, source, metadata, created_by,
created_at, updated_at
FROM agent_hooks WHERE id = ?`, id.String())
FROM agent_hooks WHERE id = ?`
args := []any{id.String()}
// Tenant-scope guard: non-master callers only see own + global rows.
if !store.IsMasterScope(ctx) {
tid := store.TenantIDFromContext(ctx)
if tid == uuid.Nil {
return nil, fmt.Errorf("tenant_id required for non-master scope")
}
q += " AND (tenant_id = ? OR tenant_id = ?)"
args = append(args, tid.String(), store.MasterTenantID.String())
}
row := s.db.QueryRowContext(ctx, q, args...)
cfg, err := scanHookSQLiteRow(row)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil