Files
Elydora-Go-SDK/cmd/elydora/plugins/augment_contract.go
T
雷电芽衣 96489cd051 chore(go): audit providers kirocli kiroide opencode
Move Kiro IDE onto managed_runtime.go and shell_command.go, share
cloneJSONObject across providers, make OpenCode status verify the managed
runtime contract instead of any hook.js, propagate script removal errors,
and build the legacy runtime config through buildManagedRuntimeConfig.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 02:32:20 -04:00

399 lines
10 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 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 cloneJSONObject(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: cloneJSONObject(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 := cloneJSONObject(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 := cloneJSONObject(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
}