Move the four providers onto managed_runtime.go and shell_command.go; keep the Auggie wrappers, Cline metadata wrappers, and the Copilot and Droid precondition handling provider-local. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
407 lines
11 KiB
Go
407 lines
11 KiB
Go
package plugins
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
augmentAgentKey = "augment"
|
|
augmentGuardScript = "guard.js"
|
|
augmentAuditScript = "hook.js"
|
|
augmentHookTimeout = float64(10_000)
|
|
)
|
|
|
|
var (
|
|
augmentToolEvents = map[string]struct{}{
|
|
"PreToolUse": {},
|
|
"PostToolUse": {},
|
|
}
|
|
augmentSessionEvents = map[string]struct{}{
|
|
"Stop": {},
|
|
"SessionStart": {},
|
|
"SessionEnd": {},
|
|
"Notification": {},
|
|
"PromptSubmit": {},
|
|
}
|
|
)
|
|
|
|
type augmentGroup struct {
|
|
object map[string]any
|
|
handlers []map[string]any
|
|
}
|
|
|
|
type augmentHooks map[string][]augmentGroup
|
|
|
|
type augmentDocument struct {
|
|
exists bool
|
|
configPath string
|
|
root map[string]any
|
|
hooks augmentHooks
|
|
raw []byte
|
|
}
|
|
|
|
type augmentRenderedDocument struct {
|
|
document *augmentDocument
|
|
changed bool
|
|
next []byte
|
|
remove bool
|
|
}
|
|
|
|
type augmentWrapperPaths struct {
|
|
guard string
|
|
audit string
|
|
}
|
|
|
|
type augmentRuntimeContract struct {
|
|
agentID string
|
|
guardPath string
|
|
auditPath string
|
|
guardWrapper string
|
|
auditWrapper string
|
|
}
|
|
|
|
func cloneAugmentObject(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 validateAugmentHandler(
|
|
value any,
|
|
event string,
|
|
groupIndex, handlerIndex int,
|
|
) (map[string]any, error) {
|
|
label := fmt.Sprintf(
|
|
"Auggie settings handler hooks.%s[%d].hooks[%d]",
|
|
event, groupIndex, handlerIndex,
|
|
)
|
|
handler, ok := value.(map[string]any)
|
|
if !ok || handler == nil {
|
|
return nil, fmt.Errorf("%s must be an object", label)
|
|
}
|
|
handlerType, ok := handler["type"].(string)
|
|
if !ok || handlerType != "command" {
|
|
return nil, fmt.Errorf(`%s type must be "command"`, label)
|
|
}
|
|
command, ok := handler["command"].(string)
|
|
if !ok || command == "" {
|
|
return nil, fmt.Errorf("%s requires a non-empty command", label)
|
|
}
|
|
if arguments, exists := handler["args"]; exists {
|
|
values, ok := arguments.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s args must be an array of strings", label)
|
|
}
|
|
for _, argument := range values {
|
|
if _, ok := argument.(string); !ok {
|
|
return nil, fmt.Errorf("%s args must be an array of strings", label)
|
|
}
|
|
}
|
|
}
|
|
if timeoutValue, exists := handler["timeout"]; exists {
|
|
timeout, ok := timeoutValue.(float64)
|
|
if !ok || math.IsNaN(timeout) || math.IsInf(timeout, 0) || timeout <= 0 {
|
|
return nil, fmt.Errorf("%s timeout must be a positive finite number", label)
|
|
}
|
|
}
|
|
return cloneAugmentObject(handler), nil
|
|
}
|
|
|
|
func validateAugmentMetadata(value any, label string) error {
|
|
metadata, ok := value.(map[string]any)
|
|
if !ok || metadata == nil {
|
|
return fmt.Errorf("%s metadata must be an object", label)
|
|
}
|
|
for _, key := range []string{
|
|
"includeConversationData",
|
|
"includeMCPMetadata",
|
|
"includeUserContext",
|
|
} {
|
|
if field, exists := metadata[key]; exists {
|
|
if _, ok := field.(bool); !ok {
|
|
return fmt.Errorf("%s metadata.%s must be a boolean", label, key)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateAugmentGroup(value any, event string, groupIndex int) (augmentGroup, error) {
|
|
label := fmt.Sprintf("Auggie settings group hooks.%s[%d]", event, groupIndex)
|
|
object, ok := value.(map[string]any)
|
|
if !ok || object == nil {
|
|
return augmentGroup{}, fmt.Errorf("%s must be an object", label)
|
|
}
|
|
if _, sessionEvent := augmentSessionEvents[event]; sessionEvent {
|
|
if _, hasMatcher := object["matcher"]; hasMatcher {
|
|
return augmentGroup{}, fmt.Errorf("%s matcher is only supported for tool events", label)
|
|
}
|
|
}
|
|
if matcher, exists := object["matcher"]; exists {
|
|
if _, ok := matcher.(string); !ok {
|
|
return augmentGroup{}, fmt.Errorf("%s matcher must be a string", label)
|
|
}
|
|
}
|
|
if metadata, exists := object["metadata"]; exists {
|
|
if err := validateAugmentMetadata(metadata, label); err != nil {
|
|
return augmentGroup{}, err
|
|
}
|
|
}
|
|
values, ok := object["hooks"].([]any)
|
|
if !ok {
|
|
return augmentGroup{}, fmt.Errorf("%s must contain a hooks array", label)
|
|
}
|
|
handlers := make([]map[string]any, 0, len(values))
|
|
for handlerIndex, value := range values {
|
|
handler, err := validateAugmentHandler(value, event, groupIndex, handlerIndex)
|
|
if err != nil {
|
|
return augmentGroup{}, err
|
|
}
|
|
handlers = append(handlers, handler)
|
|
}
|
|
return augmentGroup{object: cloneAugmentObject(object), handlers: handlers}, nil
|
|
}
|
|
|
|
func readAugmentHooks(root map[string]any) (augmentHooks, error) {
|
|
value, exists := root["hooks"]
|
|
if !exists {
|
|
return augmentHooks{}, nil
|
|
}
|
|
object, ok := value.(map[string]any)
|
|
if !ok || object == nil {
|
|
return nil, fmt.Errorf(`Auggie settings field "hooks" must be an object`)
|
|
}
|
|
hooks := make(augmentHooks, len(object))
|
|
for event, groupValue := range object {
|
|
if _, toolEvent := augmentToolEvents[event]; !toolEvent {
|
|
if _, sessionEvent := augmentSessionEvents[event]; !sessionEvent {
|
|
return nil, fmt.Errorf(
|
|
`Auggie settings field "hooks.%s" uses an unsupported event`,
|
|
event,
|
|
)
|
|
}
|
|
}
|
|
values, ok := groupValue.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf(
|
|
`Auggie settings field "hooks.%s" must be an array`,
|
|
event,
|
|
)
|
|
}
|
|
groups := make([]augmentGroup, 0, len(values))
|
|
for groupIndex, value := range values {
|
|
group, err := validateAugmentGroup(value, event, groupIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
groups = append(groups, group)
|
|
}
|
|
hooks[event] = groups
|
|
}
|
|
return hooks, nil
|
|
}
|
|
|
|
func parseAugmentDocument(configPath string, raw []byte) (*augmentDocument, error) {
|
|
label := fmt.Sprintf("Auggie user settings at %s", configPath)
|
|
root, err := decodeStrictJSONObject(raw, label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hooks, err := readAugmentHooks(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &augmentDocument{
|
|
exists: true, configPath: configPath, root: root, hooks: hooks,
|
|
raw: append([]byte(nil), raw...),
|
|
}, nil
|
|
}
|
|
|
|
func createAugmentDocument(configPath string) *augmentDocument {
|
|
return &augmentDocument{
|
|
configPath: configPath,
|
|
root: map[string]any{},
|
|
hooks: augmentHooks{},
|
|
}
|
|
}
|
|
|
|
func renderAugmentDocument(
|
|
document *augmentDocument,
|
|
hooks augmentHooks,
|
|
) (*augmentRenderedDocument, error) {
|
|
if document == nil {
|
|
return nil, fmt.Errorf("Auggie settings document is required")
|
|
}
|
|
if reflect.DeepEqual(hooks, document.hooks) {
|
|
return &augmentRenderedDocument{document: document}, nil
|
|
}
|
|
if !document.exists && len(hooks) == 0 {
|
|
return &augmentRenderedDocument{document: document}, nil
|
|
}
|
|
root := cloneAugmentObject(document.root)
|
|
if len(hooks) == 0 {
|
|
delete(root, "hooks")
|
|
} else {
|
|
root["hooks"] = renderAugmentHooks(hooks)
|
|
}
|
|
if len(root) == 0 {
|
|
return &augmentRenderedDocument{
|
|
document: document,
|
|
changed: true,
|
|
remove: true,
|
|
}, nil
|
|
}
|
|
next, err := json.MarshalIndent(root, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode Auggie user settings: %w", err)
|
|
}
|
|
next = append(next, '\n')
|
|
if _, err := parseAugmentDocument(document.configPath, next); err != nil {
|
|
return nil, fmt.Errorf("validate rendered Auggie user settings: %w", err)
|
|
}
|
|
return &augmentRenderedDocument{
|
|
document: document,
|
|
changed: !document.exists || !bytes.Equal(next, document.raw),
|
|
next: next,
|
|
}, nil
|
|
}
|
|
|
|
func renderAugmentHooks(hooks augmentHooks) map[string]any {
|
|
result := make(map[string]any, len(hooks))
|
|
for event, groups := range hooks {
|
|
values := make([]any, 0, len(groups))
|
|
for _, group := range groups {
|
|
object := cloneAugmentObject(group.object)
|
|
handlers := make([]any, 0, len(group.handlers))
|
|
for _, handler := range group.handlers {
|
|
handlers = append(handlers, handler)
|
|
}
|
|
object["hooks"] = handlers
|
|
values = append(values, object)
|
|
}
|
|
result[event] = values
|
|
}
|
|
return result
|
|
}
|
|
|
|
func readAugmentWindowsArgument(command string) (string, bool) {
|
|
if len(command) < 2 || command[0] != '"' {
|
|
return "", false
|
|
}
|
|
var value strings.Builder
|
|
for index := 1; index < len(command); index++ {
|
|
if command[index] == '\\' && index+1 < len(command) && command[index+1] == '"' {
|
|
value.WriteByte('"')
|
|
index++
|
|
continue
|
|
}
|
|
if command[index] == '"' {
|
|
return value.String(), index == len(command)-1 && value.Len() > 0
|
|
}
|
|
value.WriteByte(command[index])
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func readAugmentPOSIXArgument(command string) (string, bool) {
|
|
value, end, ok := readPOSIXArgument(command, 0)
|
|
return value, ok && end == len(command) && value != ""
|
|
}
|
|
|
|
func parseAugmentCommand(command string) (string, bool) {
|
|
if runtime.GOOS == "windows" {
|
|
return readAugmentWindowsArgument(command)
|
|
}
|
|
return readAugmentPOSIXArgument(command)
|
|
}
|
|
|
|
func managedAugmentAgentID(
|
|
handler map[string]any,
|
|
wrapperName, runtimeRoot string,
|
|
) (string, bool) {
|
|
if handler["type"] != "command" || handler["timeout"] != augmentHookTimeout {
|
|
return "", false
|
|
}
|
|
if _, hasArguments := handler["args"]; hasArguments {
|
|
return "", false
|
|
}
|
|
command, ok := handler["command"].(string)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
wrapperPath, ok := parseAugmentCommand(command)
|
|
if !ok || !sameManagedName(filepath.Base(wrapperPath), wrapperName) {
|
|
return "", false
|
|
}
|
|
agentDirectory := filepath.Dir(wrapperPath)
|
|
if !sameManagedPath(filepath.Dir(agentDirectory), runtimeRoot) {
|
|
return "", false
|
|
}
|
|
agentID := filepath.Base(agentDirectory)
|
|
return agentID, agentID != "" && agentID != "." && agentID != ".."
|
|
}
|
|
|
|
func removeManagedAugmentGroups(
|
|
groups []augmentGroup,
|
|
wrapperName, agentID, runtimeRoot string,
|
|
) ([]augmentGroup, bool) {
|
|
result := make([]augmentGroup, 0, len(groups))
|
|
changed := false
|
|
for _, group := range groups {
|
|
handlers := make([]map[string]any, 0, len(group.handlers))
|
|
groupChanged := false
|
|
for _, handler := range group.handlers {
|
|
managedID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot)
|
|
remove := managed && (agentID == "" || sameManagedAgentID(managedID, agentID))
|
|
if remove {
|
|
changed = true
|
|
groupChanged = true
|
|
continue
|
|
}
|
|
handlers = append(handlers, handler)
|
|
}
|
|
if len(handlers) > 0 || !groupChanged {
|
|
result = append(result, augmentGroup{object: group.object, handlers: handlers})
|
|
}
|
|
}
|
|
return result, changed
|
|
}
|
|
|
|
func removeManagedAugmentHooks(
|
|
hooks augmentHooks,
|
|
agentID, runtimeRoot string,
|
|
) (augmentHooks, bool) {
|
|
result := make(augmentHooks, len(hooks))
|
|
for event, groups := range hooks {
|
|
result[event] = groups
|
|
}
|
|
changed := false
|
|
for _, contract := range []struct{ event, wrapper string }{
|
|
{"PreToolUse", augmentGuardWrapperName()},
|
|
{"PostToolUse", augmentAuditWrapperName()},
|
|
} {
|
|
groups, eventChanged := removeManagedAugmentGroups(
|
|
result[contract.event], contract.wrapper, agentID, runtimeRoot,
|
|
)
|
|
if !eventChanged {
|
|
continue
|
|
}
|
|
changed = true
|
|
if len(groups) == 0 {
|
|
delete(result, contract.event)
|
|
} else {
|
|
result[contract.event] = groups
|
|
}
|
|
}
|
|
return result, changed
|
|
}
|