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:
1 parent
34ff4c1ea8
commit
96489cd051
20 files changed
+185
-650
No files matched your search
@@ -67,14 +67,6 @@ type augmentRuntimeContract struct {
|
||||
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,
|
||||
@@ -113,7 +105,7 @@ func validateAugmentHandler(
|
||||
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 {
|
||||
@@ -168,7 +160,7 @@ func validateAugmentGroup(value any, event string, groupIndex int) (augmentGroup
|
||||
}
|
||||
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) {
|
||||
@@ -247,7 +239,7 @@ func renderAugmentDocument(
|
||||
if !document.exists && len(hooks) == 0 {
|
||||
return &augmentRenderedDocument{document: document}, nil
|
||||
}
|
||||
root := cloneAugmentObject(document.root)
|
||||
root := cloneJSONObject(document.root)
|
||||
if len(hooks) == 0 {
|
||||
delete(root, "hooks")
|
||||
} else {
|
||||
@@ -280,7 +272,7 @@ func renderAugmentHooks(hooks augmentHooks) map[string]any {
|
||||
for event, groups := range hooks {
|
||||
values := make([]any, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
object := cloneAugmentObject(group.object)
|
||||
object := cloneJSONObject(group.object)
|
||||
handlers := make([]any, 0, len(group.handlers))
|
||||
for _, handler := range group.handlers {
|
||||
handlers = append(handlers, handler)
|
||||
|
||||
@@ -79,14 +79,6 @@ type claudeRenderedDocument struct {
|
||||
|
||||
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 {
|
||||
clone := make(claudeHooks, len(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) {
|
||||
@@ -328,7 +320,7 @@ func validateClaudeGroup(value any, event string, groupIndex int) (claudeGroup,
|
||||
}
|
||||
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) {
|
||||
@@ -407,7 +399,7 @@ func renderClaudeHooks(hooks claudeHooks) map[string]any {
|
||||
for event, groups := range hooks {
|
||||
values := make([]any, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
object := cloneClaudeObject(group.object)
|
||||
object := cloneJSONObject(group.object)
|
||||
handlers := make([]any, 0, len(group.handlers))
|
||||
for _, handler := range group.handlers {
|
||||
handlers = append(handlers, handler)
|
||||
@@ -435,7 +427,7 @@ func renderClaudeDocument(
|
||||
document: document, changed: true, remove: true,
|
||||
}, nil
|
||||
}
|
||||
root := cloneClaudeObject(document.root)
|
||||
root := cloneJSONObject(document.root)
|
||||
if len(hooks) == 0 {
|
||||
delete(root, "hooks")
|
||||
} else {
|
||||
|
||||
@@ -99,7 +99,7 @@ func removeManagedClaudeGroups(
|
||||
kept = append(kept, handler)
|
||||
}
|
||||
if len(kept) > 0 || !exactManagedClaudeGroup(group) {
|
||||
object := cloneClaudeObject(group.object)
|
||||
object := cloneJSONObject(group.object)
|
||||
object["hooks"] = group.object["hooks"]
|
||||
result = append(result, claudeGroup{object: object, handlers: kept})
|
||||
}
|
||||
|
||||
@@ -39,14 +39,6 @@ type codexRenderedDocument struct {
|
||||
|
||||
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 {
|
||||
clone := make(codexHooks, len(source))
|
||||
for event, groups := range source {
|
||||
@@ -147,7 +139,7 @@ func removeCodexGroups(
|
||||
kept = append(kept, handler)
|
||||
}
|
||||
if len(kept) > 0 || !exactCodexMatcherGroup(group) {
|
||||
next := cloneCodexObject(group)
|
||||
next := cloneJSONObject(group)
|
||||
next["hooks"] = kept
|
||||
result = append(result, next)
|
||||
}
|
||||
@@ -223,7 +215,7 @@ func renderCodexDocument(
|
||||
if len(hooks) == 0 && entirelyManagedCodexDocument(document) {
|
||||
return &codexRenderedDocument{document: document, changed: true, remove: true}, nil
|
||||
}
|
||||
root := cloneCodexObject(document.root)
|
||||
root := cloneJSONObject(document.root)
|
||||
if len(hooks) == 0 {
|
||||
delete(root, "hooks")
|
||||
} else {
|
||||
|
||||
@@ -74,14 +74,6 @@ func copilotManagedEntryForHandler(
|
||||
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 {
|
||||
clone := make(copilotHooks, len(source))
|
||||
for event, handlers := range source {
|
||||
@@ -240,7 +232,7 @@ func renderCopilotDocument(
|
||||
document: document, changed: true, remove: true,
|
||||
}, nil
|
||||
}
|
||||
root := cloneCopilotObject(document.root)
|
||||
root := cloneJSONObject(document.root)
|
||||
root["version"] = float64(1)
|
||||
if len(hooks) == 0 {
|
||||
delete(root, "hooks")
|
||||
|
||||
@@ -274,7 +274,7 @@ func validateCopilotHooks(value any, label string) (copilotHooks, error) {
|
||||
if err := validateCopilotHandler(handler, event, itemLabel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handlers = append(handlers, cloneCopilotObject(handler))
|
||||
handlers = append(handlers, cloneJSONObject(handler))
|
||||
}
|
||||
hooks[event] = handlers
|
||||
}
|
||||
|
||||
@@ -46,14 +46,6 @@ type grokRenderedDocument struct {
|
||||
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 {
|
||||
clone := make(grokHooks, len(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) {
|
||||
@@ -158,7 +150,7 @@ func validateGrokGroup(value any, event string, groupIndex int) (grokGroup, erro
|
||||
}
|
||||
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) {
|
||||
@@ -221,7 +213,7 @@ func renderGrokHooks(hooks grokHooks) map[string]any {
|
||||
for event, groups := range hooks {
|
||||
values := make([]any, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
object := cloneGrokObject(group.object)
|
||||
object := cloneJSONObject(group.object)
|
||||
handlers := make([]any, 0, len(group.handlers))
|
||||
for _, handler := range group.handlers {
|
||||
handlers = append(handlers, handler)
|
||||
@@ -378,7 +370,7 @@ func renderGrokDocument(
|
||||
document: document, changed: true, remove: true,
|
||||
}, nil
|
||||
}
|
||||
root := cloneGrokObject(document.root)
|
||||
root := cloneJSONObject(document.root)
|
||||
if len(hooks) == 0 {
|
||||
delete(root, "hooks")
|
||||
} else {
|
||||
|
||||
@@ -293,3 +293,11 @@ func jsonPointer(path []any) 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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -49,6 +50,20 @@ func hookScriptPath(agentId string) (string, error) {
|
||||
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.
|
||||
func guardScriptPath(agentId string) (string, error) {
|
||||
agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
|
||||
|
||||
@@ -79,7 +79,7 @@ func (p *KiroCliPlugin) Install(config InstallConfig) error {
|
||||
copyKiroObject(nextV2, v2Settings)
|
||||
nextV2["hooks"] = v2Hooks
|
||||
|
||||
nextV3 := cloneKiroObject(v3Settings)
|
||||
nextV3 := cloneJSONObject(v3Settings)
|
||||
nextV3["version"] = "v1"
|
||||
v3Hooks := make([]any, 0, len(currentV3Hooks)+2)
|
||||
for _, hook := range currentV3Hooks {
|
||||
|
||||
@@ -28,12 +28,6 @@ type kiroConfigMutation struct {
|
||||
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) {
|
||||
for key, value := range source {
|
||||
destination[key] = value
|
||||
@@ -49,7 +43,7 @@ func kiroHooksObject(settings map[string]any, label string) (map[string]any, err
|
||||
if !ok || hooks == nil {
|
||||
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) {
|
||||
@@ -193,7 +187,7 @@ func prepareKiroV2Uninstall(
|
||||
}
|
||||
hooks["preToolUse"] = filteredPre
|
||||
hooks["postToolUse"] = filteredPost
|
||||
next := cloneKiroObject(settings)
|
||||
next := cloneJSONObject(settings)
|
||||
next["hooks"] = hooks
|
||||
mutation.value = next
|
||||
mutation.changed = true
|
||||
@@ -250,7 +244,7 @@ func prepareKiroV3Uninstall(
|
||||
if len(filtered) == len(hooks) {
|
||||
return mutation, nil
|
||||
}
|
||||
next := cloneKiroObject(settings)
|
||||
next := cloneJSONObject(settings)
|
||||
next["hooks"] = filtered
|
||||
owned := true
|
||||
for key := range settings {
|
||||
|
||||
@@ -1,101 +1,32 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"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(
|
||||
command string,
|
||||
scriptName string,
|
||||
) (*kiroIdeRuntimeReference, error) {
|
||||
runtimePath, scriptPath, ok := parseKiroIdeCommand(command)
|
||||
if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) ||
|
||||
!isNodeExecutable(runtimePath) ||
|
||||
!sameKiroIdeFileName(filepath.Base(scriptPath), scriptName) {
|
||||
) (*managedScriptReference, error) {
|
||||
runtimePath, scriptPath, ok := parseEncodedCommand(command)
|
||||
if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) {
|
||||
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
|
||||
return resolveManagedScript(scriptPath, scriptName)
|
||||
}
|
||||
|
||||
func currentKiroIdeReference(
|
||||
hook map[string]any,
|
||||
nodePath string,
|
||||
) (*kiroIdeRuntimeReference, error) {
|
||||
) (*managedScriptReference, error) {
|
||||
reference, err := managedKiroIdeReference(hook)
|
||||
if err != nil || reference == nil {
|
||||
return reference, err
|
||||
}
|
||||
action := hook["action"].(map[string]any)
|
||||
command := action["command"].(string)
|
||||
expected, err := buildKiroIdeCommand(nodePath, reference.scriptPath)
|
||||
expected, err := buildEncodedCommand("Kiro IDE", nodePath, reference.scriptPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -105,30 +36,14 @@ func currentKiroIdeReference(
|
||||
return reference, nil
|
||||
}
|
||||
|
||||
// legacyKiroIdeGoReference reads the pre-2.1 `node <script>` form.
|
||||
func legacyKiroIdeGoReference(
|
||||
command any,
|
||||
scriptName string,
|
||||
) (*kiroIdeRuntimeReference, error) {
|
||||
) (*managedScriptReference, error) {
|
||||
text, ok := command.(string)
|
||||
if !ok || !strings.HasPrefix(text, "node ") {
|
||||
return nil, nil
|
||||
}
|
||||
scriptPath := strings.TrimPrefix(text, "node ")
|
||||
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
|
||||
return resolveManagedScript(strings.TrimPrefix(text, "node "), scriptName)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ type kiroIdeDocument struct {
|
||||
type kiroIdeLegacyDocument struct {
|
||||
filePath string
|
||||
snapshot *managedFileSnapshot
|
||||
contract *kiroIdeRuntimeContract
|
||||
contract *managedRuntimeContract
|
||||
}
|
||||
|
||||
type kiroIdeRenderedDocument struct {
|
||||
@@ -55,12 +55,6 @@ type kiroIdeRenderedDocument struct {
|
||||
remove bool
|
||||
}
|
||||
|
||||
type kiroIdeRuntimeContract struct {
|
||||
agentID string
|
||||
guardPath string
|
||||
auditPath string
|
||||
}
|
||||
|
||||
type kiroIdeHookSpecification struct {
|
||||
description 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 {
|
||||
action, ok := value.(map[string]any)
|
||||
if !ok || action == nil {
|
||||
@@ -212,7 +198,7 @@ func exactKiroIdeKeys(value map[string]any, keys ...string) bool {
|
||||
|
||||
func ownedKiroIdeReference(
|
||||
hook map[string]any,
|
||||
) (*kiroIdeRuntimeReference, error) {
|
||||
) (*managedScriptReference, error) {
|
||||
specification, managed := kiroIdeSpecification(hook["name"])
|
||||
if !managed {
|
||||
return nil, nil
|
||||
@@ -230,7 +216,7 @@ func ownedKiroIdeReference(
|
||||
|
||||
func managedKiroIdeReference(
|
||||
hook map[string]any,
|
||||
) (*kiroIdeRuntimeReference, error) {
|
||||
) (*managedScriptReference, error) {
|
||||
specification, managed := kiroIdeSpecification(hook["name"])
|
||||
if !managed ||
|
||||
!exactKiroIdeKeys(
|
||||
@@ -278,7 +264,7 @@ func buildKiroIdeHook(
|
||||
if !managed {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -298,7 +284,7 @@ func withoutManagedKiroIdeHooks(
|
||||
for _, hook := range hooks {
|
||||
reference, err := ownedKiroIdeReference(hook)
|
||||
remove := err == nil && reference != nil &&
|
||||
(agentID == "" || sameKiroIdeAgentID(reference.agentID, agentID))
|
||||
(agentID == "" || sameManagedAgentID(reference.agentID, agentID))
|
||||
if !remove {
|
||||
filtered = append(filtered, hook)
|
||||
}
|
||||
@@ -339,7 +325,7 @@ func renderKiroIdeDocument(
|
||||
document: document, changed: true, remove: true,
|
||||
}, nil
|
||||
}
|
||||
root := cloneKiroIdeObject(document.root)
|
||||
root := cloneJSONObject(document.root)
|
||||
root["version"] = "v1"
|
||||
values := make([]any, 0, len(hooks))
|
||||
for _, hook := range hooks {
|
||||
@@ -364,7 +350,7 @@ func renderKiroIdeDocument(
|
||||
|
||||
func kiroIdeRuntimeContracts(
|
||||
hooks []map[string]any,
|
||||
) ([]kiroIdeRuntimeContract, error) {
|
||||
) ([]managedRuntimeContract, error) {
|
||||
managedHooks := make([]map[string]any, 0, 2)
|
||||
for _, hook := range hooks {
|
||||
_, managed := kiroIdeSpecification(hook["name"])
|
||||
@@ -373,38 +359,38 @@ func kiroIdeRuntimeContracts(
|
||||
}
|
||||
}
|
||||
if len(managedHooks) != 2 {
|
||||
return []kiroIdeRuntimeContract{}, nil
|
||||
return []managedRuntimeContract{}, nil
|
||||
}
|
||||
nodePath, err := resolveNodeRuntime()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var guard, audit *kiroIdeRuntimeReference
|
||||
var guard, audit *managedScriptReference
|
||||
for _, hook := range managedHooks {
|
||||
reference, err := currentKiroIdeReference(hook, nodePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if reference == nil {
|
||||
return []kiroIdeRuntimeContract{}, nil
|
||||
return []managedRuntimeContract{}, nil
|
||||
}
|
||||
if hook["name"] == kiroIdeGuardName {
|
||||
if guard != nil {
|
||||
return []kiroIdeRuntimeContract{}, nil
|
||||
return []managedRuntimeContract{}, nil
|
||||
}
|
||||
guard = reference
|
||||
} else {
|
||||
if audit != nil {
|
||||
return []kiroIdeRuntimeContract{}, nil
|
||||
return []managedRuntimeContract{}, nil
|
||||
}
|
||||
audit = reference
|
||||
}
|
||||
}
|
||||
if guard == nil || audit == nil ||
|
||||
!sameKiroIdeAgentID(guard.agentID, audit.agentID) {
|
||||
return []kiroIdeRuntimeContract{}, nil
|
||||
!sameManagedAgentID(guard.agentID, audit.agentID) {
|
||||
return []managedRuntimeContract{}, nil
|
||||
}
|
||||
return []kiroIdeRuntimeContract{{
|
||||
return []managedRuntimeContract{{
|
||||
agentID: guard.agentID, guardPath: guard.scriptPath,
|
||||
auditPath: audit.scriptPath,
|
||||
}}, nil
|
||||
@@ -449,10 +435,10 @@ func parseKiroIdeLegacyDocument(
|
||||
return nil, err
|
||||
}
|
||||
if guardReference == nil || auditReference == nil ||
|
||||
!sameKiroIdeAgentID(guardReference.agentID, auditReference.agentID) {
|
||||
!sameManagedAgentID(guardReference.agentID, auditReference.agentID) {
|
||||
return document, nil
|
||||
}
|
||||
document.contract = &kiroIdeRuntimeContract{
|
||||
document.contract = &managedRuntimeContract{
|
||||
agentID: guardReference.agentID, guardPath: guardReference.scriptPath,
|
||||
auditPath: auditReference.scriptPath,
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type kiroIdeRuntimePaths struct {
|
||||
runtimeRoot string
|
||||
agentDirectory string
|
||||
configPath string
|
||||
keyPath string
|
||||
guardPath string
|
||||
auditPath string
|
||||
managedRuntimePaths
|
||||
initialDirectories []kiroIdeDirectoryIdentity
|
||||
}
|
||||
|
||||
@@ -28,84 +21,17 @@ type kiroIdePreparedTransaction struct {
|
||||
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(
|
||||
config InstallConfig,
|
||||
) (*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 {
|
||||
return nil, "", err
|
||||
}
|
||||
paths := &kiroIdeRuntimePaths{managedRuntimePaths: *managed}
|
||||
initialDirectories := make([]kiroIdeDirectoryIdentity, 0, 3)
|
||||
for _, item := range []struct{ path, label string }{
|
||||
{filepath.Dir(paths.runtimeRoot), "home directory"},
|
||||
@@ -124,9 +50,8 @@ func preflightKiroIdeInstallation(
|
||||
initialDirectories[0].path,
|
||||
)
|
||||
}
|
||||
if err := validateKiroIdeRuntimeIdentity(
|
||||
paths.agentDirectory,
|
||||
config.AgentID,
|
||||
if err := validateManagedRuntimeIdentity(
|
||||
paths.agentDirectory, config.AgentID, kiroIdeAgentKey, "Kiro IDE",
|
||||
); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -144,29 +69,6 @@ func preflightKiroIdeInstallation(
|
||||
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(
|
||||
filePath string,
|
||||
label string,
|
||||
@@ -221,10 +123,11 @@ func prepareKiroIdeInstallation(
|
||||
if sources == nil || paths == nil || rendered == nil {
|
||||
return nil, fmt.Errorf("Kiro IDE installation requires prepared sources")
|
||||
}
|
||||
runtimeConfig, err := buildKiroIdeRuntimeConfig(config)
|
||||
runtimeConfig, err := buildManagedRuntimeConfig(config, kiroIdeAgentKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
guardScript, auditScript := kiroIdeExpectedScripts(config.AgentID, nil)
|
||||
prepared := &kiroIdePreparedTransaction{
|
||||
operation: "installation", paths: sources.paths, runtimePaths: paths,
|
||||
initialDirectories: append(
|
||||
@@ -238,20 +141,10 @@ func prepareKiroIdeInstallation(
|
||||
mode os.FileMode
|
||||
maximum int64
|
||||
}{
|
||||
{
|
||||
paths.guardPath, "Elydora guard runtime",
|
||||
[]byte(generateGuardScript(kiroIdeAgentKey, config.AgentID, "", false, "")),
|
||||
0700, maxManagedSourceBytes,
|
||||
},
|
||||
{paths.guardPath, "Elydora guard runtime", guardScript, 0700, maxManagedSourceBytes},
|
||||
{paths.configPath, "Elydora runtime config", runtimeConfig, 0600, maxRuntimeConfigBytes},
|
||||
{paths.keyPath, "Elydora private key", []byte(config.PrivateKey), 0600, maxProtectedSecretBytes},
|
||||
{
|
||||
paths.auditPath, "Elydora audit runtime",
|
||||
[]byte(buildHookScriptWithOutput(
|
||||
kiroIdeAgentKey, config.AgentID, "", false, true,
|
||||
)),
|
||||
0700, maxManagedSourceBytes,
|
||||
},
|
||||
{paths.auditPath, "Elydora audit runtime", auditScript, 0700, maxManagedSourceBytes},
|
||||
} {
|
||||
change, condition, err := prepareKiroIdeFile(
|
||||
item.path, item.label, item.contents, item.mode, item.maximum,
|
||||
@@ -274,7 +167,7 @@ func prepareKiroIdeInstallation(
|
||||
prepared.changes = append(prepared.changes, documentChange)
|
||||
}
|
||||
removeLegacy := sources.legacy.contract != nil &&
|
||||
sameKiroIdeAgentID(sources.legacy.contract.agentID, config.AgentID)
|
||||
sameManagedAgentID(sources.legacy.contract.agentID, config.AgentID)
|
||||
if removeLegacy {
|
||||
legacyChange, err := prepareSnapshotSourceChange(
|
||||
sources.legacy.filePath,
|
||||
@@ -335,7 +228,7 @@ func prepareKiroIdeUninstall(
|
||||
prepared.changes = append(prepared.changes, documentChange)
|
||||
}
|
||||
removeLegacy := sources.legacy.contract != nil &&
|
||||
(agentID == "" || sameKiroIdeAgentID(sources.legacy.contract.agentID, agentID))
|
||||
(agentID == "" || sameManagedAgentID(sources.legacy.contract.agentID, agentID))
|
||||
if removeLegacy {
|
||||
change, err := prepareSnapshotSourceChange(
|
||||
sources.legacy.filePath,
|
||||
@@ -383,7 +276,7 @@ func commitKiroIdeTransaction(
|
||||
}
|
||||
workspaceChange := false
|
||||
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
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,157 +1,13 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func readKiroIdeRuntimeConfig(path string) (map[string]any, bool, error) {
|
||||
snapshot, err := readManagedFile(path, "Elydora runtime config", maxRuntimeConfigBytes)
|
||||
if err != nil || snapshot == nil {
|
||||
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 kiroIdeExpectedScripts(agentID string, _ map[string]any) ([]byte, []byte) {
|
||||
return []byte(generateGuardScript(kiroIdeAgentKey, agentID, "", false, "")),
|
||||
[]byte(buildHookScriptWithOutput(kiroIdeAgentKey, agentID, "", false, true))
|
||||
}
|
||||
|
||||
func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool {
|
||||
@@ -161,116 +17,44 @@ func kiroIdeRuntimeFileMode(snapshot *managedFileSnapshot, expected uint32) bool
|
||||
return uint32(snapshot.mode.Perm()) == expected
|
||||
}
|
||||
|
||||
func kiroIdeRuntimeContractExists(contract kiroIdeRuntimeContract) (bool, error) {
|
||||
runtimeRoot, err := AgentRuntimeRoot()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// kiroIdeRuntimeModesMatch requires the private modes the installer writes.
|
||||
func kiroIdeRuntimeModesMatch(contract managedRuntimeContract) (bool, error) {
|
||||
agentDirectory := filepath.Dir(contract.guardPath)
|
||||
if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) ||
|
||||
!sameKiroIdeAgentID(filepath.Base(agentDirectory), contract.agentID) ||
|
||||
!sameKiroIdePath(
|
||||
contract.guardPath,
|
||||
filepath.Join(agentDirectory, kiroIdeGuardScript),
|
||||
) ||
|
||||
!sameKiroIdePath(
|
||||
contract.auditPath,
|
||||
filepath.Join(agentDirectory, kiroIdeAuditScript),
|
||||
) {
|
||||
return false, nil
|
||||
for _, item := range []struct {
|
||||
path, label string
|
||||
limit int64
|
||||
mode uint32
|
||||
}{
|
||||
{filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", maxRuntimeConfigBytes, 0600},
|
||||
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", maxProtectedSecretBytes, 0600},
|
||||
{contract.guardPath, "Elydora guard runtime", maxManagedSourceBytes, 0700},
|
||||
{contract.auditPath, "Elydora audit runtime", maxManagedSourceBytes, 0700},
|
||||
} {
|
||||
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(
|
||||
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
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func kiroIdeRuntimeFilesExist(contracts []kiroIdeRuntimeContract) (bool, error) {
|
||||
func kiroIdeRuntimeFilesExist(contracts []managedRuntimeContract) (bool, error) {
|
||||
for _, contract := range contracts {
|
||||
exists, err := kiroIdeRuntimeContractExists(contract)
|
||||
exists, err := managedRuntimeContractExists(
|
||||
contract, kiroIdeAgentKey, "Kiro IDE", kiroIdeExpectedScripts,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if exists {
|
||||
return true, nil
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
modes, err := kiroIdeRuntimeModesMatch(contract)
|
||||
if err != nil || modes {
|
||||
return modes, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestKiroIdeStatusRequiresUniqueExactPairAndRuntimeIntegrity(t *testing.T) {
|
||||
document = readKiroIdeObject(t, fixture.configPath)
|
||||
document["hooks"] = append(
|
||||
requireArray(t, document["hooks"]),
|
||||
cloneKiroIdeObject(findKiroIdeHook(t, document, kiroIdeGuardName)),
|
||||
cloneJSONObject(findKiroIdeHook(t, document, kiroIdeGuardName)),
|
||||
)
|
||||
writeKiroIdeJSON(t, fixture.configPath, document)
|
||||
assertStatus(false, false)
|
||||
@@ -283,7 +283,7 @@ func TestKiroIdeStatusRejectsExtraOrphanAndStaleRuntimeCommands(t *testing.T) {
|
||||
name = "node.exe"
|
||||
}
|
||||
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 {
|
||||
t.Fatalf("build stale Kiro IDE command: %v", err)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestKiroIdeRollbackRestoresRuntimeWorkspaceAndLegacySources(t *testing.T) {
|
||||
legacyKiroIdeGoDocument(fixture, kiroIdeTestAgentID),
|
||||
)
|
||||
fixture.plugin.rename = func(source, destination string) error {
|
||||
if sameKiroIdePath(source, fixture.legacyPath) &&
|
||||
if sameManagedPath(source, fixture.legacyPath) &&
|
||||
strings.HasSuffix(destination, ".rollback") {
|
||||
return errors.New("injected Kiro IDE legacy cleanup failure")
|
||||
}
|
||||
@@ -320,7 +320,7 @@ func TestKiroIdeInstallSurvivesAdversarialWorkspaceLinkSwap(t *testing.T) {
|
||||
}
|
||||
swapped := false
|
||||
fixture.plugin.rename = func(source, destination string) error {
|
||||
if !swapped && sameKiroIdePath(destination, fixture.configPath) &&
|
||||
if !swapped && sameManagedPath(destination, fixture.configPath) &&
|
||||
strings.HasSuffix(source, ".tmp") {
|
||||
swapped = true
|
||||
backup := kiroDirectory + ".original"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -99,14 +98,8 @@ func (p *LettaPlugin) Uninstall(agentID string) error {
|
||||
return err
|
||||
}
|
||||
if agentID != "" {
|
||||
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)
|
||||
}
|
||||
if err := removeAgentScripts(agentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Println("Uninstalled Elydora hook for Letta Code.")
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// OpenCodePlugin manages the Elydora audit hook for OpenCode.
|
||||
// It writes .config/opencode/plugins/elydora-audit.mjs in the home directory.
|
||||
var openCodeScriptPathPattern = regexp.MustCompile(`(?m)^const (GUARD_SCRIPT_PATH|HOOK_SCRIPT_PATH) = (".*");$`)
|
||||
|
||||
// OpenCodePlugin writes ~/.config/opencode/plugins/elydora-audit.mjs.
|
||||
type OpenCodePlugin struct{}
|
||||
|
||||
func (p *OpenCodePlugin) configPath() (string, error) {
|
||||
@@ -59,8 +62,7 @@ func (p *OpenCodePlugin) Install(config InstallConfig) error {
|
||||
}
|
||||
|
||||
func buildOpenCodePlugin(scriptPath, guardPath string) string {
|
||||
return fmt.Sprintf(`// Elydora Audit Plugin for OpenCode
|
||||
// Generated by @elydora/sdk — DO NOT EDIT
|
||||
return fmt.Sprintf(`// Elydora audit plugin for OpenCode. Generated by the Elydora CLI; do not edit.
|
||||
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
|
||||
@@ -112,62 +114,72 @@ func (p *OpenCodePlugin) Uninstall(agentID string) error {
|
||||
}
|
||||
|
||||
if agentID != "" {
|
||||
scriptPath, _ := hookScriptPath(agentID)
|
||||
if scriptPath != "" {
|
||||
os.Remove(scriptPath)
|
||||
}
|
||||
gPath, _ := guardScriptPath(agentID)
|
||||
if gPath != "" {
|
||||
os.Remove(gPath)
|
||||
if err := removeAgentScripts(agentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Uninstalled Elydora hook for OpenCode.")
|
||||
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) {
|
||||
pluginFile, err := p.configPath()
|
||||
if err != nil {
|
||||
return PluginStatus{}, err
|
||||
}
|
||||
|
||||
status := PluginStatus{
|
||||
AgentName: "opencode",
|
||||
DisplayName: "OpenCode",
|
||||
ConfigPath: pluginFile,
|
||||
}
|
||||
|
||||
if _, err := os.Stat(pluginFile); err == nil {
|
||||
status.HookConfigured = true
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data, err := os.ReadFile(pluginFile)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return status, nil
|
||||
}
|
||||
|
||||
status.Installed = status.HookConfigured && status.HookScriptExists
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -13,15 +12,6 @@ const (
|
||||
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.
|
||||
func WriteRuntimeFileAtomic(
|
||||
path string,
|
||||
@@ -54,27 +44,12 @@ func generateHookScriptWithRename(
|
||||
return err
|
||||
}
|
||||
|
||||
baseURL := config.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.elydora.com"
|
||||
if config.BaseURL == "" {
|
||||
return fmt.Errorf("base URL is required")
|
||||
}
|
||||
encodedConfig, err := json.MarshalIndent(agentRuntimeConfig{
|
||||
OrgID: config.OrgID,
|
||||
AgentID: config.AgentID,
|
||||
KID: config.KID,
|
||||
BaseURL: baseURL,
|
||||
Token: config.Token,
|
||||
AgentName: config.AgentName,
|
||||
}, "", " ")
|
||||
encodedConfig, err := buildManagedRuntimeConfig(config, config.AgentName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode Elydora runtime config: %w", err)
|
||||
}
|
||||
encodedConfig = append(encodedConfig, '\n')
|
||||
if len(encodedConfig) > maxRuntimeConfigBytes {
|
||||
return fmt.Errorf(
|
||||
"Elydora runtime config exceeds %d bytes after JSON encoding",
|
||||
maxRuntimeConfigBytes,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
items := []struct {
|
||||
|
||||
Reference in new issue
Block a user