fix(gemini): implement native transactional hooks

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 08:39:37 -04:00
1 parent 15d044cf63
commit 2dc633a506
17 files changed
+3131 -169

No files matched your search

+208
View File
@@ -0,0 +1,208 @@
package plugins
import (
"fmt"
"math"
)
var geminiKnownEvents = map[string]bool{
"BeforeTool": true, "AfterTool": true, "BeforeAgent": true,
"Notification": true, "AfterAgent": true, "SessionStart": true,
"SessionEnd": true, "PreCompress": true, "BeforeModel": true,
"AfterModel": true, "BeforeToolSelection": true,
}
type geminiHooks map[string][]any
type geminiHookControls struct {
enabled bool
disabled []string
}
func optionalGeminiString(value map[string]any, field, label string) error {
if item, exists := value[field]; exists {
if _, ok := item.(string); !ok {
return fmt.Errorf(`%s field %q must be a string`, label, field)
}
}
return nil
}
func validateGeminiHandler(
value any,
event string,
groupIndex int,
handlerIndex int,
) error {
label := fmt.Sprintf(
"Gemini CLI settings handler hooks.%s[%d].hooks[%d]",
event,
groupIndex,
handlerIndex,
)
handler, ok := value.(map[string]any)
if !ok || handler == nil {
return fmt.Errorf("%s must be an object", label)
}
if handler["type"] != "command" {
return fmt.Errorf(
`%s has unsupported type %q`,
label,
fmt.Sprint(handler["type"]),
)
}
for _, field := range []string{"name", "description", "source"} {
if err := optionalGeminiString(handler, field, label); err != nil {
return err
}
}
if timeout, exists := handler["timeout"]; exists {
number, ok := timeout.(float64)
if !ok || math.IsNaN(number) || math.IsInf(number, 0) || number < 0 {
return fmt.Errorf(
"%s timeout must be a non-negative finite number",
label,
)
}
}
if environment, exists := handler["env"]; exists {
entries, ok := environment.(map[string]any)
if !ok {
return fmt.Errorf("%s env must map names to strings", label)
}
for _, entry := range entries {
if _, ok := entry.(string); !ok {
return fmt.Errorf("%s env must map names to strings", label)
}
}
}
command, ok := handler["command"].(string)
if !ok || command == "" {
return fmt.Errorf("%s requires a non-empty command", label)
}
return nil
}
func validateGeminiGroup(value any, event string, groupIndex int) error {
label := fmt.Sprintf(
"Gemini CLI settings group hooks.%s[%d]",
event,
groupIndex,
)
group, ok := value.(map[string]any)
if !ok || group == nil {
return fmt.Errorf("%s must be an object", label)
}
if matcher, exists := group["matcher"]; exists {
if _, ok := matcher.(string); !ok {
return fmt.Errorf("%s matcher must be a string", label)
}
}
if sequential, exists := group["sequential"]; exists {
if _, ok := sequential.(bool); !ok {
return fmt.Errorf("%s sequential must be a boolean", label)
}
}
handlers, ok := group["hooks"].([]any)
if !ok {
return fmt.Errorf("%s must contain a hooks array", label)
}
for handlerIndex, handler := range handlers {
if err := validateGeminiHandler(
handler,
event,
groupIndex,
handlerIndex,
); err != nil {
return err
}
}
return nil
}
func readGeminiHooks(root map[string]any) (geminiHooks, error) {
value, exists := root["hooks"]
if !exists {
return geminiHooks{}, nil
}
object, ok := value.(map[string]any)
if !ok || object == nil {
return nil, fmt.Errorf(`Gemini CLI settings field "hooks" must be an object`)
}
hooks := make(geminiHooks, len(object))
for event, groupsValue := range object {
groups, ok := groupsValue.([]any)
if !ok {
return nil, fmt.Errorf(
`Gemini CLI settings field "hooks.%s" must be an array`,
event,
)
}
if geminiKnownEvents[event] {
for groupIndex, group := range groups {
if err := validateGeminiGroup(group, event, groupIndex); err != nil {
return nil, err
}
}
}
hooks[event] = append([]any(nil), groups...)
}
return hooks, nil
}
func readGeminiHookControls(root map[string]any) (geminiHookControls, error) {
value, exists := root["hooksConfig"]
if !exists {
return geminiHookControls{enabled: true}, nil
}
object, ok := value.(map[string]any)
if !ok || object == nil {
return geminiHookControls{}, fmt.Errorf(
`Gemini CLI settings field "hooksConfig" must be an object`,
)
}
for field := range object {
if field != "enabled" && field != "disabled" && field != "notifications" {
return geminiHookControls{}, fmt.Errorf(
`Gemini CLI settings field "hooksConfig" contains unsupported field %q`,
field,
)
}
}
enabled := true
if value, exists := object["enabled"]; exists {
flag, ok := value.(bool)
if !ok {
return geminiHookControls{}, fmt.Errorf(
`Gemini CLI settings field "hooksConfig.enabled" must be a boolean`,
)
}
enabled = flag
}
if value, exists := object["notifications"]; exists {
if _, ok := value.(bool); !ok {
return geminiHookControls{}, fmt.Errorf(
`Gemini CLI settings field "hooksConfig.notifications" must be a boolean`,
)
}
}
disabled := []string{}
if value, exists := object["disabled"]; exists {
items, ok := value.([]any)
if !ok {
return geminiHookControls{}, fmt.Errorf(
`Gemini CLI settings field "hooksConfig.disabled" must be an array of strings`,
)
}
for _, item := range items {
entry, ok := item.(string)
if !ok {
return geminiHookControls{}, fmt.Errorf(
`Gemini CLI settings field "hooksConfig.disabled" must be an array of strings`,
)
}
disabled = append(disabled, entry)
}
}
return geminiHookControls{enabled: enabled, disabled: disabled}, nil
}