fix(droid): align hooks with Factory 0.175

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 14:34:54 -04:00
1 parent 827374e7f8
commit 4521cbe32f
15 files changed
+2749 -881

No files matched your search

+254 -138
View File
@@ -3,8 +3,8 @@ package plugins
import (
"bytes"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
)
@@ -13,21 +13,22 @@ type droidDocument struct {
filePath string
exists bool
raw []byte
snapshot *managedFileSnapshot
root map[string]any
hooks droidHookSettings
basePath []any
hasHooksContainer bool
hooksDisabled *bool
showHookOutput *bool
ownedFile bool
}
type droidSources struct {
rootPath string
primary *droidDocument
settings *droidDocument
}
type droidInstallationTargets struct {
targets map[string]*droidDocument
createdRoot *droidDocument
root *droidDocument
legacy *droidDocument
settings *droidDocument
localSettings *droidDocument
policy *droidPolicyState
}
type droidRenderedDocument struct {
@@ -37,72 +38,39 @@ type droidRenderedDocument struct {
remove bool
}
func droidFactoryPaths() (string, string, string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", "", "", fmt.Errorf("resolve home directory: %w", err)
}
directory := filepath.Join(home, ".factory")
return filepath.Join(directory, "hooks.json"),
filepath.Join(directory, "hooks", "hooks.json"),
filepath.Join(directory, "settings.json"), nil
type droidHookBlock struct {
field string
filePath string
label string
}
func readDroidSources() (*droidSources, error) {
rootPath, legacyPath, settingsPath, err := droidFactoryPaths()
if err != nil {
return nil, err
}
rootRaw, rootExists, err := readOptionalFile(rootPath, "Factory Droid hooks")
if err != nil {
return nil, err
}
var primary *droidDocument
if rootExists {
primary, err = parseDroidDocument(true, rootPath, "hooks", rootRaw)
if err != nil {
return nil, err
}
} else {
legacyRaw, legacyExists, readErr := readOptionalFile(legacyPath, "Factory Droid legacy hooks")
if readErr != nil {
return nil, readErr
}
if legacyExists {
primary, err = parseDroidDocument(true, legacyPath, "legacy", legacyRaw)
if err != nil {
return nil, err
}
}
}
settingsRaw, settingsExists, err := readOptionalFile(settingsPath, "Factory Droid settings")
if err != nil {
return nil, err
}
var settings *droidDocument
if settingsExists {
settings, err = parseDroidDocument(true, settingsPath, "settings", settingsRaw)
} else {
settings, err = parseDroidDocument(false, settingsPath, "settings", []byte("{}\n"))
}
if err != nil {
return nil, err
}
return &droidSources{rootPath: rootPath, primary: primary, settings: settings}, nil
}
func parseDroidDocument(exists bool, filePath, kind string, raw []byte) (*droidDocument, error) {
func parseDroidDocument(
exists bool,
filePath, kind string,
raw []byte,
snapshot *managedFileSnapshot,
) (*droidDocument, error) {
label := droidDocumentLabel(kind, filePath)
root, err := decodeJSONCObject(raw, label, true)
if err != nil {
return nil, err
}
document := &droidDocument{
kind: kind, filePath: filePath, exists: exists, raw: append([]byte(nil), raw...),
kind: kind, filePath: filePath, exists: exists,
raw: append([]byte(nil), raw...), snapshot: snapshot, root: root,
ownedFile: bytes.HasPrefix(raw, []byte(droidOwnedFileMarker)),
}
if kind == "settings" {
hooksValue, hasHooks := root["hooks"]
if kind == "settings" || kind == "local-settings" {
document.basePath = []any{"hooks"}
document.hooksDisabled, err = droidOptionalBoolean(root, "hooksDisabled", label)
if err != nil {
return nil, err
}
document.showHookOutput, err = droidOptionalBoolean(root, "showHookOutput", label)
if err != nil {
return nil, err
}
hooksValue, hasHooks := root["hooks"]
document.hasHooksContainer = hasHooks
if !hasHooks {
document.hooks = droidHookSettings{}
@@ -111,16 +79,51 @@ func parseDroidDocument(exists bool, filePath, kind string, raw []byte) (*droidD
document.hooks, err = readDroidHookSettings(hooksValue, label+` field "hooks"`)
return document, err
}
document.hooks, err = readDroidHookSettings(root, label)
document.hasHooksContainer = true
document.ownedFile = bytes.HasPrefix(raw, []byte(droidOwnedFileMarker))
if hooksValue, hasHooks := root["hooks"]; hasHooks {
document.basePath = []any{"hooks"}
document.hasHooksContainer = true
document.hooks, err = readDroidHookSettings(hooksValue, label+` field "hooks"`)
return document, err
}
document.hooksDisabled, err = droidOptionalBoolean(root, "hooksDisabled", label)
if err != nil {
return nil, err
}
document.showHookOutput, err = droidOptionalBoolean(root, "showHookOutput", label)
if err != nil {
return nil, err
}
direct := make(map[string]any, len(root))
for key, value := range root {
if key != "hooksDisabled" && key != "showHookOutput" {
direct[key] = value
}
}
document.hooks, err = readDroidHookSettings(direct, label)
return document, err
}
func droidOptionalBoolean(
root map[string]any,
field, label string,
) (*bool, error) {
value, exists := root[field]
if !exists {
return nil, nil
}
boolean, ok := value.(bool)
if !ok {
return nil, fmt.Errorf(`%s field %q must be a boolean`, label, field)
}
return &boolean, nil
}
func droidDocumentLabel(kind, filePath string) string {
switch kind {
case "settings":
return fmt.Sprintf("Factory Droid settings at %s", filePath)
case "local-settings":
return fmt.Sprintf("Factory Droid local settings at %s", filePath)
case "legacy":
return fmt.Sprintf("Factory Droid legacy hooks at %s", filePath)
default:
@@ -128,41 +131,91 @@ func droidDocumentLabel(kind, filePath string) string {
}
}
func createDroidDocument(filePath, kind string, raw []byte) (*droidDocument, error) {
return parseDroidDocument(false, filePath, kind, raw, nil)
}
func createOwnedDroidDocument(filePath string) (*droidDocument, error) {
return parseDroidDocument(
false, filePath, "hooks", []byte(droidOwnedFileMarker+"\n{}\n"),
return createDroidDocument(
filePath,
"hooks",
[]byte(droidOwnedFileMarker+"\n{\n \"hooks\": {}\n}\n"),
)
}
func selectDroidInstallationTargets(sources *droidSources) (*droidInstallationTargets, error) {
selection := &droidInstallationTargets{targets: make(map[string]*droidDocument, len(droidToolEvents))}
for _, event := range droidToolEvents {
switch {
case sources.primary != nil && hasDroidHookField(sources.primary.hooks, event):
selection.targets[event] = sources.primary
case sources.settings.hasHooksContainer && hasDroidHookField(sources.settings.hooks, event):
selection.targets[event] = sources.settings
case sources.primary != nil:
selection.targets[event] = sources.primary
case sources.settings.hasHooksContainer:
selection.targets[event] = sources.settings
default:
if selection.createdRoot == nil {
created, err := createOwnedDroidDocument(sources.rootPath)
if err != nil {
return nil, err
}
selection.createdRoot = created
}
selection.targets[event] = selection.createdRoot
}
func activeDroidDocument(sources *droidSources) *droidDocument {
switch {
case sources.root.exists:
return sources.root
case sources.legacy.exists:
return sources.legacy
case sources.localSettings.hasHooksContainer:
return sources.localSettings
case sources.settings.hasHooksContainer:
return sources.settings
default:
return sources.root
}
return selection, nil
}
func hasDroidHookField(settings droidHookSettings, key string) bool {
_, exists := settings[key]
return exists
func effectiveDroidHooks(sources *droidSources) droidHookSettings {
return activeDroidDocument(sources).hooks
}
func droidHookBlocked(sources *droidSources) *droidHookBlock {
if sources.policy != nil && sources.policy.allowManagedHooksOnlyBy != nil {
origin := sources.policy.allowManagedHooksOnlyBy
return &droidHookBlock{"allowManagedHooksOnly", origin.filePath, origin.label}
}
if sources.policy != nil && sources.policy.hooksDisabled != nil {
if *sources.policy.hooksDisabled {
origin := sources.policy.hooksDisabledBy
return &droidHookBlock{"hooksDisabled", origin.filePath, origin.label}
}
return nil
}
selected := sources.settings
if sources.localSettings.hooksDisabled != nil {
selected = sources.localSettings
}
if selected.hooksDisabled != nil && *selected.hooksDisabled {
return &droidHookBlock{
"hooksDisabled", selected.filePath,
droidDocumentLabel(selected.kind, selected.filePath),
}
}
active := activeDroidDocument(sources)
if active.hooksDisabled != nil && *active.hooksDisabled {
return &droidHookBlock{
"hooksDisabled", active.filePath,
droidDocumentLabel(active.kind, active.filePath),
}
}
return nil
}
func droidSourceDocuments(sources *droidSources) []*droidDocument {
return []*droidDocument{
sources.root, sources.legacy, sources.settings, sources.localSettings,
}
}
func droidInstallationDocuments(sources *droidSources) []*droidDocument {
target := activeDroidDocument(sources)
candidates := []*droidDocument{target}
if sources.root.exists {
candidates = append(candidates, sources.root)
}
if sources.legacy.exists {
candidates = append(candidates, sources.legacy)
}
if sources.settings.hasHooksContainer {
candidates = append(candidates, sources.settings)
}
if sources.localSettings.hasHooksContainer {
candidates = append(candidates, sources.localSettings)
}
return uniqueDroidDocuments(candidates...)
}
func uniqueDroidDocuments(documents ...*droidDocument) []*droidDocument {
@@ -183,17 +236,13 @@ func uniqueDroidDocuments(documents ...*droidDocument) []*droidDocument {
}
func droidAdditionsFor(
document *droidDocument,
targets map[string]*droidDocument,
document, target *droidDocument,
groups map[string]map[string]any,
) map[string]map[string]any {
additions := map[string]map[string]any{}
for _, event := range droidToolEvents {
if sameDroidPath(targets[event].filePath, document.filePath) {
additions[event] = groups[event]
}
if sameDroidPath(document.filePath, target.filePath) {
return groups
}
return additions
return map[string]map[string]any{}
}
func renderDroidDocument(
@@ -201,7 +250,14 @@ func renderDroidDocument(
agentID, runtimeRoot string,
additions map[string]map[string]any,
) (*droidRenderedDocument, error) {
editor, err := newJSONCEditor(document.raw, droidDocumentLabel(document.kind, document.filePath), true)
if len(additions) > 0 && droidHasExactInstallation(document, additions, runtimeRoot) {
return &droidRenderedDocument{document: document, next: document.raw}, nil
}
editor, err := newJSONCEditor(
document.raw,
droidDocumentLabel(document.kind, document.filePath),
true,
)
if err != nil {
return nil, err
}
@@ -212,28 +268,33 @@ func renderDroidDocument(
}
return removals[left].event < removals[right].event
})
for _, removal := range removals {
groupPath := appendDroidPath(document.basePath, removal.event, removal.groupIndex)
if removal.removeGroup {
if err := editor.remove(groupPath); err != nil {
return nil, err
for _, event := range droidToolEvents {
removedEvent := false
for _, removal := range removals {
if removal.event != event {
continue
}
continue
}
sort.Sort(sort.Reverse(sort.IntSlice(removal.handlerIndexes)))
for _, handlerIndex := range removal.handlerIndexes {
if err := editor.remove(appendDroidPath(groupPath, "hooks", handlerIndex)); err != nil {
return nil, err
removedEvent = true
groupPath := appendDroidPath(document.basePath, removal.event, removal.groupIndex)
if removal.removeGroup {
if err := editor.remove(groupPath); err != nil {
return nil, err
}
continue
}
sort.Sort(sort.Reverse(sort.IntSlice(removal.handlerIndexes)))
for _, handlerIndex := range removal.handlerIndexes {
if err := editor.remove(appendDroidPath(groupPath, "hooks", handlerIndex)); err != nil {
return nil, err
}
}
}
}
current, err := parseDroidDocument(document.exists, document.filePath, document.kind, editor.pack())
if err != nil {
return nil, err
}
if document.ownedFile {
for _, event := range droidToolEvents {
groups, exists := current.hooks[event].([]any)
if removedEvent {
current, parseErr := droidHooksFromRaw(document, editor.pack())
if parseErr != nil {
return nil, parseErr
}
groups, exists := current[event].([]any)
if exists && len(groups) == 0 {
if err := editor.remove(appendDroidPath(document.basePath, event)); err != nil {
return nil, err
@@ -246,12 +307,12 @@ func renderDroidDocument(
if !exists {
continue
}
current, err = parseDroidDocument(document.exists, document.filePath, document.kind, editor.pack())
if err != nil {
return nil, err
current, parseErr := droidHooksFromRaw(document, editor.pack())
if parseErr != nil {
return nil, parseErr
}
eventPath := appendDroidPath(document.basePath, event)
if hasDroidHookField(current.hooks, event) {
if _, exists := current[event]; exists {
err = editor.appendArray(eventPath, group)
} else {
err = editor.addProperty(document.basePath, event, []any{group})
@@ -261,16 +322,77 @@ func renderDroidDocument(
}
}
next := editor.pack()
nextDocument, err := parseDroidDocument(document.exists, document.filePath, document.kind, next)
nextDocument, err := parseDroidDocument(
document.exists, document.filePath, document.kind, next, document.snapshot,
)
if err != nil {
return nil, err
}
remove := len(additions) == 0 && document.ownedFile && document.kind != "settings" && len(nextDocument.hooks) == 0
remove := len(additions) == 0 && document.exists && document.ownedFile &&
droidHookDocumentEmpty(nextDocument)
return &droidRenderedDocument{
document: document, changed: remove || !bytes.Equal(next, document.raw), next: next, remove: remove,
document: document,
changed: remove || !bytes.Equal(next, document.raw),
next: next,
remove: remove,
}, nil
}
func droidHasExactInstallation(
document *droidDocument,
additions map[string]map[string]any,
runtimeRoot string,
) bool {
if len(additions) != len(droidToolEvents) {
return false
}
removals := managedDroidRemovals(document.hooks, "", runtimeRoot)
if len(removals) != len(droidToolEvents) {
return false
}
for _, event := range droidToolEvents {
matches := make([]droidManagedRemoval, 0, 1)
for _, removal := range removals {
if removal.event == event {
matches = append(matches, removal)
}
}
if len(matches) != 1 || !matches[0].removeGroup {
return false
}
groups := document.hooks[event].([]any)
if !reflect.DeepEqual(groups[matches[0].groupIndex], additions[event]) {
return false
}
}
return true
}
func droidHooksFromRaw(
document *droidDocument,
raw []byte,
) (droidHookSettings, error) {
current, err := parseDroidDocument(
document.exists, document.filePath, document.kind, raw, document.snapshot,
)
if err != nil {
return nil, err
}
return current.hooks, nil
}
func droidHookDocumentEmpty(document *droidDocument) bool {
if document.kind == "settings" || document.kind == "local-settings" || len(document.hooks) > 0 {
return false
}
for field := range document.root {
if field != "hooks" {
return false
}
}
return true
}
func appendDroidPath(base []any, parts ...any) []any {
path := make([]any, 0, len(base)+len(parts))
path = append(path, base...)
@@ -278,11 +400,5 @@ func appendDroidPath(base []any, parts ...any) []any {
}
func displayDroidConfigPath(sources *droidSources) string {
if sources.primary != nil {
return sources.primary.filePath
}
if sources.settings.exists && sources.settings.hasHooksContainer {
return sources.settings.filePath
}
return sources.rootPath
return activeDroidDocument(sources).filePath
}