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
This commit is contained in:
雷电芽衣 committed 2026-09-03 02:32:20 -04:00
1 parent 34ff4c1ea8
commit 96489cd051
20 files changed
+185 -650

No files matched your search

+4 -12
View File
@@ -67,14 +67,6 @@ type augmentRuntimeContract struct {
auditWrapper 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( func validateAugmentHandler(
value any, value any,
event string, event string,
@@ -113,7 +105,7 @@ func validateAugmentHandler(
return nil, fmt.Errorf("%s timeout must be a positive finite number", label) return nil, fmt.Errorf("%s timeout must be a positive finite number", label)
} }
} }
return cloneAugmentObject(handler), nil return cloneJSONObject(handler), nil
} }
func validateAugmentMetadata(value any, label string) error { func validateAugmentMetadata(value any, label string) error {
@@ -168,7 +160,7 @@ func validateAugmentGroup(value any, event string, groupIndex int) (augmentGroup
} }
handlers = append(handlers, handler) handlers = append(handlers, handler)
} }
return augmentGroup{object: cloneAugmentObject(object), handlers: handlers}, nil return augmentGroup{object: cloneJSONObject(object), handlers: handlers}, nil
} }
func readAugmentHooks(root map[string]any) (augmentHooks, error) { func readAugmentHooks(root map[string]any) (augmentHooks, error) {
@@ -247,7 +239,7 @@ func renderAugmentDocument(
if !document.exists && len(hooks) == 0 { if !document.exists && len(hooks) == 0 {
return &augmentRenderedDocument{document: document}, nil return &augmentRenderedDocument{document: document}, nil
} }
root := cloneAugmentObject(document.root) root := cloneJSONObject(document.root)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
} else { } else {
@@ -280,7 +272,7 @@ func renderAugmentHooks(hooks augmentHooks) map[string]any {
for event, groups := range hooks { for event, groups := range hooks {
values := make([]any, 0, len(groups)) values := make([]any, 0, len(groups))
for _, group := range groups { for _, group := range groups {
object := cloneAugmentObject(group.object) object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers)) handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers { for _, handler := range group.handlers {
handlers = append(handlers, handler) handlers = append(handlers, handler)
+4 -12
View File
@@ -79,14 +79,6 @@ type claudeRenderedDocument struct {
type claudeRuntimeContract = managedRuntimeContract type claudeRuntimeContract = managedRuntimeContract
func cloneClaudeObject(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 cloneClaudeHooks(source claudeHooks) claudeHooks { func cloneClaudeHooks(source claudeHooks) claudeHooks {
clone := make(claudeHooks, len(source)) clone := make(claudeHooks, len(source))
for event, groups := range source { for event, groups := range source {
@@ -295,7 +287,7 @@ func validateClaudeHandler(
} }
} }
} }
return cloneClaudeObject(handler), nil return cloneJSONObject(handler), nil
} }
func validateClaudeGroup(value any, event string, groupIndex int) (claudeGroup, error) { func validateClaudeGroup(value any, event string, groupIndex int) (claudeGroup, error) {
@@ -328,7 +320,7 @@ func validateClaudeGroup(value any, event string, groupIndex int) (claudeGroup,
} }
handlers = append(handlers, handler) handlers = append(handlers, handler)
} }
return claudeGroup{object: cloneClaudeObject(object), handlers: handlers}, nil return claudeGroup{object: cloneJSONObject(object), handlers: handlers}, nil
} }
func readClaudeHooks(root map[string]any) (claudeHooks, error) { func readClaudeHooks(root map[string]any) (claudeHooks, error) {
@@ -407,7 +399,7 @@ func renderClaudeHooks(hooks claudeHooks) map[string]any {
for event, groups := range hooks { for event, groups := range hooks {
values := make([]any, 0, len(groups)) values := make([]any, 0, len(groups))
for _, group := range groups { for _, group := range groups {
object := cloneClaudeObject(group.object) object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers)) handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers { for _, handler := range group.handlers {
handlers = append(handlers, handler) handlers = append(handlers, handler)
@@ -435,7 +427,7 @@ func renderClaudeDocument(
document: document, changed: true, remove: true, document: document, changed: true, remove: true,
}, nil }, nil
} }
root := cloneClaudeObject(document.root) root := cloneJSONObject(document.root)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
} else { } else {
+1 -1
View File
@@ -99,7 +99,7 @@ func removeManagedClaudeGroups(
kept = append(kept, handler) kept = append(kept, handler)
} }
if len(kept) > 0 || !exactManagedClaudeGroup(group) { if len(kept) > 0 || !exactManagedClaudeGroup(group) {
object := cloneClaudeObject(group.object) object := cloneJSONObject(group.object)
object["hooks"] = group.object["hooks"] object["hooks"] = group.object["hooks"]
result = append(result, claudeGroup{object: object, handlers: kept}) result = append(result, claudeGroup{object: object, handlers: kept})
} }
+2 -10
View File
@@ -39,14 +39,6 @@ type codexRenderedDocument struct {
type codexRuntimeContract = managedRuntimeContract type codexRuntimeContract = managedRuntimeContract
func cloneCodexObject(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 cloneCodexHooks(source codexHooks) codexHooks { func cloneCodexHooks(source codexHooks) codexHooks {
clone := make(codexHooks, len(source)) clone := make(codexHooks, len(source))
for event, groups := range source { for event, groups := range source {
@@ -147,7 +139,7 @@ func removeCodexGroups(
kept = append(kept, handler) kept = append(kept, handler)
} }
if len(kept) > 0 || !exactCodexMatcherGroup(group) { if len(kept) > 0 || !exactCodexMatcherGroup(group) {
next := cloneCodexObject(group) next := cloneJSONObject(group)
next["hooks"] = kept next["hooks"] = kept
result = append(result, next) result = append(result, next)
} }
@@ -223,7 +215,7 @@ func renderCodexDocument(
if len(hooks) == 0 && entirelyManagedCodexDocument(document) { if len(hooks) == 0 && entirelyManagedCodexDocument(document) {
return &codexRenderedDocument{document: document, changed: true, remove: true}, nil return &codexRenderedDocument{document: document, changed: true, remove: true}, nil
} }
root := cloneCodexObject(document.root) root := cloneJSONObject(document.root)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
} else { } else {
+1 -9
View File
@@ -74,14 +74,6 @@ func copilotManagedEntryForHandler(
return resolveManagedScript(scriptPath, scriptName) return resolveManagedScript(scriptPath, scriptName)
} }
func cloneCopilotObject(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 cloneCopilotHooks(source copilotHooks) copilotHooks { func cloneCopilotHooks(source copilotHooks) copilotHooks {
clone := make(copilotHooks, len(source)) clone := make(copilotHooks, len(source))
for event, handlers := range source { for event, handlers := range source {
@@ -240,7 +232,7 @@ func renderCopilotDocument(
document: document, changed: true, remove: true, document: document, changed: true, remove: true,
}, nil }, nil
} }
root := cloneCopilotObject(document.root) root := cloneJSONObject(document.root)
root["version"] = float64(1) root["version"] = float64(1)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
+1 -1
View File
@@ -274,7 +274,7 @@ func validateCopilotHooks(value any, label string) (copilotHooks, error) {
if err := validateCopilotHandler(handler, event, itemLabel); err != nil { if err := validateCopilotHandler(handler, event, itemLabel); err != nil {
return nil, err return nil, err
} }
handlers = append(handlers, cloneCopilotObject(handler)) handlers = append(handlers, cloneJSONObject(handler))
} }
hooks[event] = handlers hooks[event] = handlers
} }
+4 -12
View File
@@ -46,14 +46,6 @@ type grokRenderedDocument struct {
remove bool remove bool
} }
func cloneGrokObject(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 cloneGrokHooks(source grokHooks) grokHooks { func cloneGrokHooks(source grokHooks) grokHooks {
clone := make(grokHooks, len(source)) clone := make(grokHooks, len(source))
for event, groups := range source { for event, groups := range source {
@@ -120,7 +112,7 @@ func validateGrokHandler(
} }
} }
} }
return cloneGrokObject(handler), nil return cloneJSONObject(handler), nil
} }
func validateGrokGroup(value any, event string, groupIndex int) (grokGroup, error) { func validateGrokGroup(value any, event string, groupIndex int) (grokGroup, error) {
@@ -158,7 +150,7 @@ func validateGrokGroup(value any, event string, groupIndex int) (grokGroup, erro
} }
handlers = append(handlers, handler) handlers = append(handlers, handler)
} }
return grokGroup{object: cloneGrokObject(object), handlers: handlers}, nil return grokGroup{object: cloneJSONObject(object), handlers: handlers}, nil
} }
func readGrokHooks(root map[string]any) (grokHooks, error) { func readGrokHooks(root map[string]any) (grokHooks, error) {
@@ -221,7 +213,7 @@ func renderGrokHooks(hooks grokHooks) map[string]any {
for event, groups := range hooks { for event, groups := range hooks {
values := make([]any, 0, len(groups)) values := make([]any, 0, len(groups))
for _, group := range groups { for _, group := range groups {
object := cloneGrokObject(group.object) object := cloneJSONObject(group.object)
handlers := make([]any, 0, len(group.handlers)) handlers := make([]any, 0, len(group.handlers))
for _, handler := range group.handlers { for _, handler := range group.handlers {
handlers = append(handlers, handler) handlers = append(handlers, handler)
@@ -378,7 +370,7 @@ func renderGrokDocument(
document: document, changed: true, remove: true, document: document, changed: true, remove: true,
}, nil }, nil
} }
root := cloneGrokObject(document.root) root := cloneJSONObject(document.root)
if len(hooks) == 0 { if len(hooks) == 0 {
delete(root, "hooks") delete(root, "hooks")
} else { } else {
+8
View File
@@ -293,3 +293,11 @@ func jsonPointer(path []any) string {
} }
return pointer.String() return pointer.String()
} }
func cloneJSONObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
for key, item := range value {
clone[key] = item
}
return clone
}
+15
View File
@@ -2,6 +2,7 @@ package plugins
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -49,6 +50,20 @@ func hookScriptPath(agentId string) (string, error) {
return filepath.Join(agentDirectory, "hook.js"), nil return filepath.Join(agentDirectory, "hook.js"), nil
} }
// removeAgentScripts deletes hook.js and guard.js; missing files are fine.
func removeAgentScripts(agentID string) error {
for _, resolve := range []func(string) (string, error){hookScriptPath, guardScriptPath} {
path, err := resolve(agentID)
if err != nil {
return err
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove %s: %w", path, err)
}
}
return nil
}
// guardScriptPath returns ~/.elydora/<agentId>/guard.js. // guardScriptPath returns ~/.elydora/<agentId>/guard.js.
func guardScriptPath(agentId string) (string, error) { func guardScriptPath(agentId string) (string, error) {
agentDirectory, err := ResolveAgentRuntimeDirectory(agentId) agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
+1 -1
View File
@@ -79,7 +79,7 @@ func (p *KiroCliPlugin) Install(config InstallConfig) error {
copyKiroObject(nextV2, v2Settings) copyKiroObject(nextV2, v2Settings)
nextV2["hooks"] = v2Hooks nextV2["hooks"] = v2Hooks
nextV3 := cloneKiroObject(v3Settings) nextV3 := cloneJSONObject(v3Settings)
nextV3["version"] = "v1" nextV3["version"] = "v1"
v3Hooks := make([]any, 0, len(currentV3Hooks)+2) v3Hooks := make([]any, 0, len(currentV3Hooks)+2)
for _, hook := range currentV3Hooks { for _, hook := range currentV3Hooks {
+3 -9
View File
@@ -28,12 +28,6 @@ type kiroConfigMutation struct {
remove bool remove bool
} }
func cloneKiroObject(value map[string]any) map[string]any {
clone := make(map[string]any, len(value))
copyKiroObject(clone, value)
return clone
}
func copyKiroObject(destination, source map[string]any) { func copyKiroObject(destination, source map[string]any) {
for key, value := range source { for key, value := range source {
destination[key] = value destination[key] = value
@@ -49,7 +43,7 @@ func kiroHooksObject(settings map[string]any, label string) (map[string]any, err
if !ok || hooks == nil { if !ok || hooks == nil {
return nil, fmt.Errorf(`%s field "hooks" must be an object`, label) return nil, fmt.Errorf(`%s field "hooks" must be an object`, label)
} }
return cloneKiroObject(hooks), nil return cloneJSONObject(hooks), nil
} }
func kiroHookEntries(hooks map[string]any, event, label string) ([]map[string]any, error) { func kiroHookEntries(hooks map[string]any, event, label string) ([]map[string]any, error) {
@@ -193,7 +187,7 @@ func prepareKiroV2Uninstall(
} }
hooks["preToolUse"] = filteredPre hooks["preToolUse"] = filteredPre
hooks["postToolUse"] = filteredPost hooks["postToolUse"] = filteredPost
next := cloneKiroObject(settings) next := cloneJSONObject(settings)
next["hooks"] = hooks next["hooks"] = hooks
mutation.value = next mutation.value = next
mutation.changed = true mutation.changed = true
@@ -250,7 +244,7 @@ func prepareKiroV3Uninstall(
if len(filtered) == len(hooks) { if len(filtered) == len(hooks) {
return mutation, nil return mutation, nil
} }
next := cloneKiroObject(settings) next := cloneJSONObject(settings)
next["hooks"] = filtered next["hooks"] = filtered
owned := true owned := true
for key := range settings { for key := range settings {
+9 -94
View File
@@ -1,101 +1,32 @@
package plugins package plugins
import ( import (
"fmt"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
) )
type kiroIdeRuntimeReference struct {
agentID string
scriptPath string
}
func sameKiroIdePath(left, right string) bool {
left = filepath.Clean(left)
right = filepath.Clean(right)
if absolute, err := filepath.Abs(left); err == nil {
left = absolute
}
if absolute, err := filepath.Abs(right); err == nil {
right = absolute
}
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameKiroIdeAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func sameKiroIdeFileName(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func buildKiroIdeCommand(runtimePath, scriptPath string) (string, error) {
if !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) {
return "", fmt.Errorf(
"Kiro IDE hook commands require absolute runtime and script paths",
)
}
if runtime.GOOS == "windows" {
return encodedWindowsCommand(runtimePath, scriptPath), nil
}
return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath), nil
}
func parseKiroIdeCommand(command string) (string, string, bool) {
if runtime.GOOS == "windows" {
return parseEncodedWindowsCommand(command)
}
return parsePOSIXCommand(command)
}
func kiroIdeRuntimeReferenceForCommand( func kiroIdeRuntimeReferenceForCommand(
command string, command string,
scriptName string, scriptName string,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
runtimePath, scriptPath, ok := parseKiroIdeCommand(command) runtimePath, scriptPath, ok := parseEncodedCommand(command)
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) || if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) {
!isNodeExecutable(runtimePath) ||
!sameKiroIdeFileName(filepath.Base(scriptPath), scriptName) {
return nil, nil return nil, nil
} }
agentDirectory := filepath.Dir(scriptPath) return resolveManagedScript(scriptPath, scriptName)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &kiroIdeRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil
} }
func currentKiroIdeReference( func currentKiroIdeReference(
hook map[string]any, hook map[string]any,
nodePath string, nodePath string,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
reference, err := managedKiroIdeReference(hook) reference, err := managedKiroIdeReference(hook)
if err != nil || reference == nil { if err != nil || reference == nil {
return reference, err return reference, err
} }
action := hook["action"].(map[string]any) action := hook["action"].(map[string]any)
command := action["command"].(string) command := action["command"].(string)
expected, err := buildKiroIdeCommand(nodePath, reference.scriptPath) expected, err := buildEncodedCommand("Kiro IDE", nodePath, reference.scriptPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -105,30 +36,14 @@ func currentKiroIdeReference(
return reference, nil return reference, nil
} }
// legacyKiroIdeGoReference reads the pre-2.1 `node <script>` form.
func legacyKiroIdeGoReference( func legacyKiroIdeGoReference(
command any, command any,
scriptName string, scriptName string,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
text, ok := command.(string) text, ok := command.(string)
if !ok || !strings.HasPrefix(text, "node ") { if !ok || !strings.HasPrefix(text, "node ") {
return nil, nil return nil, nil
} }
scriptPath := strings.TrimPrefix(text, "node ") return resolveManagedScript(strings.TrimPrefix(text, "node "), scriptName)
if !filepath.IsAbs(scriptPath) ||
!sameKiroIdeFileName(filepath.Base(scriptPath), scriptName) {
return nil, nil
}
agentDirectory := filepath.Dir(scriptPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) {
return nil, nil
}
agentID := filepath.Base(agentDirectory)
if agentID == "" || agentID == "." || agentID == ".." {
return nil, nil
}
return &kiroIdeRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil
} }
+17 -31
View File
@@ -45,7 +45,7 @@ type kiroIdeDocument struct {
type kiroIdeLegacyDocument struct { type kiroIdeLegacyDocument struct {
filePath string filePath string
snapshot *managedFileSnapshot snapshot *managedFileSnapshot
contract *kiroIdeRuntimeContract contract *managedRuntimeContract
} }
type kiroIdeRenderedDocument struct { type kiroIdeRenderedDocument struct {
@@ -55,12 +55,6 @@ type kiroIdeRenderedDocument struct {
remove bool remove bool
} }
type kiroIdeRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
type kiroIdeHookSpecification struct { type kiroIdeHookSpecification struct {
description string description string
trigger string trigger string
@@ -82,14 +76,6 @@ func kiroIdeSpecification(name any) (kiroIdeHookSpecification, bool) {
} }
} }
func cloneKiroIdeObject(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 validateKiroIdeAction(value any, label string) error { func validateKiroIdeAction(value any, label string) error {
action, ok := value.(map[string]any) action, ok := value.(map[string]any)
if !ok || action == nil { if !ok || action == nil {
@@ -212,7 +198,7 @@ func exactKiroIdeKeys(value map[string]any, keys ...string) bool {
func ownedKiroIdeReference( func ownedKiroIdeReference(
hook map[string]any, hook map[string]any,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
specification, managed := kiroIdeSpecification(hook["name"]) specification, managed := kiroIdeSpecification(hook["name"])
if !managed { if !managed {
return nil, nil return nil, nil
@@ -230,7 +216,7 @@ func ownedKiroIdeReference(
func managedKiroIdeReference( func managedKiroIdeReference(
hook map[string]any, hook map[string]any,
) (*kiroIdeRuntimeReference, error) { ) (*managedScriptReference, error) {
specification, managed := kiroIdeSpecification(hook["name"]) specification, managed := kiroIdeSpecification(hook["name"])
if !managed || if !managed ||
!exactKiroIdeKeys( !exactKiroIdeKeys(
@@ -278,7 +264,7 @@ func buildKiroIdeHook(
if !managed { if !managed {
return nil, fmt.Errorf("unsupported Elydora Kiro IDE hook name %q", name) return nil, fmt.Errorf("unsupported Elydora Kiro IDE hook name %q", name)
} }
command, err := buildKiroIdeCommand(runtimePath, scriptPath) command, err := buildEncodedCommand("Kiro IDE", runtimePath, scriptPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -298,7 +284,7 @@ func withoutManagedKiroIdeHooks(
for _, hook := range hooks { for _, hook := range hooks {
reference, err := ownedKiroIdeReference(hook) reference, err := ownedKiroIdeReference(hook)
remove := err == nil && reference != nil && remove := err == nil && reference != nil &&
(agentID == "" || sameKiroIdeAgentID(reference.agentID, agentID)) (agentID == "" || sameManagedAgentID(reference.agentID, agentID))
if !remove { if !remove {
filtered = append(filtered, hook) filtered = append(filtered, hook)
} }
@@ -339,7 +325,7 @@ func renderKiroIdeDocument(
document: document, changed: true, remove: true, document: document, changed: true, remove: true,
}, nil }, nil
} }
root := cloneKiroIdeObject(document.root) root := cloneJSONObject(document.root)
root["version"] = "v1" root["version"] = "v1"
values := make([]any, 0, len(hooks)) values := make([]any, 0, len(hooks))
for _, hook := range hooks { for _, hook := range hooks {
@@ -364,7 +350,7 @@ func renderKiroIdeDocument(
func kiroIdeRuntimeContracts( func kiroIdeRuntimeContracts(
hooks []map[string]any, hooks []map[string]any,
) ([]kiroIdeRuntimeContract, error) { ) ([]managedRuntimeContract, error) {
managedHooks := make([]map[string]any, 0, 2) managedHooks := make([]map[string]any, 0, 2)
for _, hook := range hooks { for _, hook := range hooks {
_, managed := kiroIdeSpecification(hook["name"]) _, managed := kiroIdeSpecification(hook["name"])
@@ -373,38 +359,38 @@ func kiroIdeRuntimeContracts(
} }
} }
if len(managedHooks) != 2 { if len(managedHooks) != 2 {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
nodePath, err := resolveNodeRuntime() nodePath, err := resolveNodeRuntime()
if err != nil { if err != nil {
return nil, err return nil, err
} }
var guard, audit *kiroIdeRuntimeReference var guard, audit *managedScriptReference
for _, hook := range managedHooks { for _, hook := range managedHooks {
reference, err := currentKiroIdeReference(hook, nodePath) reference, err := currentKiroIdeReference(hook, nodePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if reference == nil { if reference == nil {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
if hook["name"] == kiroIdeGuardName { if hook["name"] == kiroIdeGuardName {
if guard != nil { if guard != nil {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
guard = reference guard = reference
} else { } else {
if audit != nil { if audit != nil {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
audit = reference audit = reference
} }
} }
if guard == nil || audit == nil || if guard == nil || audit == nil ||
!sameKiroIdeAgentID(guard.agentID, audit.agentID) { !sameManagedAgentID(guard.agentID, audit.agentID) {
return []kiroIdeRuntimeContract{}, nil return []managedRuntimeContract{}, nil
} }
return []kiroIdeRuntimeContract{{ return []managedRuntimeContract{{
agentID: guard.agentID, guardPath: guard.scriptPath, agentID: guard.agentID, guardPath: guard.scriptPath,
auditPath: audit.scriptPath, auditPath: audit.scriptPath,
}}, nil }}, nil
@@ -449,10 +435,10 @@ func parseKiroIdeLegacyDocument(
return nil, err return nil, err
} }
if guardReference == nil || auditReference == nil || if guardReference == nil || auditReference == nil ||
!sameKiroIdeAgentID(guardReference.agentID, auditReference.agentID) { !sameManagedAgentID(guardReference.agentID, auditReference.agentID) {
return document, nil return document, nil
} }
document.contract = &kiroIdeRuntimeContract{ document.contract = &managedRuntimeContract{
agentID: guardReference.agentID, guardPath: guardReference.scriptPath, agentID: guardReference.agentID, guardPath: guardReference.scriptPath,
auditPath: auditReference.scriptPath, auditPath: auditReference.scriptPath,
} }
+15 -122
View File
@@ -1,20 +1,13 @@
package plugins package plugins
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
type kiroIdeRuntimePaths struct { type kiroIdeRuntimePaths struct {
runtimeRoot string managedRuntimePaths
agentDirectory string
configPath string
keyPath string
guardPath string
auditPath string
initialDirectories []kiroIdeDirectoryIdentity initialDirectories []kiroIdeDirectoryIdentity
} }
@@ -28,84 +21,17 @@ type kiroIdePreparedTransaction struct {
initialDirectories []kiroIdeDirectoryIdentity initialDirectories []kiroIdeDirectoryIdentity
} }
func validateKiroIdeInstallConfig(config InstallConfig) error {
for _, field := range []struct{ name, value string }{
{"agent name", config.AgentName},
{"organization ID", config.OrgID},
{"agent ID", config.AgentID},
{"key ID", config.KID},
{"private key", config.PrivateKey},
{"base URL", config.BaseURL},
{"guard script path", config.GuardScriptPath},
} {
if field.value == "" {
return fmt.Errorf("%s is required", field.name)
}
}
if config.AgentName != kiroIdeAgentKey {
return fmt.Errorf(
"Kiro IDE installation requires agent name %s",
kiroIdeAgentKey,
)
}
if strings.TrimSpace(config.OrgID) == "" {
return fmt.Errorf("organization ID is required")
}
if strings.TrimSpace(config.KID) == "" {
return fmt.Errorf("key ID is required")
}
if config.Token != "" && strings.TrimSpace(config.Token) == "" {
return fmt.Errorf("token must contain a non-whitespace value when provided")
}
if err := validateManagedPrivateKey(config.PrivateKey); err != nil {
return err
}
return validateManagedBaseURL(config.BaseURL)
}
func kiroIdeAgentPaths(config InstallConfig) (*kiroIdeRuntimePaths, error) {
if err := validateKiroIdeInstallConfig(config); err != nil {
return nil, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return nil, err
}
paths := &kiroIdeRuntimePaths{
runtimeRoot: runtimeRoot, agentDirectory: agentDirectory,
configPath: filepath.Join(agentDirectory, "config.json"),
keyPath: filepath.Join(agentDirectory, "private.key"),
guardPath: filepath.Join(agentDirectory, kiroIdeGuardScript),
auditPath: filepath.Join(agentDirectory, kiroIdeAuditScript),
}
if !filepath.IsAbs(config.GuardScriptPath) ||
!sameKiroIdePath(config.GuardScriptPath, paths.guardPath) {
return nil, fmt.Errorf(
"guard runtime must use the managed Elydora agent directory: %s",
paths.guardPath,
)
}
if config.HookScript != "" && (!filepath.IsAbs(config.HookScript) ||
!sameKiroIdePath(config.HookScript, paths.auditPath)) {
return nil, fmt.Errorf(
"audit runtime must use the managed Elydora agent directory: %s",
paths.auditPath,
)
}
return paths, nil
}
func preflightKiroIdeInstallation( func preflightKiroIdeInstallation(
config InstallConfig, config InstallConfig,
) (*kiroIdeRuntimePaths, string, error) { ) (*kiroIdeRuntimePaths, string, error) {
paths, err := kiroIdeAgentPaths(config) if err := validateManagedInstallConfig(config, kiroIdeAgentKey, "Kiro IDE"); err != nil {
return nil, "", err
}
managed, err := resolveManagedRuntimePaths(config, kiroIdeGuardScript, kiroIdeAuditScript)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
paths := &kiroIdeRuntimePaths{managedRuntimePaths: *managed}
initialDirectories := make([]kiroIdeDirectoryIdentity, 0, 3) initialDirectories := make([]kiroIdeDirectoryIdentity, 0, 3)
for _, item := range []struct{ path, label string }{ for _, item := range []struct{ path, label string }{
{filepath.Dir(paths.runtimeRoot), "home directory"}, {filepath.Dir(paths.runtimeRoot), "home directory"},
@@ -124,9 +50,8 @@ func preflightKiroIdeInstallation(
initialDirectories[0].path, initialDirectories[0].path,
) )
} }
if err := validateKiroIdeRuntimeIdentity( if err := validateManagedRuntimeIdentity(
paths.agentDirectory, paths.agentDirectory, config.AgentID, kiroIdeAgentKey, "Kiro IDE",
config.AgentID,
); err != nil { ); err != nil {
return nil, "", err return nil, "", err
} }
@@ -144,29 +69,6 @@ func preflightKiroIdeInstallation(
return paths, nodePath, nil return paths, nodePath, nil
} }
func buildKiroIdeRuntimeConfig(config InstallConfig) ([]byte, error) {
value := map[string]any{
"org_id": config.OrgID, "agent_id": config.AgentID,
"kid": config.KID, "base_url": config.BaseURL,
"agent_name": kiroIdeAgentKey,
}
if config.Token != "" {
value["token"] = config.Token
}
encoded, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, fmt.Errorf("encode Elydora runtime config: %w", err)
}
encoded = append(encoded, '\n')
if len(encoded) > maxRuntimeConfigBytes {
return nil, fmt.Errorf(
"Elydora runtime config exceeds %d bytes after JSON encoding",
maxRuntimeConfigBytes,
)
}
return encoded, nil
}
func prepareKiroIdeFile( func prepareKiroIdeFile(
filePath string, filePath string,
label string, label string,
@@ -221,10 +123,11 @@ func prepareKiroIdeInstallation(
if sources == nil || paths == nil || rendered == nil { if sources == nil || paths == nil || rendered == nil {
return nil, fmt.Errorf("Kiro IDE installation requires prepared sources") return nil, fmt.Errorf("Kiro IDE installation requires prepared sources")
} }
runtimeConfig, err := buildKiroIdeRuntimeConfig(config) runtimeConfig, err := buildManagedRuntimeConfig(config, kiroIdeAgentKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
guardScript, auditScript := kiroIdeExpectedScripts(config.AgentID, nil)
prepared := &kiroIdePreparedTransaction{ prepared := &kiroIdePreparedTransaction{
operation: "installation", paths: sources.paths, runtimePaths: paths, operation: "installation", paths: sources.paths, runtimePaths: paths,
initialDirectories: append( initialDirectories: append(
@@ -238,20 +141,10 @@ func prepareKiroIdeInstallation(
mode os.FileMode mode os.FileMode
maximum int64 maximum int64
}{ }{
{ {paths.guardPath, "Elydora guard runtime", guardScript, 0700, maxManagedSourceBytes},
paths.guardPath, "Elydora guard runtime",
[]byte(generateGuardScript(kiroIdeAgentKey, config.AgentID, "", false, "")),
0700, maxManagedSourceBytes,
},
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600, maxRuntimeConfigBytes}, {paths.configPath, "Elydora runtime config", runtimeConfig, 0600, maxRuntimeConfigBytes},
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600, maxProtectedSecretBytes}, {paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600, maxProtectedSecretBytes},
{ {paths.auditPath, "Elydora audit runtime", auditScript, 0700, maxManagedSourceBytes},
paths.auditPath, "Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
kiroIdeAgentKey, config.AgentID, "", false, true,
)),
0700, maxManagedSourceBytes,
},
} { } {
change, condition, err := prepareKiroIdeFile( change, condition, err := prepareKiroIdeFile(
item.path, item.label, item.contents, item.mode, item.maximum, item.path, item.label, item.contents, item.mode, item.maximum,
@@ -274,7 +167,7 @@ func prepareKiroIdeInstallation(
prepared.changes = append(prepared.changes, documentChange) prepared.changes = append(prepared.changes, documentChange)
} }
removeLegacy := sources.legacy.contract != nil && removeLegacy := sources.legacy.contract != nil &&
sameKiroIdeAgentID(sources.legacy.contract.agentID, config.AgentID) sameManagedAgentID(sources.legacy.contract.agentID, config.AgentID)
if removeLegacy { if removeLegacy {
legacyChange, err := prepareSnapshotSourceChange( legacyChange, err := prepareSnapshotSourceChange(
sources.legacy.filePath, sources.legacy.filePath,
@@ -335,7 +228,7 @@ func prepareKiroIdeUninstall(
prepared.changes = append(prepared.changes, documentChange) prepared.changes = append(prepared.changes, documentChange)
} }
removeLegacy := sources.legacy.contract != nil && removeLegacy := sources.legacy.contract != nil &&
(agentID == "" || sameKiroIdeAgentID(sources.legacy.contract.agentID, agentID)) (agentID == "" || sameManagedAgentID(sources.legacy.contract.agentID, agentID))
if removeLegacy { if removeLegacy {
change, err := prepareSnapshotSourceChange( change, err := prepareSnapshotSourceChange(
sources.legacy.filePath, sources.legacy.filePath,
@@ -383,7 +276,7 @@ func commitKiroIdeTransaction(
} }
workspaceChange := false workspaceChange := false
for _, change := range prepared.changes { for _, change := range prepared.changes {
if change != nil && sameKiroIdePath(change.filePath, prepared.paths.configPath) { if change != nil && sameManagedPath(change.filePath, prepared.paths.configPath) {
workspaceChange = true workspaceChange = true
break break
} }
+33 -249
View File
@@ -1,157 +1,13 @@
package plugins package plugins
import ( import (
"bytes"
"fmt"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sort"
"strings"
) )
func readKiroIdeRuntimeConfig(path string) (map[string]any, bool, error) { func kiroIdeExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes) return []byte(generateGuardScript(kiroIdeAgentKey, agentID, "", false, "")),
if err != nil || snapshot == nil { []byte(buildHookScriptWithOutput(kiroIdeAgentKey, agentID, "", false, true))
return nil, snapshot != nil, err
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
config, err := decodeStrictJSONObject(snapshot.contents, label)
return config, true, err
}
func requireKiroIdeRuntimeString(
config map[string]any,
field string,
configPath string,
) (string, error) {
value, ok := config[field].(string)
if !ok || strings.TrimSpace(value) == "" {
return "", fmt.Errorf(
"Elydora runtime config %s is invalid: %s",
field,
configPath,
)
}
return value, nil
}
func validateKiroIdeRuntimeConfig(
config map[string]any,
expectedAgentID string,
configPath string,
) error {
supported := stringSet(
"org_id", "agent_id", "kid", "base_url", "token", "agent_name",
)
fields := make([]string, 0, len(config))
for field := range config {
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
if _, ok := supported[field]; !ok {
return fmt.Errorf(
`Elydora runtime config has unsupported field %q: %s`,
field,
configPath,
)
}
}
if _, err := requireKiroIdeRuntimeString(config, "org_id", configPath); err != nil {
return err
}
if _, err := requireKiroIdeRuntimeString(config, "kid", configPath); err != nil {
return err
}
agentID, err := requireKiroIdeRuntimeString(config, "agent_id", configPath)
if err != nil {
return err
}
if !sameKiroIdeAgentID(agentID, expectedAgentID) ||
config["agent_name"] != kiroIdeAgentKey {
return fmt.Errorf(
"Elydora runtime identity does not match Kiro IDE hooks: %s",
configPath,
)
}
if _, exists := config["token"]; exists {
if _, err := requireKiroIdeRuntimeString(config, "token", configPath); err != nil {
return err
}
}
baseURL, err := requireKiroIdeRuntimeString(config, "base_url", configPath)
if err != nil {
return err
}
if err := validateManagedBaseURL(baseURL); err != nil {
return fmt.Errorf(
"Elydora runtime config base URL is invalid at %s: %w",
configPath,
err,
)
}
return nil
}
func validateKiroIdeRuntimeIdentity(agentDirectory, agentID string) error {
runtimeRoot := filepath.Dir(agentDirectory)
rootExists, err := managedPhysicalDirectoryExists(
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return err
}
configPath := filepath.Join(agentDirectory, "config.json")
config, configExists, err := readKiroIdeRuntimeConfig(configPath)
if err != nil {
return err
}
artifactExists := false
for _, item := range []struct {
path string
label string
limit int64
}{
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes},
{filepath.Join(agentDirectory, kiroIdeGuardScript), "Elydora guard runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, kiroIdeAuditScript), "Elydora audit runtime", maxManagedSourceBytes},
{filepath.Join(agentDirectory, "chain-state.json"), "Elydora chain state", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "status-cache.json"), "Elydora status cache", maxRuntimeConfigBytes},
{filepath.Join(agentDirectory, "error.log"), "Elydora error log", maxManagedSourceBytes},
} {
exists, inspectErr := managedPhysicalFileExists(item.path, item.label, item.limit)
if inspectErr != nil {
return inspectErr
}
artifactExists = artifactExists || exists
}
if !configExists {
if artifactExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != kiroIdeAgentKey ||
!sameKiroIdeAgentID(configuredID, agentID) {
return fmt.Errorf(
"Elydora runtime config identity does not match Kiro IDE agent %s: %s",
agentID,
configPath,
)
}
return nil
} }
func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool { func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool {
@@ -161,116 +17,44 @@ func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool
return uint32(snapshot.mode.Perm()) == expected return uint32(snapshot.mode.Perm()) == expected
} }
func kiroIdeRuntimeContractExists(contract kiroIdeRuntimeContract) (bool, error) { // kiroIdeRuntimeModesMatch requires the private modes the installer writes.
runtimeRoot, err := AgentRuntimeRoot() func kiroIdeRuntimeModesMatch(contract managedRuntimeContract) (bool, error) {
if err != nil {
return false, err
}
agentDirectory := filepath.Dir(contract.guardPath) agentDirectory := filepath.Dir(contract.guardPath)
if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) || for _, item := range []struct {
!sameKiroIdeAgentID(filepath.Base(agentDirectory), contract.agentID) || path, label string
!sameKiroIdePath( limit int64
contract.guardPath, mode uint32
filepath.Join(agentDirectory, kiroIdeGuardScript), }{
) || {filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", maxRuntimeConfigBytes, 0600},
!sameKiroIdePath( {filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes, 0600},
contract.auditPath, {contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes, 0700},
filepath.Join(agentDirectory, kiroIdeAuditScript), {contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes, 0700},
) { } {
return false, nil snapshot, err := readManagedFile(item.path, item.label, item.limit)
if err != nil || snapshot == nil {
return false, err
}
if !kiroIdeRuntimeFileMode(snapshot, item.mode) {
return false, nil
}
} }
rootExists, err := managedPhysicalDirectoryExists( return true, nil
runtimeRoot,
"Elydora runtime directory",
)
if err != nil || !rootExists {
return false, err
}
directoryExists, err := managedPhysicalDirectoryExists(
agentDirectory,
"Elydora agent runtime directory",
)
if err != nil || !directoryExists {
return false, err
}
configPath := filepath.Join(agentDirectory, "config.json")
keyPath := filepath.Join(agentDirectory, "private.key")
configSnapshot, err := readManagedFile(
configPath,
"Elydora runtime config",
maxRuntimeConfigBytes,
)
if err != nil {
return false, err
}
key, err := readManagedFile(keyPath, "Elydora private key", maxProtectedSecretBytes)
if err != nil {
return false, err
}
guard, err := readManagedFile(
contract.guardPath,
"Elydora guard runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
audit, err := readManagedFile(
contract.auditPath,
"Elydora audit runtime",
maxManagedSourceBytes,
)
if err != nil {
return false, err
}
if configSnapshot == nil || key == nil || guard == nil || audit == nil {
return false, nil
}
config, err := decodeStrictJSONObject(
configSnapshot.contents,
fmt.Sprintf("Elydora runtime config at %s", configPath),
)
if err != nil {
return false, err
}
if err := validateKiroIdeRuntimeConfig(config, contract.agentID, configPath); err != nil {
return false, err
}
if err := validateManagedPrivateKey(string(key.contents)); err != nil {
return false, fmt.Errorf("Elydora private key at %s: %w", keyPath, err)
}
if !kiroIdeRuntimeFileMode(configSnapshot, 0600) ||
!kiroIdeRuntimeFileMode(key, 0600) ||
!kiroIdeRuntimeFileMode(guard, 0700) ||
!kiroIdeRuntimeFileMode(audit, 0700) {
return false, nil
}
expectedGuard := []byte(generateGuardScript(
kiroIdeAgentKey,
contract.agentID,
"",
false,
"",
))
expectedAudit := []byte(buildHookScriptWithOutput(
kiroIdeAgentKey,
contract.agentID,
"",
false,
true,
))
return bytes.Equal(guard.contents, expectedGuard) &&
bytes.Equal(audit.contents, expectedAudit), nil
} }
func kiroIdeRuntimeFilesExist(contracts []kiroIdeRuntimeContract) (bool, error) { func kiroIdeRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
for _, contract := range contracts { for _, contract := range contracts {
exists, err := kiroIdeRuntimeContractExists(contract) exists, err := managedRuntimeContractExists(
contract, kiroIdeAgentKey, "Kiro IDE", kiroIdeExpectedScripts,
)
if err != nil { if err != nil {
return false, err return false, err
} }
if exists { if !exists {
return true, nil continue
}
modes, err := kiroIdeRuntimeModesMatch(contract)
if err != nil || modes {
return modes, err
} }
} }
return false, nil return false, nil
+2 -2
View File
@@ -164,7 +164,7 @@ func TestKiroIdeStatusRequiresUniqueExactPairAndRuntimeIntegrity(t *testing.T) {
document = readKiroIdeObject(t, fixture.configPath) document = readKiroIdeObject(t, fixture.configPath)
document["hooks"] = append( document["hooks"] = append(
requireArray(t, document["hooks"]), requireArray(t, document["hooks"]),
cloneKiroIdeObject(findKiroIdeHook(t, document, kiroIdeGuardName)), cloneJSONObject(findKiroIdeHook(t, document, kiroIdeGuardName)),
) )
writeKiroIdeJSON(t, fixture.configPath, document) writeKiroIdeJSON(t, fixture.configPath, document)
assertStatus(false, false) assertStatus(false, false)
@@ -283,7 +283,7 @@ func TestKiroIdeStatusRejectsExtraOrphanAndStaleRuntimeCommands(t *testing.T) {
name = "node.exe" name = "node.exe"
} }
staleNode := filepath.Join(fixture.homeDir, "old-node", name) staleNode := filepath.Join(fixture.homeDir, "old-node", name)
command, err := buildKiroIdeCommand(staleNode, fixture.guardPath) command, err := buildEncodedCommand("Kiro IDE", staleNode, fixture.guardPath)
if err != nil { if err != nil {
t.Fatalf("build stale Kiro IDE command: %v", err) t.Fatalf("build stale Kiro IDE command: %v", err)
} }
@@ -20,7 +20,7 @@ func TestKiroIdeRollbackRestoresRuntimeWorkspaceAndLegacySources(t *testing.T) {
legacyKiroIdeGoDocument(fixture, kiroIdeTestAgentID), legacyKiroIdeGoDocument(fixture, kiroIdeTestAgentID),
) )
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if sameKiroIdePath(source, fixture.legacyPath) && if sameManagedPath(source, fixture.legacyPath) &&
strings.HasSuffix(destination, ".rollback") { strings.HasSuffix(destination, ".rollback") {
return errors.New("injected Kiro IDE legacy cleanup failure") return errors.New("injected Kiro IDE legacy cleanup failure")
} }
@@ -320,7 +320,7 @@ func TestKiroIdeInstallSurvivesAdversarialWorkspaceLinkSwap(t *testing.T) {
} }
swapped := false swapped := false
fixture.plugin.rename = func(source, destination string) error { fixture.plugin.rename = func(source, destination string) error {
if !swapped && sameKiroIdePath(destination, fixture.configPath) && if !swapped && sameManagedPath(destination, fixture.configPath) &&
strings.HasSuffix(source, ".tmp") { strings.HasSuffix(source, ".tmp") {
swapped = true swapped = true
backup := kiroDirectory + ".original" backup := kiroDirectory + ".original"
+2 -9
View File
@@ -1,7 +1,6 @@
package plugins package plugins
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -99,14 +98,8 @@ func (p *LettaPlugin) Uninstall(agentID string) error {
return err return err
} }
if agentID != "" { if agentID != "" {
for _, resolve := range []func(string) (string, error){hookScriptPath, guardScriptPath} { if err := removeAgentScripts(agentID); err != nil {
path, err := resolve(agentID) return err
if err != nil {
return err
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove %s: %w", path, err)
}
} }
} }
fmt.Println("Uninstalled Elydora hook for Letta Code.") fmt.Println("Uninstalled Elydora hook for Letta Code.")
+57 -45
View File
@@ -1,14 +1,17 @@
package plugins package plugins
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "regexp"
"strconv"
) )
// OpenCodePlugin manages the Elydora audit hook for OpenCode. var openCodeScriptPathPattern = regexp.MustCompile(`(?m)^const (GUARD_SCRIPT_PATH|HOOK_SCRIPT_PATH) = (".*");$`)
// It writes .config/opencode/plugins/elydora-audit.mjs in the home directory.
// OpenCodePlugin writes ~/.config/opencode/plugins/elydora-audit.mjs.
type OpenCodePlugin struct{} type OpenCodePlugin struct{}
func (p *OpenCodePlugin) configPath() (string, error) { func (p *OpenCodePlugin) configPath() (string, error) {
@@ -59,8 +62,7 @@ func (p *OpenCodePlugin) Install(config InstallConfig) error {
} }
func buildOpenCodePlugin(scriptPath, guardPath string) string { func buildOpenCodePlugin(scriptPath, guardPath string) string {
return fmt.Sprintf(`// Elydora Audit Plugin for OpenCode return fmt.Sprintf(`// Elydora audit plugin for OpenCode. Generated by the Elydora CLI; do not edit.
// Generated by @elydora/sdk — DO NOT EDIT
import { spawn, spawnSync } from 'node:child_process'; import { spawn, spawnSync } from 'node:child_process';
@@ -112,62 +114,72 @@ func (p *OpenCodePlugin) Uninstall(agentID string) error {
} }
if agentID != "" { if agentID != "" {
scriptPath, _ := hookScriptPath(agentID) if err := removeAgentScripts(agentID); err != nil {
if scriptPath != "" { return err
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
} }
} }
fmt.Println("Uninstalled Elydora hook for OpenCode.") fmt.Println("Uninstalled Elydora hook for OpenCode.")
return nil return nil
} }
func openCodeScriptPaths(data []byte, pluginFile string) (string, string, error) {
paths := map[string]string{}
for _, match := range openCodeScriptPathPattern.FindAllSubmatch(data, -1) {
value, err := strconv.Unquote(string(match[2]))
if err != nil {
return "", "", fmt.Errorf(
"OpenCode plugin at %s has an invalid %s: %w", pluginFile, match[1], err,
)
}
paths[string(match[1])] = value
}
guardPath, hasGuard := paths["GUARD_SCRIPT_PATH"]
hookPath, hasHook := paths["HOOK_SCRIPT_PATH"]
if !hasGuard || !hasHook {
return "", "", fmt.Errorf("OpenCode plugin at %s has no Elydora script paths", pluginFile)
}
return guardPath, hookPath, nil
}
func (p *OpenCodePlugin) Status() (PluginStatus, error) { func (p *OpenCodePlugin) Status() (PluginStatus, error) {
pluginFile, err := p.configPath() pluginFile, err := p.configPath()
if err != nil { if err != nil {
return PluginStatus{}, err return PluginStatus{}, err
} }
status := PluginStatus{ status := PluginStatus{
AgentName: "opencode", AgentName: "opencode",
DisplayName: "OpenCode", DisplayName: "OpenCode",
ConfigPath: pluginFile, ConfigPath: pluginFile,
} }
data, err := os.ReadFile(pluginFile)
if _, err := os.Stat(pluginFile); err == nil { if errors.Is(err, os.ErrNotExist) {
status.HookConfigured = true return status, nil
// Read the plugin file to extract the hook script path
data, readErr := os.ReadFile(pluginFile)
if readErr == nil {
content := string(data)
// Look for the elydora hook.js path in the plugin file content
if idx := strings.Index(content, ".elydora"); idx >= 0 {
// Extract path from the spawn('node', ['/path/to/hook.js']) pattern
// Pragmatic: scan ~/.elydora/*/hook.js for existence
home, homeErr := os.UserHomeDir()
if homeErr == nil {
entries, dirErr := os.ReadDir(filepath.Join(home, ".elydora"))
if dirErr == nil {
for _, entry := range entries {
if entry.IsDir() {
hookPath := filepath.Join(home, ".elydora", entry.Name(), "hook.js")
if _, statErr := os.Stat(hookPath); statErr == nil {
status.HookScriptExists = true
break
}
}
}
}
}
}
}
} }
if err != nil {
status.Installed = status.HookConfigured && status.HookScriptExists return status, fmt.Errorf("read %s: %w", pluginFile, err)
}
status.HookConfigured = true
guardPath, hookPath, err := openCodeScriptPaths(data, pluginFile)
if err != nil {
return status, err
}
guard, err := resolveManagedScript(guardPath, "guard.js")
if err != nil {
return status, err
}
audit, err := resolveManagedScript(hookPath, "hook.js")
if err != nil {
return status, err
}
if guard == nil || audit == nil || !sameManagedAgentID(guard.agentID, audit.agentID) {
return status, nil
}
status.HookScriptExists, err = managedRuntimePresent(managedRuntimeContract{
agentID: guard.agentID, guardPath: guard.scriptPath, auditPath: audit.scriptPath,
}, "opencode")
if err != nil {
return status, err
}
status.Installed = status.HookScriptExists
return status, nil return status, nil
} }
+4 -29
View File
@@ -1,7 +1,6 @@
package plugins package plugins
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -13,15 +12,6 @@ const (
maxRuntimeConfigBytes = 512 * 1024 maxRuntimeConfigBytes = 512 * 1024
) )
type agentRuntimeConfig struct {
OrgID string `json:"org_id"`
AgentID string `json:"agent_id"`
KID string `json:"kid"`
BaseURL string `json:"base_url"`
Token string `json:"token"`
AgentName string `json:"agent_name"`
}
// WriteRuntimeFileAtomic replaces one managed runtime file atomically. // WriteRuntimeFileAtomic replaces one managed runtime file atomically.
func WriteRuntimeFileAtomic( func WriteRuntimeFileAtomic(
path string, path string,
@@ -54,27 +44,12 @@ func generateHookScriptWithRename(
return err return err
} }
baseURL := config.BaseURL if config.BaseURL == "" {
if baseURL == "" { return fmt.Errorf("base URL is required")
baseURL = "https://api.elydora.com"
} }
encodedConfig, err := json.MarshalIndent(agentRuntimeConfig{ encodedConfig, err := buildManagedRuntimeConfig(config, config.AgentName)
OrgID: config.OrgID,
AgentID: config.AgentID,
KID: config.KID,
BaseURL: baseURL,
Token: config.Token,
AgentName: config.AgentName,
}, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("encode Elydora runtime config: %w", err) return err
}
encodedConfig = append(encodedConfig, '\n')
if len(encodedConfig) > maxRuntimeConfigBytes {
return fmt.Errorf(
"Elydora runtime config exceeds %d bytes after JSON encoding",
maxRuntimeConfigBytes,
)
} }
items := []struct { items := []struct {