feat: add native Grok hook integration
This commit is contained in:
1 parent
287322977b
commit
a0fe657959
8 files changed
+1140
No files matched your search
@@ -0,0 +1,181 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user