fix: make Codex hooks transactional

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 03:22:58 -04:00
1 parent 415a884efe
commit f0e4694da2
17 files changed
+2550 -506

No files matched your search

+265 -184
View File
@@ -1,20 +1,51 @@
package plugins
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
)
const (
codexAgentKey = "codex"
codexConfigFile = "hooks.json"
codexGuardScript = "guard.js"
codexAuditScript = "hook.js"
codexHookTimeout = float64(10)
codexOwnedDescription = "Elydora audit and freeze enforcement"
codexGuardStatusMessage = "Checking Elydora agent state"
codexAuditStatusMessage = "Recording Elydora tool use"
codexPOSIXApostrophe = `'"'"'`
)
type codexHooks map[string][]map[string]any
type codexDocument struct {
exists bool
filePath string
root map[string]any
hooks codexHooks
raw []byte
}
type codexRenderedDocument struct {
document *codexDocument
changed bool
next []byte
remove bool
}
type codexRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
func cloneCodexObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
@@ -23,230 +54,280 @@ func cloneCodexObject(value map[string]any) map[string]any {
return clone
}
func codexHooksObject(settings map[string]any) (map[string]any, error) {
value, exists := settings["hooks"]
if !exists {
return map[string]any{}, nil
func cloneCodexHooks(source codexHooks) codexHooks {
clone := make(codexHooks, len(source))
for event, groups := range source {
clone[event] = append([]map[string]any(nil), groups...)
}
hooks, ok := value.(map[string]any)
if !ok || hooks == nil {
return nil, fmt.Errorf(`Codex hooks config field "hooks" must be an object`)
}
return cloneCodexObject(hooks), nil
return clone
}
func codexEventGroups(hooks map[string]any, event string) ([]map[string]any, error) {
value, exists := hooks[event]
if !exists {
return []map[string]any{}, nil
func sameCodexPath(left, right string) bool {
left = filepath.Clean(left)
right = filepath.Clean(right)
if absolute, err := filepath.Abs(left); err == nil {
left = absolute
}
groups, ok := value.([]any)
if !ok {
return nil, fmt.Errorf(`Codex hooks config field "hooks.%s" must be an array of objects`, event)
if absolute, err := filepath.Abs(right); err == nil {
right = absolute
}
objects := make([]map[string]any, 0, len(groups))
for _, group := range groups {
object, ok := group.(map[string]any)
if !ok || object == nil {
return nil, fmt.Errorf(`Codex hooks config field "hooks.%s" must be an array of objects`, event)
}
objects = append(objects, object)
}
return objects, nil
}
func codexGroupHandlers(group map[string]any) ([]map[string]any, error) {
value, exists := group["hooks"]
if !exists {
return nil, fmt.Errorf("Codex hook matcher group must contain a hooks array")
}
handlers, ok := value.([]any)
if !ok {
return nil, fmt.Errorf("Codex hook matcher group must contain a hooks array")
}
objects := make([]map[string]any, 0, len(handlers))
for _, handler := range handlers {
object, ok := handler.(map[string]any)
if !ok || object == nil {
return nil, fmt.Errorf("Codex hook matcher group must contain a hooks array")
}
objects = append(objects, object)
}
return objects, nil
}
func normalizeCodexPath(value string) string {
normalized := filepath.ToSlash(value)
if runtime.GOOS == "windows" {
return strings.ToLower(normalized)
return strings.EqualFold(left, right)
}
return normalized
return left == right
}
func isManagedCodexCommand(command any, scriptName, agentID string) bool {
text, ok := command.(string)
func sameCodexAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func readCodexHooks(value any, label string) (codexHooks, error) {
object, ok := value.(map[string]any)
if !ok {
return false
return nil, fmt.Errorf(`%s field "hooks" must be an object`, label)
}
normalized := normalizeCodexPath(text)
if !strings.Contains(normalized, "/.elydora/") {
return false
}
suffix := "/" + normalizeCodexPath(scriptName)
if agentID == "" {
return codexCommandEndsWith(normalized, suffix)
}
expected := normalizeCodexPath(filepath.Join(".elydora", agentID, scriptName))
return codexCommandEndsWith(normalized, "/"+expected)
}
func codexCommandEndsWith(command, scriptPath string) bool {
trimmed := strings.TrimSpace(command)
return strings.HasSuffix(trimmed, scriptPath) ||
strings.HasSuffix(trimmed, scriptPath+`'`) ||
strings.HasSuffix(trimmed, scriptPath+`"`)
}
func isElydoraCodexHandler(handler map[string]any, agentID string) bool {
var scriptName string
switch handler["statusMessage"] {
case codexGuardStatusMessage:
scriptName = "guard.js"
case codexAuditStatusMessage:
scriptName = "hook.js"
default:
return false
}
return isManagedCodexCommand(handler["command"], scriptName, agentID) ||
isManagedCodexCommand(handler["commandWindows"], scriptName, agentID)
}
func withoutCodexHandlers(groups []map[string]any, agentID string) ([]map[string]any, bool, error) {
filteredGroups := make([]map[string]any, 0, len(groups))
changed := false
for _, group := range groups {
handlers, err := codexGroupHandlers(group)
if err != nil {
return nil, false, err
hooks := make(codexHooks, len(object))
for event, candidate := range object {
values, ok := candidate.([]any)
if !ok {
return nil, fmt.Errorf(`%s field "hooks.%s" must be an array`, label, event)
}
filteredHandlers := make([]any, 0, len(handlers))
removed := false
for _, handler := range handlers {
if isElydoraCodexHandler(handler, agentID) {
changed = true
removed = true
continue
groups := make([]map[string]any, 0, len(values))
for groupIndex, value := range values {
group, ok := value.(map[string]any)
if !ok || group == nil {
return nil, fmt.Errorf(
`%s matcher group hooks.%s[%d] must be an object`,
label, event, groupIndex,
)
}
filteredHandlers = append(filteredHandlers, handler)
handlerValues, ok := group["hooks"].([]any)
if !ok {
return nil, fmt.Errorf(
`%s matcher group hooks.%s[%d] must contain a hooks array`,
label, event, groupIndex,
)
}
for handlerIndex, handler := range handlerValues {
if object, ok := handler.(map[string]any); !ok || object == nil {
return nil, fmt.Errorf(
`%s handler hooks.%s[%d].hooks[%d] must be an object`,
label, event, groupIndex, handlerIndex,
)
}
}
groups = append(groups, group)
}
if len(filteredHandlers) == 0 && removed {
continue
}
nextGroup := cloneCodexObject(group)
nextGroup["hooks"] = filteredHandlers
filteredGroups = append(filteredGroups, nextGroup)
hooks[event] = groups
}
return filteredGroups, changed, nil
return hooks, nil
}
func findCodexHandler(hooks map[string]any, event, status string) (map[string]any, error) {
groups, err := codexEventGroups(hooks, event)
func decodeStrictJSONObject(source []byte, label string) (map[string]any, error) {
if !json.Valid(source) {
var value any
if err := json.Unmarshal(source, &value); err != nil {
return nil, fmt.Errorf("parse %s: %w", label, err)
}
}
return decodeJSONCObject(source, label, false)
}
func parseCodexDocument(filePath string, raw []byte) (*codexDocument, error) {
label := fmt.Sprintf("Codex user hooks at %s", filePath)
root, err := decodeStrictJSONObject(raw, label)
if err != nil {
return nil, err
}
for _, group := range groups {
handlers, err := codexGroupHandlers(group)
hooks := codexHooks{}
if value, exists := root["hooks"]; exists {
hooks, err = readCodexHooks(value, label)
if err != nil {
return nil, err
}
for _, handler := range handlers {
if handler["statusMessage"] == status && isElydoraCodexHandler(handler, "") {
return handler, nil
}
}
}
return nil, nil
return &codexDocument{
exists: true, filePath: filePath, root: root,
hooks: hooks, raw: append([]byte(nil), raw...),
}, nil
}
func codexSettingsOwned(settings, hooks map[string]any) bool {
for key := range settings {
func createCodexDocument(filePath string) *codexDocument {
return &codexDocument{
filePath: filePath,
root: map[string]any{"description": codexOwnedDescription},
hooks: codexHooks{},
}
}
func exactCodexMatcherGroup(group map[string]any) bool {
return len(group) == 2 && group["matcher"] == "*"
}
func removeCodexGroups(
groups []map[string]any,
scriptName string,
status string,
agentID string,
) []map[string]any {
result := make([]map[string]any, 0, len(groups))
for _, group := range groups {
handlers := group["hooks"].([]any)
kept := make([]any, 0, len(handlers))
for _, value := range handlers {
handler := value.(map[string]any)
managedID, managed := codexManagedAgentID(handler, scriptName, status)
if managed && (agentID == "" || sameCodexAgentID(managedID, agentID)) {
continue
}
kept = append(kept, handler)
}
if len(kept) > 0 || !exactCodexMatcherGroup(group) {
next := cloneCodexObject(group)
next["hooks"] = kept
result = append(result, next)
}
}
return result
}
func removeManagedCodexHooks(hooks codexHooks, agentID string) codexHooks {
next := cloneCodexHooks(hooks)
for _, contract := range []struct{ event, script, status string }{
{"PreToolUse", codexGuardScript, codexGuardStatusMessage},
{"PostToolUse", codexAuditScript, codexAuditStatusMessage},
} {
groups := removeCodexGroups(
next[contract.event], contract.script, contract.status, agentID,
)
if len(groups) == 0 {
delete(next, contract.event)
} else {
next[contract.event] = groups
}
}
return next
}
func entirelyManagedCodexDocument(document *codexDocument) bool {
if !document.exists || document.root["description"] != codexOwnedDescription ||
len(document.hooks) == 0 {
return false
}
for key := range document.root {
if key != "description" && key != "hooks" {
return false
}
}
if settings["description"] != codexOwnedDescription {
return false
}
for event, value := range hooks {
if event != "PreToolUse" && event != "PostToolUse" {
handlerCount := 0
for event, groups := range document.hooks {
contract := map[string][2]string{
"PreToolUse": {codexGuardScript, codexGuardStatusMessage},
"PostToolUse": {codexAuditScript, codexAuditStatusMessage},
}[event]
if contract[0] == "" || len(groups) == 0 {
return false
}
groups, ok := value.([]map[string]any)
if !ok || len(groups) != 0 {
return false
for _, group := range groups {
handlers := group["hooks"].([]any)
if !exactCodexMatcherGroup(group) || len(handlers) == 0 {
return false
}
for _, value := range handlers {
if _, managed := codexManagedAgentID(
value.(map[string]any), contract[0], contract[1],
); !managed {
return false
}
handlerCount++
}
}
}
return true
return handlerCount > 0
}
func codexCommandReferences(handler map[string]any, scriptPath string) bool {
expected := normalizeCodexPath(scriptPath)
for _, key := range []string{"command", "commandWindows"} {
command, ok := handler[key].(string)
if ok && codexCommandEndsWith(normalizeCodexPath(command), expected) {
return true
}
func renderCodexDocument(
document *codexDocument,
hooks codexHooks,
) (*codexRenderedDocument, error) {
if !document.exists && len(hooks) == 0 {
return &codexRenderedDocument{document: document}, nil
}
return false
if document.exists && reflect.DeepEqual(hooks, document.hooks) {
return &codexRenderedDocument{document: document}, nil
}
if len(hooks) == 0 && entirelyManagedCodexDocument(document) {
return &codexRenderedDocument{document: document, changed: true, remove: true}, nil
}
root := cloneCodexObject(document.root)
if len(hooks) == 0 {
delete(root, "hooks")
} else {
root["hooks"] = hooks
}
next, err := json.MarshalIndent(root, "", " ")
if err != nil {
return nil, err
}
next = append(next, '\n')
return &codexRenderedDocument{
document: document,
changed: !document.exists || !bytes.Equal(next, document.raw),
next: next,
}, nil
}
func codexRuntimeScriptsExist(guard, audit map[string]any) (bool, error) {
home, err := os.UserHomeDir()
if err != nil {
return false, fmt.Errorf("resolve home directory: %w", err)
func managedCodexIDs(
groups []map[string]any,
scriptName string,
status string,
) map[string]string {
result := map[string]string{}
for _, group := range groups {
if !exactCodexMatcherGroup(group) {
continue
}
for _, value := range group["hooks"].([]any) {
agentID, managed := codexManagedAgentID(value.(map[string]any), scriptName, status)
if !managed {
continue
}
key := agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(agentID)
}
result[key] = agentID
}
}
root := filepath.Join(home, ".elydora")
entries, err := os.ReadDir(root)
return result
}
func codexRuntimeContracts(hooks codexHooks) ([]codexRuntimeContract, error) {
guards := managedCodexIDs(hooks["PreToolUse"], codexGuardScript, codexGuardStatusMessage)
audits := managedCodexIDs(hooks["PostToolUse"], codexAuditScript, codexAuditStatusMessage)
keys := make([]string, 0, len(guards))
for key := range guards {
if _, exists := audits[key]; exists {
keys = append(keys, key)
}
}
sort.Strings(keys)
root, err := AgentRuntimeRoot()
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("read Elydora runtime directory at %s: %w", root, err)
return nil, err
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
agentDir := filepath.Join(root, entry.Name())
guardPath := filepath.Join(agentDir, "guard.js")
hookPath := filepath.Join(agentDir, "hook.js")
if !codexCommandReferences(guard, guardPath) || !codexCommandReferences(audit, hookPath) {
continue
}
configPath := filepath.Join(agentDir, "config.json")
config, exists, err := readHookJSONObject(configPath, "Elydora runtime config")
if err != nil {
return false, err
}
if !exists {
continue
}
agentName, ok := config["agent_name"].(string)
if !ok {
return false, fmt.Errorf(`Elydora runtime config at %s field "agent_name" must be a string`, configPath)
}
if agentName != codexAgentKey {
continue
}
guardExists, err := regularFileExists(guardPath, "Elydora guard runtime")
if err != nil {
return false, err
}
hookExists, err := regularFileExists(hookPath, "Elydora audit runtime")
if err != nil {
return false, err
}
return guardExists && hookExists, nil
contracts := make([]codexRuntimeContract, 0, len(keys))
for _, key := range keys {
agentID := guards[key]
directory := filepath.Join(root, agentID)
contracts = append(contracts, codexRuntimeContract{
agentID: agentID,
guardPath: filepath.Join(directory, codexGuardScript),
auditPath: filepath.Join(directory, codexAuditScript),
})
}
return false, nil
return contracts, nil
}