fix: align Grok hooks with official contracts
This commit is contained in:
1 parent
c60b46b994
commit
f71db95502
19 files changed
+2241
-641
No files matched your search
+79
-144
@@ -4,178 +4,113 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func grokHomeDirectory() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
func grokHomePath() (string, error) {
|
||||
configured := os.Getenv("GROK_HOME")
|
||||
if configured == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".grok"), nil
|
||||
}
|
||||
home, err := filepath.Abs(configured)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve home directory: %w", err)
|
||||
return "", fmt.Errorf("resolve GROK_HOME at %s: %w", configured, 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()
|
||||
home, err := grokHomePath()
|
||||
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
|
||||
return filepath.Join(home, "hooks", grokConfigFile), nil
|
||||
}
|
||||
|
||||
func readGrokConfig() (grokDocument, error) {
|
||||
func readGrokDocument() (*grokDocument, error) {
|
||||
configPath, err := grokConfigPath()
|
||||
if err != nil {
|
||||
return grokDocument{}, err
|
||||
return nil, err
|
||||
}
|
||||
root, exists, err := readHookJSONObject(configPath, "Grok hooks config")
|
||||
home := filepath.Dir(filepath.Dir(configPath))
|
||||
if _, err := managedPhysicalDirectoryExists(home, "Grok home directory"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hooksDirectory := filepath.Dir(configPath)
|
||||
if _, err := managedPhysicalDirectoryExists(
|
||||
hooksDirectory,
|
||||
"Grok hooks directory",
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snapshot, err := readManagedFile(
|
||||
configPath,
|
||||
"Grok user hooks",
|
||||
maxManagedSourceBytes,
|
||||
)
|
||||
if err != nil {
|
||||
return grokDocument{}, err
|
||||
return nil, err
|
||||
}
|
||||
hooks, err := readGrokHooks(root)
|
||||
if err != nil {
|
||||
return grokDocument{}, err
|
||||
if snapshot == nil {
|
||||
return createGrokDocument(configPath), nil
|
||||
}
|
||||
return grokDocument{exists: exists, configPath: configPath, root: root, hooks: hooks}, nil
|
||||
return parseGrokDocument(configPath, snapshot.contents)
|
||||
}
|
||||
|
||||
func quoteGrokWindowsArgument(value string) string {
|
||||
return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"`
|
||||
func prepareRenderedGrokChange(
|
||||
rendered *grokRenderedDocument,
|
||||
) (*fileChange, error) {
|
||||
if rendered == nil || !rendered.changed {
|
||||
return nil, nil
|
||||
}
|
||||
return prepareSourceChange(
|
||||
rendered.document.configPath,
|
||||
"Grok user hooks",
|
||||
rendered.document.raw,
|
||||
rendered.document.exists,
|
||||
rendered.next,
|
||||
0600,
|
||||
rendered.remove,
|
||||
)
|
||||
}
|
||||
|
||||
func buildGrokCommand(runtimePath, scriptPath string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return quoteGrokWindowsArgument(runtimePath) + " " + quoteGrokWindowsArgument(scriptPath)
|
||||
func writeGrokChanges(
|
||||
changes []*fileChange,
|
||||
label string,
|
||||
rename renameFunc,
|
||||
runtimeRoot string,
|
||||
agentDirectory string,
|
||||
hooksDirectory string,
|
||||
) error {
|
||||
hasChanges := false
|
||||
for _, change := range changes {
|
||||
if change != nil {
|
||||
hasChanges = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath)
|
||||
}
|
||||
|
||||
func buildGrokHandler(command string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "command",
|
||||
"command": command,
|
||||
"timeout": grokHookTimeout,
|
||||
if !hasChanges {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if agentDirectory != "" {
|
||||
if err := EnsurePrivateDirectory(runtimeRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
if err := EnsurePrivateDirectory(agentDirectory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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)
|
||||
if err := ensureManagedDirectory(
|
||||
filepath.Dir(hooksDirectory),
|
||||
"Grok home directory",
|
||||
); err != nil {
|
||||
return 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
|
||||
if err := ensureManagedDirectory(hooksDirectory, "Grok hooks directory"); err != nil {
|
||||
return err
|
||||
}
|
||||
return false, nil
|
||||
return writeChanges(changes, label, rename)
|
||||
}
|
||||
Reference in new issue
Block a user