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
|
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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user