feat: add native Auggie hook integration
This commit is contained in:
1 parent
a0fe657959
commit
9d03a68477
8 files changed
+1429
No files matched your search
@@ -0,0 +1,292 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const augmentRegexValidator = `import fs from "node:fs";
|
||||
const pattern = fs.readFileSync(0, "utf8");
|
||||
try {
|
||||
new RegExp(pattern);
|
||||
} catch (error) {
|
||||
process.stderr.write(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}`
|
||||
|
||||
func augmentGuardWrapperName() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "augment-guard.cmd"
|
||||
}
|
||||
return "augment-guard.sh"
|
||||
}
|
||||
|
||||
func augmentAuditWrapperName() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "augment-hook.cmd"
|
||||
}
|
||||
return "augment-hook.sh"
|
||||
}
|
||||
|
||||
func augmentHomeDirectory() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve home directory: %w", err)
|
||||
}
|
||||
return home, nil
|
||||
}
|
||||
|
||||
func augmentRuntimeRoot() (string, error) {
|
||||
home, err := augmentHomeDirectory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, ".elydora"), nil
|
||||
}
|
||||
|
||||
func augmentConfigPath() (string, error) {
|
||||
home, err := augmentHomeDirectory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, ".augment", "settings.json"), nil
|
||||
}
|
||||
|
||||
func readAugmentConfig() (augmentDocument, error) {
|
||||
configPath, err := augmentConfigPath()
|
||||
if err != nil {
|
||||
return augmentDocument{}, err
|
||||
}
|
||||
root, exists, err := readHookJSONObject(configPath, "Auggie settings")
|
||||
if err != nil {
|
||||
return augmentDocument{}, err
|
||||
}
|
||||
hooks, err := readAugmentHooks(root)
|
||||
if err != nil {
|
||||
return augmentDocument{}, err
|
||||
}
|
||||
return augmentDocument{
|
||||
exists: exists,
|
||||
configPath: configPath,
|
||||
root: root,
|
||||
hooks: hooks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateAugmentMatchers(hooks augmentHooks, nodePath string) error {
|
||||
events := make([]string, 0, len(hooks))
|
||||
for event := range hooks {
|
||||
events = append(events, event)
|
||||
}
|
||||
sort.Strings(events)
|
||||
for _, event := range events {
|
||||
for groupIndex, group := range hooks[event] {
|
||||
matcher, exists := group.object["matcher"].(string)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
// #nosec G204 -- nodePath is resolved with exec.LookPath and matcher data uses stdin.
|
||||
command := exec.Command(
|
||||
nodePath, "--input-type=module", "--eval", augmentRegexValidator,
|
||||
)
|
||||
command.Stdin = strings.NewReader(matcher)
|
||||
output, err := command.CombinedOutput()
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
message := strings.TrimSpace(string(output))
|
||||
if message == "" {
|
||||
message = err.Error()
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"Auggie settings group hooks.%s[%d] matcher "+
|
||||
"must be a valid JavaScript regular expression: %s",
|
||||
event, groupIndex, message,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func quoteAugmentWindowsCommand(value string) string {
|
||||
return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"`
|
||||
}
|
||||
|
||||
func quoteAugmentBatchArgument(value string) string {
|
||||
return `"` + strings.ReplaceAll(value, "%", "%%") + `"`
|
||||
}
|
||||
|
||||
func buildAugmentCommand(wrapperPath string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return quoteAugmentWindowsCommand(wrapperPath)
|
||||
}
|
||||
return quotePOSIXArgument(wrapperPath)
|
||||
}
|
||||
|
||||
func buildAugmentWrapper(runtimePath, scriptPath string) []byte {
|
||||
if runtime.GOOS == "windows" {
|
||||
return []byte(
|
||||
"@echo off\r\n" +
|
||||
quoteAugmentBatchArgument(runtimePath) + " " +
|
||||
quoteAugmentBatchArgument(scriptPath) + "\r\n" +
|
||||
"exit /b %errorlevel%\r\n",
|
||||
)
|
||||
}
|
||||
return []byte(
|
||||
"#!/bin/sh\nexec " +
|
||||
quotePOSIXArgument(runtimePath) + " " +
|
||||
quotePOSIXArgument(scriptPath) + "\n",
|
||||
)
|
||||
}
|
||||
|
||||
func buildAugmentHandler(wrapperPath string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "command",
|
||||
"command": buildAugmentCommand(wrapperPath),
|
||||
"timeout": augmentHookTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func buildAugmentGroup(handler map[string]any) augmentGroup {
|
||||
return augmentGroup{
|
||||
object: map[string]any{"matcher": ".*"},
|
||||
handlers: []map[string]any{handler},
|
||||
}
|
||||
}
|
||||
|
||||
func resolveAugmentWrapperPaths(runtimeRoot, agentID string) augmentWrapperPaths {
|
||||
agentDirectory := filepath.Join(runtimeRoot, agentID)
|
||||
return augmentWrapperPaths{
|
||||
guard: filepath.Join(agentDirectory, augmentGuardWrapperName()),
|
||||
audit: filepath.Join(agentDirectory, augmentAuditWrapperName()),
|
||||
}
|
||||
}
|
||||
|
||||
func managedAugmentIDs(
|
||||
groups []augmentGroup,
|
||||
wrapperName, runtimeRoot string,
|
||||
) map[string]string {
|
||||
result := map[string]string{}
|
||||
for _, group := range groups {
|
||||
for _, handler := range group.handlers {
|
||||
agentID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
|
||||
if managed {
|
||||
key := agentID
|
||||
if runtime.GOOS == "windows" {
|
||||
key = strings.ToLower(key)
|
||||
}
|
||||
result[key] = agentID
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func augmentRuntimeContracts(
|
||||
hooks augmentHooks,
|
||||
runtimeRoot string,
|
||||
) []augmentRuntimeContract {
|
||||
guards := managedAugmentIDs(
|
||||
hooks["PreToolUse"], augmentGuardWrapperName(), runtimeRoot,
|
||||
)
|
||||
audits := managedAugmentIDs(
|
||||
hooks["PostToolUse"], augmentAuditWrapperName(), 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([]augmentRuntimeContract, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
agentID := guards[key]
|
||||
agentDirectory := filepath.Join(runtimeRoot, agentID)
|
||||
contracts = append(contracts, augmentRuntimeContract{
|
||||
agentID: agentID,
|
||||
guardPath: filepath.Join(agentDirectory, augmentGuardScript),
|
||||
auditPath: filepath.Join(agentDirectory, augmentAuditScript),
|
||||
guardWrapper: filepath.Join(agentDirectory, augmentGuardWrapperName()),
|
||||
auditWrapper: filepath.Join(agentDirectory, augmentAuditWrapperName()),
|
||||
})
|
||||
}
|
||||
return contracts
|
||||
}
|
||||
|
||||
func augmentRuntimeFilesExist(
|
||||
contracts []augmentRuntimeContract,
|
||||
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() && sameAugmentAgentID(entry.Name(), contract.agentID) {
|
||||
entryName = entry.Name()
|
||||
break
|
||||
}
|
||||
}
|
||||
if entryName == "" {
|
||||
continue
|
||||
}
|
||||
agentDirectory := filepath.Join(runtimeRoot, entryName)
|
||||
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 != augmentAgentKey {
|
||||
continue
|
||||
}
|
||||
files := []struct {
|
||||
path string
|
||||
label string
|
||||
}{
|
||||
{contract.guardPath, "Elydora guard runtime"},
|
||||
{contract.auditPath, "Elydora audit runtime"},
|
||||
{contract.guardWrapper, "Auggie guard wrapper"},
|
||||
{contract.auditWrapper, "Auggie audit wrapper"},
|
||||
}
|
||||
complete := true
|
||||
for _, file := range files {
|
||||
exists, err := regularFileExists(file.path, file.label)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
complete = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if complete {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in new issue
Block a user