feat(go): durable transaction engine and Kiro IDE 1.0 adapter
Split the monolithic transaction into focused modules: append-only crash-recoverable journal under ~/.elydora/transactions with owner tokens and per-transaction namespaces, staged atomic publication (stage .pending, fsync, rename, fsync), platform-specific atomic replacement (MoveFileEx, renameat2, renamex_np), private-directory hardening per OS, lock-guarded recovery, and workspace cleanup that removes only transaction-created empty directories. Add the Kiro IDE 1.0 plugin: strict v1 hook schema with one enabled PreToolUse guard and PostToolUse audit pair bound to the active workspace, agent-to-workspace identity binding in runtime config, and exact migration of the legacy elydora-audit.kiro.hook contract. Gates: go test ./... and -race pass, go vet clean, govulncheck reports zero reachable vulnerabilities, git diff --check clean.
This commit is contained in:
1 parent
dd72793cdb
commit
eb41b991bf
70 files changed
+8182
-358
No files matched your search
+92
-107
@@ -1,143 +1,128 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
// KiroIdePlugin manages the Elydora audit hook for Kiro IDE.
|
||||
// It writes a .kiro/hooks/elydora-audit.kiro.hook JSON file in the home directory.
|
||||
type KiroIdePlugin struct{}
|
||||
// KiroIdePlugin manages Kiro IDE 1.0 workspace hooks.
|
||||
type KiroIdePlugin struct {
|
||||
rename renameFunc
|
||||
}
|
||||
|
||||
func (p *KiroIdePlugin) configPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
// ManagesGuardRuntime reports that Kiro IDE commits both generated runtimes,
|
||||
// runtime metadata, credentials, workspace hooks, and legacy migration in one
|
||||
// transaction.
|
||||
func (p *KiroIdePlugin) ManagesGuardRuntime() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PreflightInstall validates every source and runtime identity before writes.
|
||||
func (p *KiroIdePlugin) PreflightInstall(config InstallConfig) error {
|
||||
sources, err := readKiroIdeSources()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve home directory: %w", err)
|
||||
return err
|
||||
}
|
||||
return filepath.Join(home, ".kiro", "hooks", "elydora-audit.kiro.hook"), nil
|
||||
if err := requireAvailableKiroIdeHooks(sources.document.hooks); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err = preflightKiroIdeInstallation(config)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *KiroIdePlugin) Install(config InstallConfig) error {
|
||||
scriptPath, err := hookScriptPath(config.AgentID)
|
||||
sources, err := readKiroIdeSources()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.HookScript != "" {
|
||||
scriptPath = config.HookScript
|
||||
if err := requireAvailableKiroIdeHooks(sources.document.hooks); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := GenerateHookScript(scriptPath, config); err != nil {
|
||||
return fmt.Errorf("generate hook script: %w", err)
|
||||
}
|
||||
|
||||
guardPath := config.GuardScriptPath
|
||||
if guardPath == "" {
|
||||
guardPath, err = guardScriptPath(config.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hookFile, err := p.configPath()
|
||||
paths, nodePath, err := preflightKiroIdeInstallation(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(hookFile)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("create directory %s: %w", dir, err)
|
||||
hooks := withoutManagedKiroIdeHooks(sources.document.hooks, "")
|
||||
guard, err := buildKiroIdeHook(kiroIdeGuardName, nodePath, paths.guardPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := buildKiroIdeHookFile(scriptPath, guardPath)
|
||||
if err := os.WriteFile(hookFile, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", hookFile, err)
|
||||
audit, err := buildKiroIdeHook(kiroIdeAuditName, nodePath, paths.auditPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Installed Elydora hook for Kiro IDE at %s\n", hookFile)
|
||||
rendered, err := renderKiroIdeDocument(
|
||||
sources.document,
|
||||
append(hooks, guard, audit),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prepared, err := prepareKiroIdeInstallation(config, sources, paths, rendered)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := commitKiroIdeInstallation(prepared, p.rename); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Kiro IDE workspace hooks: %s\n", sources.paths.configPath)
|
||||
fmt.Println("Kiro IDE verification: confirm both Elydora entries in the Agent Hooks panel.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildKiroIdeHookFile(scriptPath, guardPath string) string {
|
||||
hookConfig := map[string]interface{}{
|
||||
"name": "Elydora Audit",
|
||||
"hooks": map[string]interface{}{
|
||||
"pre_tool_use": map[string]interface{}{
|
||||
"command": "node " + guardPath,
|
||||
"timeout_ms": 5000,
|
||||
},
|
||||
"post_tool_use": map[string]interface{}{
|
||||
"command": "node " + scriptPath,
|
||||
"timeout_ms": 5000,
|
||||
},
|
||||
},
|
||||
}
|
||||
encoded, _ := json.MarshalIndent(hookConfig, "", " ")
|
||||
return string(encoded) + "\n"
|
||||
}
|
||||
|
||||
func (p *KiroIdePlugin) Uninstall(agentID string) error {
|
||||
hookFile, err := p.configPath()
|
||||
sources, err := readKiroIdeSources()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(hookFile); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("remove %s: %w", hookFile, err)
|
||||
rendered, err := renderKiroIdeDocument(
|
||||
sources.document,
|
||||
withoutManagedKiroIdeHooks(sources.document.hooks, agentID),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if agentID != "" {
|
||||
scriptPath, _ := hookScriptPath(agentID)
|
||||
if scriptPath != "" {
|
||||
os.Remove(scriptPath)
|
||||
}
|
||||
gPath, _ := guardScriptPath(agentID)
|
||||
if gPath != "" {
|
||||
os.Remove(gPath)
|
||||
}
|
||||
prepared, err := prepareKiroIdeUninstall(sources, rendered, agentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Uninstalled Elydora hook for Kiro IDE.")
|
||||
if err := commitKiroIdeUninstall(prepared, p.rename); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Uninstalled Elydora hooks from Kiro IDE.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *KiroIdePlugin) Status() (PluginStatus, error) {
|
||||
hookFile, err := p.configPath()
|
||||
if err != nil {
|
||||
return PluginStatus{}, err
|
||||
}
|
||||
|
||||
sources, err := readKiroIdeSources()
|
||||
entry := SupportedAgents[kiroIdeAgentKey]
|
||||
status := PluginStatus{
|
||||
AgentName: "kiroide",
|
||||
DisplayName: "Kiro IDE",
|
||||
ConfigPath: hookFile,
|
||||
AgentName: kiroIdeAgentKey,
|
||||
DisplayName: entry.Name,
|
||||
ConfigPath: sourcesPathOrEmpty(sources),
|
||||
}
|
||||
|
||||
// Check if hook file exists and contains both pre_tool_use and post_tool_use
|
||||
data, err := os.ReadFile(hookFile)
|
||||
if err == nil {
|
||||
var config map[string]interface{}
|
||||
if json.Unmarshal(data, &config) == nil {
|
||||
hooks, _ := config["hooks"].(map[string]interface{})
|
||||
if hooks != nil {
|
||||
_, hasPre := hooks["pre_tool_use"]
|
||||
_, hasPost := hooks["post_tool_use"]
|
||||
status.HookConfigured = hasPre && hasPost
|
||||
|
||||
// Extract hook script path from the post_tool_use command
|
||||
if postHook, ok := hooks["post_tool_use"].(map[string]interface{}); ok {
|
||||
if cmd, _ := postHook["command"].(string); cmd != "" {
|
||||
scriptPath := extractPathFromNodeCommand(cmd)
|
||||
if scriptPath != "" {
|
||||
if _, err := os.Stat(scriptPath); err == nil {
|
||||
status.HookScriptExists = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
|
||||
status.Installed = status.HookConfigured && status.HookScriptExists
|
||||
if err := requireAvailableKiroIdeHooks(sources.document.hooks); err != nil {
|
||||
return status, err
|
||||
}
|
||||
contracts, err := kiroIdeRuntimeContracts(sources.document.hooks)
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
status.HookConfigured = len(contracts) > 0
|
||||
if !status.HookConfigured {
|
||||
return status, nil
|
||||
}
|
||||
status.HookScriptExists, err = kiroIdeRuntimeFilesExist(contracts)
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
status.Installed = status.HookScriptExists
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func sourcesPathOrEmpty(sources *kiroIdeSources) string {
|
||||
if sources == nil {
|
||||
return ""
|
||||
}
|
||||
return sources.paths.configPath
|
||||
}
|
||||
Reference in new issue
Block a user