Files
Elydora-Go-SDK/cmd/elydora/plugins/codex_contract.go
T

253 lines
6.8 KiB
Go

package plugins
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
const (
codexAgentKey = "codex"
codexOwnedDescription = "Elydora audit and freeze enforcement"
codexGuardStatusMessage = "Checking Elydora agent state"
codexAuditStatusMessage = "Recording Elydora tool use"
)
func cloneCodexObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
}
return clone
}
func codexHooksObject(settings map[string]any) (map[string]any, error) {
value, exists := settings["hooks"]
if !exists {
return map[string]any{}, nil
}
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
}
func codexEventGroups(hooks map[string]any, event string) ([]map[string]any, error) {
value, exists := hooks[event]
if !exists {
return []map[string]any{}, nil
}
groups, ok := value.([]any)
if !ok {
return nil, fmt.Errorf(`Codex hooks config field "hooks.%s" must be an array of objects`, event)
}
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 normalized
}
func isManagedCodexCommand(command any, scriptName, agentID string) bool {
text, ok := command.(string)
if !ok {
return false
}
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
}
filteredHandlers := make([]any, 0, len(handlers))
removed := false
for _, handler := range handlers {
if isElydoraCodexHandler(handler, agentID) {
changed = true
removed = true
continue
}
filteredHandlers = append(filteredHandlers, handler)
}
if len(filteredHandlers) == 0 && removed {
continue
}
nextGroup := cloneCodexObject(group)
nextGroup["hooks"] = filteredHandlers
filteredGroups = append(filteredGroups, nextGroup)
}
return filteredGroups, changed, nil
}
func findCodexHandler(hooks map[string]any, event, status string) (map[string]any, error) {
groups, err := codexEventGroups(hooks, event)
if err != nil {
return nil, err
}
for _, group := range groups {
handlers, err := codexGroupHandlers(group)
if err != nil {
return nil, err
}
for _, handler := range handlers {
if handler["statusMessage"] == status && isElydoraCodexHandler(handler, "") {
return handler, nil
}
}
}
return nil, nil
}
func codexSettingsOwned(settings, hooks map[string]any) bool {
for key := range settings {
if key != "description" && key != "hooks" {
return false
}
}
if settings["description"] != codexOwnedDescription {
return false
}
for event, value := range hooks {
if event != "PreToolUse" && event != "PostToolUse" {
return false
}
groups, ok := value.([]map[string]any)
if !ok || len(groups) != 0 {
return false
}
}
return true
}
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
}
}
return false
}
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)
}
root := filepath.Join(home, ".elydora")
entries, err := os.ReadDir(root)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("read Elydora runtime directory at %s: %w", root, 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
}
return false, nil
}