182 lines
4.7 KiB
Go
182 lines
4.7 KiB
Go
package plugins
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
func grokHomeDirectory() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve home directory: %w", err)
|
|
}
|
|
return home, nil
|
|
}
|
|
|
|
func grokRuntimeRoot() (string, error) {
|
|
home, err := grokHomeDirectory()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, ".elydora"), nil
|
|
}
|
|
|
|
func grokConfigPath() (string, error) {
|
|
home, err := grokHomeDirectory()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
grokHome := os.Getenv("GROK_HOME")
|
|
if grokHome == "" {
|
|
grokHome = filepath.Join(home, ".grok")
|
|
}
|
|
// #nosec G703 -- GROK_HOME is an explicit local-user configuration path.
|
|
return filepath.Join(grokHome, "hooks", grokConfigFile), nil
|
|
}
|
|
|
|
func readGrokConfig() (grokDocument, error) {
|
|
configPath, err := grokConfigPath()
|
|
if err != nil {
|
|
return grokDocument{}, err
|
|
}
|
|
root, exists, err := readHookJSONObject(configPath, "Grok hooks config")
|
|
if err != nil {
|
|
return grokDocument{}, err
|
|
}
|
|
hooks, err := readGrokHooks(root)
|
|
if err != nil {
|
|
return grokDocument{}, err
|
|
}
|
|
return grokDocument{exists: exists, configPath: configPath, root: root, hooks: hooks}, nil
|
|
}
|
|
|
|
func quoteGrokWindowsArgument(value string) string {
|
|
return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"`
|
|
}
|
|
|
|
func buildGrokCommand(runtimePath, scriptPath string) string {
|
|
if runtime.GOOS == "windows" {
|
|
return quoteGrokWindowsArgument(runtimePath) + " " + quoteGrokWindowsArgument(scriptPath)
|
|
}
|
|
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath)
|
|
}
|
|
|
|
func buildGrokHandler(command string) map[string]any {
|
|
return map[string]any{
|
|
"type": "command",
|
|
"command": command,
|
|
"timeout": grokHookTimeout,
|
|
}
|
|
}
|
|
|
|
func buildGrokGroup(handler map[string]any) grokGroup {
|
|
return grokGroup{object: map[string]any{}, handlers: []map[string]any{handler}}
|
|
}
|
|
|
|
func managedGrokIDs(groups []grokGroup, scriptName, runtimeRoot string) map[string]string {
|
|
result := map[string]string{}
|
|
for _, group := range groups {
|
|
if _, hasMatcher := group.object["matcher"]; hasMatcher {
|
|
continue
|
|
}
|
|
for _, handler := range group.handlers {
|
|
agentID, managed := managedGrokAgentID(handler, scriptName, runtimeRoot)
|
|
if managed {
|
|
key := agentID
|
|
if runtime.GOOS == "windows" {
|
|
key = strings.ToLower(key)
|
|
}
|
|
result[key] = agentID
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func grokRuntimeContracts(hooks grokHooks, runtimeRoot string) []grokRuntimeContract {
|
|
guards := managedGrokIDs(hooks["PreToolUse"], grokGuardScript, runtimeRoot)
|
|
audits := managedGrokIDs(hooks["PostToolUse"], grokAuditScript, runtimeRoot)
|
|
keys := make([]string, 0, len(guards))
|
|
for key := range guards {
|
|
if _, exists := audits[key]; exists {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
contracts := make([]grokRuntimeContract, 0, len(keys))
|
|
for _, key := range keys {
|
|
agentID := guards[key]
|
|
contracts = append(contracts, grokRuntimeContract{
|
|
agentID: agentID,
|
|
guardPath: filepath.Join(runtimeRoot, agentID, grokGuardScript),
|
|
auditPath: filepath.Join(runtimeRoot, agentID, grokAuditScript),
|
|
})
|
|
}
|
|
return contracts
|
|
}
|
|
|
|
func grokRuntimeScriptsExist(contracts []grokRuntimeContract, runtimeRoot string) (bool, error) {
|
|
entries, err := os.ReadDir(runtimeRoot)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("read Elydora runtime directory at %s: %w", runtimeRoot, err)
|
|
}
|
|
for _, contract := range contracts {
|
|
var entryName string
|
|
for _, entry := range entries {
|
|
if entry.IsDir() && sameGrokAgentID(entry.Name(), contract.agentID) {
|
|
entryName = entry.Name()
|
|
break
|
|
}
|
|
}
|
|
if entryName == "" {
|
|
continue
|
|
}
|
|
agentDirectory := filepath.Join(runtimeRoot, entryName)
|
|
guardPath := filepath.Join(agentDirectory, grokGuardScript)
|
|
auditPath := filepath.Join(agentDirectory, grokAuditScript)
|
|
if !sameGrokPath(guardPath, contract.guardPath) ||
|
|
!sameGrokPath(auditPath, contract.auditPath) {
|
|
continue
|
|
}
|
|
configPath := filepath.Join(agentDirectory, "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 != grokAgentKey {
|
|
continue
|
|
}
|
|
guardExists, err := regularFileExists(
|
|
guardPath, "Elydora guard runtime",
|
|
)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
auditExists, err := regularFileExists(
|
|
auditPath, "Elydora audit runtime",
|
|
)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return guardExists && auditExists, nil
|
|
}
|
|
return false, nil
|
|
}
|