fix: harden Cursor hook enforcement

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 00:15:21 -04:00
1 parent bf777a2ff0
commit 415a884efe
12 files changed
+1980 -340

No files matched your search

+15 -5
View File
@@ -15,12 +15,12 @@ type InstallConfig struct {
// PluginStatus describes the current state of a plugin installation.
type PluginStatus struct {
Installed bool
AgentName string
DisplayName string
HookConfigured bool
Installed bool
AgentName string
DisplayName string
HookConfigured bool
HookScriptExists bool
ConfigPath string
ConfigPath string
}
// AgentPlugin is the interface that every agent integration must implement.
@@ -29,3 +29,13 @@ type AgentPlugin interface {
Uninstall(agentID string) error
Status() (PluginStatus, error)
}
// InstallPreflighter validates provider and runtime sources before CLI writes.
type InstallPreflighter interface {
PreflightInstall(config InstallConfig) error
}
// GuardRuntimeManager owns provider-specific guard generation transactionally.
type GuardRuntimeManager interface {
ManagesGuardRuntime() bool
}
+125 -189
View File
@@ -2,236 +2,172 @@ package plugins
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// CursorPlugin manages the Elydora audit hook for Cursor.
// It writes/merges into ~/.cursor/hooks.json using nested settings.hooks.postToolUse[].
type CursorPlugin struct{}
// CursorPlugin manages Cursor's native global user hooks.
type CursorPlugin struct {
rename renameFunc
}
func (p *CursorPlugin) configPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
// ManagesGuardRuntime reports that Cursor commits its provider-specific guard
// in the same transaction as the rest of the installation.
func (p *CursorPlugin) ManagesGuardRuntime() bool {
return true
}
func cursorAgentPaths(config InstallConfig) (
runtimeRoot, agentDirectory, guardPath, auditPath string,
err error,
) {
if config.AgentID == "" {
return "", "", "", "", fmt.Errorf("agent ID is required")
}
return filepath.Join(home, ".cursor", "hooks.json"), nil
runtimeRoot, err = AgentRuntimeRoot()
if err != nil {
return "", "", "", "", err
}
agentDirectory, err = ResolveAgentRuntimeDirectory(config.AgentID)
if err != nil {
return "", "", "", "", err
}
guardPath = filepath.Join(agentDirectory, cursorGuardScript)
if !sameCursorPath(config.GuardScriptPath, guardPath) {
return "", "", "", "", fmt.Errorf(
"Elydora guard runtime must use the managed agent directory: %s",
guardPath,
)
}
auditPath = filepath.Join(agentDirectory, cursorAuditScript)
if config.HookScript != "" && !sameCursorPath(config.HookScript, auditPath) {
return "", "", "", "", fmt.Errorf(
"Elydora audit runtime must use the managed agent directory: %s",
auditPath,
)
}
return runtimeRoot, agentDirectory, guardPath, auditPath, nil
}
// PreflightInstall validates every existing source before the CLI creates a
// runtime directory or guard file.
func (p *CursorPlugin) PreflightInstall(config InstallConfig) error {
if _, err := readCursorDocument(); err != nil {
return err
}
_, agentDirectory, _, _, err := cursorAgentPaths(config)
if err != nil {
return err
}
if err := preflightCursorRuntime(agentDirectory, config.AgentID); err != nil {
return err
}
_, err = resolveNodeRuntime()
return err
}
func (p *CursorPlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath(config.AgentID)
document, err := readCursorDocument()
if err != nil {
return err
}
if config.HookScript != "" {
scriptPath = config.HookScript
}
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
}
}
configPath, err := p.configPath()
runtimeRoot, agentDirectory, guardPath, auditPath, err := cursorAgentPaths(config)
if err != nil {
return err
}
settings, err := readJSONFile(configPath)
if err := preflightCursorRuntime(agentDirectory, config.AgentID); err != nil {
return err
}
nodePath, err := resolveNodeRuntime()
if err != nil {
return err
}
// Ensure hooks object exists
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil {
hooks = make(map[string]interface{})
hooks := removeManagedCursorHooks(document.hooks, runtimeRoot, "")
hooks["preToolUse"] = append(
hooks["preToolUse"],
buildCursorHandler(nodePath, guardPath),
)
hooks["postToolUse"] = append(
hooks["postToolUse"],
buildCursorHandler(nodePath, auditPath),
)
hooks["postToolUseFailure"] = append(
hooks["postToolUseFailure"],
buildCursorHandler(nodePath, auditPath),
)
rendered, err := renderCursorDocument(document, hooks, runtimeRoot)
if err != nil {
return fmt.Errorf("render Cursor user hooks: %w", err)
}
// --- preToolUse (guard — freeze enforcement) ---
preToolUse, _ := hooks["preToolUse"].([]interface{})
var preFiltered []interface{}
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if cmd, _ := m["command"].(string); strings.Contains(cmd, "elydora") {
continue
}
}
preFiltered = append(preFiltered, entry)
}
guardEntry := map[string]interface{}{
"command": "node " + guardPath,
}
preFiltered = append(preFiltered, guardEntry)
hooks["preToolUse"] = preFiltered
// --- postToolUse (audit logging) ---
postToolUse, _ := hooks["postToolUse"].([]interface{})
var postFiltered []interface{}
for _, entry := range postToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if cmd, _ := m["command"].(string); strings.Contains(cmd, "elydora") {
continue
}
}
postFiltered = append(postFiltered, entry)
}
hookEntry := map[string]interface{}{
"command": "node " + scriptPath,
}
postFiltered = append(postFiltered, hookEntry)
hooks["postToolUse"] = postFiltered
settings["hooks"] = hooks
if err := writeJSONFile(configPath, settings); err != nil {
changes, err := prepareCursorInstallationChanges(config, agentDirectory, rendered)
if err != nil {
return err
}
fmt.Printf("Installed Elydora hook for Cursor at %s\n", configPath)
if err := writeCursorChanges(
changes,
"Install Cursor hooks",
p.rename,
runtimeRoot,
agentDirectory,
); err != nil {
return err
}
fmt.Printf(" Cursor hooks: %s\n", document.filePath)
return nil
}
func (p *CursorPlugin) Uninstall(agentID string) error {
configPath, err := p.configPath()
document, err := readCursorDocument()
if err != nil {
return err
}
settings, err := readJSONFile(configPath)
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return err
}
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil {
fmt.Println("No Cursor hooks found.")
return nil
hooks := removeManagedCursorHooks(document.hooks, runtimeRoot, agentID)
rendered, err := renderCursorDocument(document, hooks, runtimeRoot)
if err != nil {
return fmt.Errorf("render Cursor user hooks: %w", err)
}
// Remove preToolUse Elydora entries
preToolUse, _ := hooks["preToolUse"].([]interface{})
var preFiltered []interface{}
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if cmd, _ := m["command"].(string); strings.Contains(cmd, "elydora") {
continue
}
}
preFiltered = append(preFiltered, entry)
}
if len(preFiltered) == 0 {
delete(hooks, "preToolUse")
} else {
hooks["preToolUse"] = preFiltered
}
// Remove postToolUse Elydora entries
postToolUse, _ := hooks["postToolUse"].([]interface{})
var postFiltered []interface{}
for _, entry := range postToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if cmd, _ := m["command"].(string); strings.Contains(cmd, "elydora") {
continue
}
}
postFiltered = append(postFiltered, entry)
}
if len(postFiltered) == 0 {
delete(hooks, "postToolUse")
} else {
hooks["postToolUse"] = postFiltered
}
if len(hooks) == 0 {
delete(settings, "hooks")
} else {
settings["hooks"] = hooks
}
if err := writeJSONFile(configPath, settings); err != nil {
change, err := prepareRenderedCursorChange(rendered)
if err != nil {
return err
}
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
}
}
fmt.Println("Uninstalled Elydora hook for Cursor.")
return nil
return writeCursorChanges(
[]*fileChange{change},
"Uninstall Cursor hooks",
p.rename,
"",
"",
)
}
func (p *CursorPlugin) Status() (PluginStatus, error) {
configPath, err := p.configPath()
if err != nil {
return PluginStatus{}, err
}
configPath, pathErr := cursorConfigPath()
entry := SupportedAgents[cursorAgentKey]
status := PluginStatus{
AgentName: "cursor",
DisplayName: "Cursor",
ConfigPath: configPath,
AgentName: cursorAgentKey, DisplayName: entry.Name, ConfigPath: configPath,
}
settings, err := readJSONFile(configPath)
if pathErr != nil {
return status, pathErr
}
document, err := readCursorDocument()
if err != nil {
return status, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return status, err
}
contracts := cursorRuntimeContracts(document.hooks, runtimeRoot)
status.HookConfigured = len(contracts) > 0
if !status.HookConfigured {
return status, nil
}
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks != nil {
preConfigured := hasCursorElydoraEntry(hooks["preToolUse"])
postConfigured := hasCursorElydoraEntry(hooks["postToolUse"])
status.HookConfigured = preConfigured && postConfigured
// Extract hook script path from the configured command
scriptPath := extractCursorElydoraScriptPath(hooks["postToolUse"])
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
status.HookScriptExists, err = cursorRuntimeFilesExist(contracts)
if err != nil {
return status, err
}
status.Installed = status.HookConfigured && status.HookScriptExists
status.Installed = status.HookScriptExists
return status, nil
}
// extractCursorElydoraScriptPath extracts the script path from a Cursor hook array's Elydora command entry.
func extractCursorElydoraScriptPath(hookArray interface{}) string {
arr, _ := hookArray.([]interface{})
for _, entry := range arr {
if m, ok := entry.(map[string]interface{}); ok {
if cmd, _ := m["command"].(string); strings.Contains(cmd, "elydora") {
return extractPathFromNodeCommand(cmd)
}
}
}
return ""
}
// hasCursorElydoraEntry checks if a Cursor hook array (flat format) contains an Elydora entry.
func hasCursorElydoraEntry(hookArray interface{}) bool {
arr, _ := hookArray.([]interface{})
for _, entry := range arr {
if m, ok := entry.(map[string]interface{}); ok {
if cmd, _ := m["command"].(string); strings.Contains(cmd, "elydora") {
return true
}
}
}
return false
}
+419
View File
@@ -0,0 +1,419 @@
package plugins
import (
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
)
const (
cursorAgentKey = "cursor"
cursorConfigFile = "hooks.json"
cursorGuardScript = "guard.js"
cursorAuditScript = "hook.js"
cursorHookTimeout = float64(10)
cursorPOSIXApostrophe = `'"'"'`
)
type cursorHooks map[string][]map[string]any
type cursorDocument struct {
exists bool
filePath string
root map[string]any
hooks cursorHooks
raw []byte
}
type cursorRenderedDocument struct {
document *cursorDocument
changed bool
next []byte
remove bool
}
type cursorRuntimeContract struct {
agentID string
guardPath string
auditPath string
}
func cloneCursorHooks(source cursorHooks) cursorHooks {
clone := make(cursorHooks, len(source))
for event, handlers := range source {
clone[event] = append([]map[string]any(nil), handlers...)
}
return clone
}
func sameCursorPath(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 sameCursorAgentID(left, right string) bool {
if runtime.GOOS == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func quoteCursorPowerShell(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
func buildCursorHandler(nodePath, scriptPath string) map[string]any {
command := quotePOSIXArgument(nodePath) + " " + quotePOSIXArgument(scriptPath)
if runtime.GOOS == "windows" {
command = "& " + quoteCursorPowerShell(nodePath) + " " +
quoteCursorPowerShell(scriptPath) + "; exit $LASTEXITCODE"
}
return map[string]any{
"command": command, "timeout": cursorHookTimeout, "failClosed": true,
}
}
func readCursorPOSIXArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); {
if strings.HasPrefix(command[index:], cursorPOSIXApostrophe) {
value.WriteByte('\'')
index += len(cursorPOSIXApostrophe)
continue
}
if command[index] == '\'' {
return value.String(), index + 1, true
}
value.WriteByte(command[index])
index++
}
return "", start, false
}
func readCursorPowerShellArgument(command string, start int) (string, int, bool) {
if start >= len(command) || command[start] != '\'' {
return "", start, false
}
var value strings.Builder
for index := start + 1; index < len(command); index++ {
if command[index] != '\'' {
value.WriteByte(command[index])
continue
}
if index+1 < len(command) && command[index+1] == '\'' {
value.WriteByte('\'')
index++
continue
}
return value.String(), index + 1, true
}
return "", start, false
}
func parseCursorPOSIXCommand(command string) (string, string, bool) {
executable, next, ok := readCursorPOSIXArgument(command, 0)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readCursorPOSIXArgument(command, next+1)
return executable, script, ok && end == len(command)
}
func parseCursorPowerShellCommand(command string) (string, string, bool) {
if !strings.HasPrefix(command, "& ") {
return "", "", false
}
executable, next, ok := readCursorPowerShellArgument(command, 2)
if !ok || next >= len(command) || command[next] != ' ' {
return "", "", false
}
script, end, ok := readCursorPowerShellArgument(command, next+1)
return executable, script, ok && command[end:] == "; exit $LASTEXITCODE"
}
func parseCursorLegacyCommand(command string) (string, bool) {
separator := strings.IndexByte(command, ' ')
if separator < 0 {
return "", false
}
executable := command[:separator]
if !strings.EqualFold(executable, "node") && !strings.EqualFold(executable, "node.exe") {
return "", false
}
script := command[separator+1:]
if len(script) >= 2 && script[0] == '"' && script[len(script)-1] == '"' {
script = script[1 : len(script)-1]
}
return script, script != "" && !strings.ContainsAny(script, "\r\n")
}
func isCursorNodeExecutable(path string) bool {
name := filepath.Base(path)
return name == "node" || strings.EqualFold(name, "node.exe")
}
func cursorManagedScriptPath(handler map[string]any) (string, bool) {
if len(handler) == 3 && handler["timeout"] == cursorHookTimeout && handler["failClosed"] == true {
command, ok := handler["command"].(string)
if !ok {
return "", false
}
var nodePath, scriptPath string
var parsed bool
if runtime.GOOS == "windows" {
nodePath, scriptPath, parsed = parseCursorPowerShellCommand(command)
} else {
nodePath, scriptPath, parsed = parseCursorPOSIXCommand(command)
}
if parsed && filepath.IsAbs(nodePath) && filepath.IsAbs(scriptPath) &&
isCursorNodeExecutable(nodePath) {
return scriptPath, true
}
}
if len(handler) != 1 {
return "", false
}
command, ok := handler["command"].(string)
if !ok {
return "", false
}
return parseCursorLegacyCommand(command)
}
func cursorManagedAgentID(
handler map[string]any,
scriptName string,
runtimeRoot string,
) (string, bool) {
scriptPath, managed := cursorManagedScriptPath(handler)
if !managed || !filepath.IsAbs(scriptPath) || filepath.Base(scriptPath) != scriptName {
return "", false
}
agentDirectory := filepath.Dir(scriptPath)
if !sameCursorPath(filepath.Dir(agentDirectory), runtimeRoot) {
return "", false
}
agentID := filepath.Base(agentDirectory)
return agentID, agentID != "" && agentID != "." && agentID != ".."
}
func readCursorHooks(value any, label string) (cursorHooks, error) {
object, ok := value.(map[string]any)
if !ok || object == nil {
return nil, fmt.Errorf(`%s field "hooks" must be an object`, label)
}
hooks := make(cursorHooks, len(object))
for event, handlerValue := range object {
values, ok := handlerValue.([]any)
if !ok {
return nil, fmt.Errorf(`%s field "hooks.%s" must be an array`, label, event)
}
handlers := make([]map[string]any, 0, len(values))
for index, value := range values {
handler, ok := value.(map[string]any)
if !ok || handler == nil {
return nil, fmt.Errorf(`%s handler hooks.%s[%d] must be an object`, label, event, index)
}
handlers = append(handlers, handler)
}
hooks[event] = handlers
}
return hooks, nil
}
func cursorContainsManagedHook(hooks cursorHooks, runtimeRoot string) bool {
for _, contract := range []struct{ event, script string }{
{"preToolUse", cursorGuardScript},
{"postToolUse", cursorAuditScript},
{"postToolUseFailure", cursorAuditScript},
} {
for _, handler := range hooks[contract.event] {
if _, managed := cursorManagedAgentID(handler, contract.script, runtimeRoot); managed {
return true
}
}
}
return false
}
func parseCursorDocument(filePath string, raw []byte, runtimeRoot string) (*cursorDocument, error) {
label := fmt.Sprintf("Cursor user hooks at %s", filePath)
if !json.Valid(raw) {
var value any
if err := json.Unmarshal(raw, &value); err != nil {
return nil, fmt.Errorf("parse %s: %w", label, err)
}
}
root, err := decodeJSONCObject(raw, label, false)
if err != nil {
return nil, err
}
hooks := cursorHooks{}
if value, exists := root["hooks"]; exists {
hooks, err = readCursorHooks(value, label)
if err != nil {
return nil, err
}
}
version, hasVersion := root["version"]
current := hasVersion && version == float64(1)
if !current && (hasVersion || !cursorContainsManagedHook(hooks, runtimeRoot)) {
return nil, fmt.Errorf("%s must declare version 1", label)
}
return &cursorDocument{
exists: true, filePath: filePath, root: root,
hooks: hooks, raw: append([]byte(nil), raw...),
}, nil
}
func createCursorDocument(filePath string) *cursorDocument {
return &cursorDocument{filePath: filePath, root: map[string]any{}, hooks: cursorHooks{}}
}
func removeManagedCursorHooks(hooks cursorHooks, runtimeRoot, agentID string) cursorHooks {
result := cloneCursorHooks(hooks)
for _, contract := range []struct{ event, script string }{
{"preToolUse", cursorGuardScript},
{"postToolUse", cursorAuditScript},
{"postToolUseFailure", cursorAuditScript},
} {
kept := make([]map[string]any, 0, len(result[contract.event]))
for _, handler := range result[contract.event] {
managedID, managed := cursorManagedAgentID(handler, contract.script, runtimeRoot)
if managed && (agentID == "" || sameCursorAgentID(managedID, agentID)) {
continue
}
kept = append(kept, handler)
}
if len(kept) == 0 {
delete(result, contract.event)
} else {
result[contract.event] = kept
}
}
return result
}
func entirelyManagedCursorDocument(document *cursorDocument, runtimeRoot string) bool {
if !document.exists || len(document.hooks) == 0 {
return false
}
for key := range document.root {
if key != "version" && key != "hooks" {
return false
}
}
count := 0
for event, handlers := range document.hooks {
script := map[string]string{
"preToolUse": cursorGuardScript,
"postToolUse": cursorAuditScript,
"postToolUseFailure": cursorAuditScript,
}[event]
if script == "" || len(handlers) == 0 {
return false
}
count += len(handlers)
for _, handler := range handlers {
if _, managed := cursorManagedAgentID(handler, script, runtimeRoot); !managed {
return false
}
}
}
return count > 0
}
func renderCursorDocument(
document *cursorDocument,
hooks cursorHooks,
runtimeRoot string,
) (*cursorRenderedDocument, error) {
if !document.exists && len(hooks) == 0 {
return &cursorRenderedDocument{document: document}, nil
}
if document.exists && reflect.DeepEqual(hooks, document.hooks) {
return &cursorRenderedDocument{document: document}, nil
}
if len(hooks) == 0 && entirelyManagedCursorDocument(document, runtimeRoot) {
return &cursorRenderedDocument{document: document, changed: true, remove: true}, nil
}
root := make(map[string]any, len(document.root)+1)
for key, value := range document.root {
root[key] = value
}
root["version"] = float64(1)
if len(hooks) == 0 {
delete(root, "hooks")
} else {
root["hooks"] = hooks
}
next, err := json.MarshalIndent(root, "", " ")
if err != nil {
return nil, err
}
next = append(next, '\n')
return &cursorRenderedDocument{
document: document, changed: !document.exists || string(next) != string(document.raw), next: next,
}, nil
}
func managedCursorIDs(
handlers []map[string]any,
scriptName, runtimeRoot string,
) map[string]string {
result := map[string]string{}
for _, handler := range handlers {
agentID, managed := cursorManagedAgentID(handler, scriptName, runtimeRoot)
if !managed {
continue
}
key := agentID
if runtime.GOOS == "windows" {
key = strings.ToLower(agentID)
}
result[key] = agentID
}
return result
}
func cursorRuntimeContracts(hooks cursorHooks, runtimeRoot string) []cursorRuntimeContract {
guards := managedCursorIDs(hooks["preToolUse"], cursorGuardScript, runtimeRoot)
audits := managedCursorIDs(hooks["postToolUse"], cursorAuditScript, runtimeRoot)
failures := managedCursorIDs(hooks["postToolUseFailure"], cursorAuditScript, runtimeRoot)
keys := make([]string, 0, len(guards))
for key := range guards {
_, hasAudit := audits[key]
_, hasFailureAudit := failures[key]
if hasAudit && hasFailureAudit {
keys = append(keys, key)
}
}
sort.Strings(keys)
contracts := make([]cursorRuntimeContract, 0, len(keys))
for _, key := range keys {
agentID := guards[key]
directory := filepath.Join(runtimeRoot, agentID)
contracts = append(contracts, cursorRuntimeContract{
agentID: agentID, guardPath: filepath.Join(directory, cursorGuardScript),
auditPath: filepath.Join(directory, cursorAuditScript),
})
}
return contracts
}
+324
View File
@@ -0,0 +1,324 @@
package plugins
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
func cursorConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".cursor", cursorConfigFile), nil
}
func readCursorDocument() (*cursorDocument, error) {
filePath, err := cursorConfigPath()
if err != nil {
return nil, err
}
runtimeRoot, err := AgentRuntimeRoot()
if err != nil {
return nil, err
}
info, err := os.Lstat(filePath)
if errors.Is(err, os.ErrNotExist) {
return createCursorDocument(filePath), nil
}
if err != nil {
return nil, fmt.Errorf("inspect Cursor user hooks at %s: %w", filePath, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return nil, fmt.Errorf("Cursor user hooks path is not a physical file: %s", filePath)
}
raw, err := os.ReadFile(filePath) // #nosec G304 -- filePath is the fixed Cursor user configuration path.
if err != nil {
return nil, fmt.Errorf("read Cursor user hooks at %s: %w", filePath, err)
}
return parseCursorDocument(filePath, raw, runtimeRoot)
}
func prepareRenderedCursorChange(rendered *cursorRenderedDocument) (*fileChange, error) {
if rendered == nil || !rendered.changed {
return nil, nil
}
return prepareSourceChange(
rendered.document.filePath,
"Cursor user hooks",
rendered.document.raw,
rendered.document.exists,
rendered.next,
0600,
rendered.remove,
)
}
func cursorPhysicalFileExists(path, label string) (bool, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("inspect %s at %s: %w", label, path, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return false, fmt.Errorf("%s path is not a physical file: %s", label, path)
}
return true, nil
}
func cursorPhysicalDirectoryExists(path string) (bool, error) {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("inspect Elydora runtime directory at %s: %w", path, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return false, fmt.Errorf("Elydora runtime path is not a physical directory: %s", path)
}
return true, nil
}
func readCursorRuntimeConfig(path string) (map[string]any, bool, error) {
exists, err := cursorPhysicalFileExists(path, "Elydora runtime config")
if err != nil || !exists {
return nil, exists, err
}
raw, err := os.ReadFile(path) // #nosec G304 -- path is confined to one validated Elydora agent directory.
if err != nil {
return nil, true, fmt.Errorf("read Elydora runtime config at %s: %w", path, err)
}
label := fmt.Sprintf("Elydora runtime config at %s", path)
if !json.Valid(raw) {
var value any
if err := json.Unmarshal(raw, &value); err != nil {
return nil, true, fmt.Errorf("parse %s: %w", label, err)
}
}
config, err := decodeJSONCObject(raw, label, false)
if err != nil {
return nil, true, err
}
return config, true, nil
}
func validateCursorRuntimeIdentity(path, agentID string) (bool, error) {
config, exists, err := readCursorRuntimeConfig(path)
if err != nil || !exists {
return exists, err
}
configuredID, ok := config["agent_id"].(string)
if !ok || config["agent_name"] != cursorAgentKey ||
!sameCursorAgentID(configuredID, agentID) {
return true, fmt.Errorf(
"Elydora runtime config identity does not match Cursor agent %s: %s",
agentID,
path,
)
}
return true, nil
}
func preflightCursorRuntime(agentDirectory, agentID string) error {
exists, err := cursorPhysicalDirectoryExists(agentDirectory)
if err != nil || !exists {
return err
}
identityExists, err := validateCursorRuntimeIdentity(
filepath.Join(agentDirectory, "config.json"), agentID,
)
if err != nil {
return err
}
runtimeExists := false
for _, item := range []struct{ path, label string }{
{filepath.Join(agentDirectory, cursorGuardScript), "Elydora guard runtime"},
{filepath.Join(agentDirectory, cursorAuditScript), "Elydora audit runtime"},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key"},
} {
exists, err := cursorPhysicalFileExists(item.path, item.label)
if err != nil {
return err
}
runtimeExists = runtimeExists || exists
}
if runtimeExists && !identityExists {
return fmt.Errorf(
"Elydora runtime identity cannot be verified without config.json: %s",
agentDirectory,
)
}
return nil
}
func prepareCursorInstallationChanges(
config InstallConfig,
agentDirectory string,
rendered *cursorRenderedDocument,
) ([]*fileChange, error) {
baseURL := config.BaseURL
if baseURL == "" {
baseURL = "https://api.elydora.com"
}
runtimeConfig, err := json.MarshalIndent(agentRuntimeConfig{
OrgID: config.OrgID, AgentID: config.AgentID, KID: config.KID,
BaseURL: baseURL, Token: config.Token, AgentName: cursorAgentKey,
}, "", " ")
if err != nil {
return nil, fmt.Errorf("encode Elydora runtime config: %w", err)
}
runtimeConfig = append(runtimeConfig, '\n')
if len(runtimeConfig) > maxRuntimeConfigBytes {
return nil, fmt.Errorf(
"Elydora runtime config exceeds %d bytes after JSON encoding",
maxRuntimeConfigBytes,
)
}
items := []struct {
path, label string
content []byte
mode os.FileMode
}{
{
filepath.Join(agentDirectory, cursorGuardScript), "Elydora guard runtime",
[]byte(generateGuardScript(
cursorAgentKey,
config.AgentID,
`{"permission":"allow"}`+"\n",
true,
)), 0700,
},
{filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", runtimeConfig, 0600},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey), 0600},
{
filepath.Join(agentDirectory, cursorAuditScript), "Elydora audit runtime",
[]byte(buildHookScriptWithOutput(
cursorAgentKey,
config.AgentID,
"{}\n",
true,
true,
)), 0700,
},
}
changes := make([]*fileChange, 0, len(items)+1)
for _, item := range items {
if err := validateRuntimeFileTarget(item.path, item.label); err != nil {
return nil, err
}
change, err := prepareFileChange(item.path, item.label, item.content, item.mode)
if err != nil {
return nil, err
}
changes = append(changes, change)
}
documentChange, err := prepareRenderedCursorChange(rendered)
if err != nil {
return nil, err
}
return append(changes, documentChange), nil
}
func ensureCursorDirectory(path, label string) error {
if err := os.MkdirAll(path, 0700); err != nil {
return fmt.Errorf("create %s directory at %s: %w", label, path, err)
}
info, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("inspect %s directory at %s: %w", label, path, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return fmt.Errorf("%s directory is not a physical directory: %s", label, path)
}
return nil
}
func writeCursorChanges(
changes []*fileChange,
label string,
rename renameFunc,
runtimeRoot, agentDirectory string,
) error {
hasChanges := false
for _, change := range changes {
if change != nil {
hasChanges = true
break
}
}
if !hasChanges {
return nil
}
if agentDirectory != "" {
if err := EnsurePrivateDirectory(runtimeRoot); err != nil {
return err
}
if err := EnsurePrivateDirectory(agentDirectory); err != nil {
return err
}
}
directories := map[string]string{}
for _, change := range changes {
if change == nil {
continue
}
directory := filepath.Dir(change.filePath)
key := filepath.Clean(directory)
if runtime.GOOS == "windows" {
key = strings.ToLower(key)
}
directories[key] = directory
}
for _, directory := range directories {
if err := ensureCursorDirectory(directory, label); err != nil {
return err
}
}
return writeChanges(changes, label, rename)
}
func cursorRuntimeFilesExist(contracts []cursorRuntimeContract) (bool, error) {
for _, contract := range contracts {
agentDirectory := filepath.Dir(contract.guardPath)
directoryExists, err := cursorPhysicalDirectoryExists(agentDirectory)
if err != nil {
return false, err
}
if !directoryExists {
continue
}
config, exists, err := readCursorRuntimeConfig(filepath.Join(agentDirectory, "config.json"))
if err != nil {
return false, err
}
configuredID, idOK := config["agent_id"].(string)
if !exists || !idOK || config["agent_name"] != cursorAgentKey ||
!sameCursorAgentID(configuredID, contract.agentID) {
continue
}
complete := true
for _, item := range []struct{ path, label string }{
{contract.guardPath, "Elydora guard runtime"},
{contract.auditPath, "Elydora audit runtime"},
{filepath.Join(agentDirectory, "private.key"), "Elydora private key"},
} {
exists, err := cursorPhysicalFileExists(item.path, item.label)
if err != nil {
return false, err
}
complete = complete && exists
}
if complete {
return true, nil
}
}
return false, nil
}
+291
View File
@@ -0,0 +1,291 @@
package plugins
import (
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestCursorInstallRejectsSymlinkRuntimeTargets(t *testing.T) {
for _, name := range []string{"config", "private key", "guard", "audit"} {
t.Run(name, func(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
path := map[string]string{
"config": fixture.runtimeConfig, "private key": fixture.privateKey,
"guard": fixture.guardPath, "audit": fixture.hookPath,
}[name]
target := filepath.Join(fixture.homeDir, strings.ReplaceAll(name, " ", "-")+"-target")
if err := os.WriteFile(target, []byte("target\n"), 0600); err != nil {
t.Fatalf("write %s target: %v", name, err)
}
if err := os.Symlink(target, path); err != nil {
t.Skipf("create %s symlink: %v", name, err)
}
if err := fixture.plugin.Install(fixture.config); err == nil ||
!strings.Contains(err.Error(), "physical file") {
t.Fatalf("symlink %s install error = %v", name, err)
}
if _, err := os.Stat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Cursor config exists after rejection: %v", err)
}
content, err := os.ReadFile(target)
if err != nil || string(content) != "target\n" {
t.Fatalf("%s target = %q, %v", name, content, err)
}
})
}
}
func TestCursorInstallRejectsSymlinkUserConfig(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
target := filepath.Join(fixture.homeDir, "cursor-hooks-target.json")
source := `{"version":1,"hooks":{}}` + "\n"
if err := os.WriteFile(target, []byte(source), 0600); err != nil {
t.Fatalf("write Cursor config target: %v", err)
}
if err := os.MkdirAll(filepath.Dir(fixture.configPath), 0700); err != nil {
t.Fatalf("create Cursor config directory: %v", err)
}
if err := os.Symlink(target, fixture.configPath); err != nil {
t.Skipf("create Cursor config symlink: %v", err)
}
if err := fixture.plugin.Install(fixture.config); err == nil ||
!strings.Contains(err.Error(), "physical file") {
t.Fatalf("symlink config install error = %v", err)
}
content, err := os.ReadFile(target)
if err != nil || string(content) != source {
t.Fatalf("Cursor config target = %q, %v", content, err)
}
}
func TestCursorInstallRejectsSymlinkUserConfigDirectory(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
target := t.TempDir()
if err := os.Symlink(target, filepath.Dir(fixture.configPath)); err != nil {
t.Skipf("create Cursor config directory symlink: %v", err)
}
if err := fixture.plugin.Install(fixture.config); err == nil ||
!strings.Contains(err.Error(), "physical directory") {
t.Fatalf("symlink config directory install error = %v", err)
}
assertCursorRuntimeAbsent(t, fixture)
entries, err := os.ReadDir(target)
if err != nil || len(entries) != 0 {
t.Fatalf("Cursor config target entries = %#v, %v", entries, err)
}
}
func TestCursorInstallRejectsRuntimeIdentityBeforeWrites(t *testing.T) {
fixture := prepareCursorFixture(t, nil, true)
writeCursorObject(t, fixture.runtimeConfig, map[string]any{
"agent_name": "codex", "agent_id": cursorTestAgentID,
})
guardBefore, err := os.ReadFile(fixture.guardPath)
if err != nil {
t.Fatalf("read guard before install: %v", err)
}
err = fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "identity does not match") {
t.Fatalf("runtime identity install error = %v", err)
}
guardAfter, readErr := os.ReadFile(fixture.guardPath)
if readErr != nil || string(guardAfter) != string(guardBefore) {
t.Fatalf("guard changed to %q, %v", guardAfter, readErr)
}
if _, err := os.Stat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Cursor config exists after identity rejection: %v", err)
}
}
func TestCursorInstallRejectsOrphanRuntimeBeforeWrites(t *testing.T) {
fixture := prepareCursorFixture(t, nil, true)
guardBefore, err := os.ReadFile(fixture.guardPath)
if err != nil {
t.Fatalf("read orphan guard: %v", err)
}
err = fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "identity cannot be verified") {
t.Fatalf("orphan runtime install error = %v", err)
}
guardAfter, readErr := os.ReadFile(fixture.guardPath)
if readErr != nil || string(guardAfter) != string(guardBefore) {
t.Fatalf("orphan guard changed to %q, %v", guardAfter, readErr)
}
if _, err := os.Stat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Cursor config exists after orphan rejection: %v", err)
}
}
func TestCursorStatusRejectsSymlinkRuntimeFiles(t *testing.T) {
for _, name := range []string{"config", "private key", "guard", "audit"} {
t.Run(name, func(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
installCursorFixture(t, fixture)
path := map[string]string{
"config": fixture.runtimeConfig, "private key": fixture.privateKey,
"guard": fixture.guardPath, "audit": fixture.hookPath,
}[name]
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s runtime: %v", name, err)
}
target := filepath.Join(fixture.homeDir, "status-"+strings.ReplaceAll(name, " ", "-"))
if err := os.WriteFile(target, content, 0600); err != nil {
t.Fatalf("write %s target: %v", name, err)
}
if err := os.Remove(path); err != nil {
t.Fatalf("remove %s runtime: %v", name, err)
}
if err := os.Symlink(target, path); err != nil {
t.Skipf("create %s runtime symlink: %v", name, err)
}
status, err := fixture.plugin.Status()
if err == nil || status.Installed {
t.Fatalf("Cursor status = %#v, %v", status, err)
}
})
}
}
func TestCursorInstallRequiresNodeBeforeWrites(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
t.Setenv("PATH", "")
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "Node.js") {
t.Fatalf("install error = %v", err)
}
assertCursorRuntimeAbsent(t, fixture)
if _, err := os.Stat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Cursor config exists after Node.js rejection: %v", err)
}
}
func TestCursorInstallCreatesRuntimeDirectoryTransactionally(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
if err := os.Remove(fixture.agentDir); err != nil {
t.Fatalf("remove empty agent directory: %v", err)
}
installCursorFixture(t, fixture)
for _, path := range []string{
fixture.runtimeConfig,
fixture.privateKey,
fixture.guardPath,
fixture.hookPath,
} {
if exists, err := cursorPhysicalFileExists(path, "Cursor runtime"); err != nil || !exists {
t.Fatalf("runtime file %s exists = %v, %v", path, exists, err)
}
}
}
func TestCursorInstallRollsBackEveryFileOnConfigCommitFailure(t *testing.T) {
fixture := prepareCursorFixture(t, cursorJSON(map[string]any{
"version": float64(1),
"hooks": map[string]any{"sessionStart": []any{map[string]any{"command": "keep"}}},
}), false)
before, err := os.ReadFile(fixture.configPath)
if err != nil {
t.Fatalf("read Cursor config: %v", err)
}
failed := false
fixture.plugin.rename = func(source, destination string) error {
if !failed && sameCursorPath(destination, fixture.configPath) {
failed = true
return errors.New("injected Cursor config commit failure")
}
return os.Rename(source, destination)
}
err = fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "injected Cursor config commit failure") {
t.Fatalf("install error = %v", err)
}
assertCursorFileBytes(t, fixture.configPath, before)
assertCursorRuntimeAbsent(t, fixture)
assertNoCursorTransactionArtifacts(t, fixture.homeDir)
}
func TestCursorInstallDetectsConcurrentSourceChangeAndRollsBack(t *testing.T) {
fixture := prepareCursorFixture(t, cursorJSON(map[string]any{
"version": float64(1), "hooks": map[string]any{},
}), false)
concurrent := []byte(`{"version":1,"hooks":{"sessionStart":[{"command":"concurrent"}]}}` + "\n")
mutated := false
fixture.plugin.rename = func(source, destination string) error {
if !mutated && strings.HasSuffix(source, ".tmp") {
mutated = true
if err := os.WriteFile(fixture.configPath, concurrent, 0600); err != nil {
return err
}
}
return os.Rename(source, destination)
}
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "changed during installation") {
t.Fatalf("install error = %v", err)
}
assertCursorFileBytes(t, fixture.configPath, concurrent)
assertCursorRuntimeAbsent(t, fixture)
assertNoCursorTransactionArtifacts(t, fixture.homeDir)
}
func TestCursorAtomicConfigIsPrivateAndLeavesNoStagingFiles(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
installCursorFixture(t, fixture)
if runtime.GOOS != "windows" {
info, err := os.Stat(fixture.configPath)
if err != nil || info.Mode().Perm() != 0600 {
t.Fatalf("Cursor config mode = %v, %v", info.Mode().Perm(), err)
}
}
assertNoCursorTransactionArtifacts(t, fixture.homeDir)
}
func assertCursorRuntimeAbsent(t *testing.T, fixture *cursorFixture) {
t.Helper()
for _, path := range []string{
fixture.runtimeConfig, fixture.privateKey, fixture.guardPath, fixture.hookPath,
} {
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("runtime file exists at %s: %v", path, err)
}
}
}
func assertCursorFileBytes(t *testing.T, path string, expected []byte) {
t.Helper()
actual, err := os.ReadFile(path)
if err != nil || string(actual) != string(expected) {
t.Fatalf("file %s = %q, %v; want %q", path, actual, err, expected)
}
}
func assertNoCursorTransactionArtifacts(t *testing.T, roots ...string) {
t.Helper()
for _, root := range roots {
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(entry.Name(), ".tmp") || strings.HasSuffix(entry.Name(), ".rollback") {
t.Errorf("transaction artifact remains at %s", path)
}
return nil
})
if err != nil {
t.Fatalf("walk transaction root %s: %v", root, err)
}
}
}
+376
View File
@@ -0,0 +1,376 @@
package plugins
import (
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
)
func TestCursorRegistryUsesNativeUserHooks(t *testing.T) {
want := AgentRegistryEntry{Name: "Cursor", ConfigDir: "~/.cursor", ConfigFile: "hooks.json"}
if got := SupportedAgents["cursor"]; !reflect.DeepEqual(got, want) {
t.Fatalf("Cursor registry = %#v, want %#v", got, want)
}
}
func TestCursorInstallPreservesUserHooksMigratesLegacyAndIsIdempotent(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
writeCursorObject(t, fixture.configPath, map[string]any{
"description": "user-owned",
"hooks": map[string]any{
"sessionStart": []any{map[string]any{"command": "user-session"}},
"preToolUse": []any{
map[string]any{"command": "user-pre"},
map[string]any{"command": "node " + fixture.guardPath},
},
"postToolUse": []any{map[string]any{"command": "node " + fixture.hookPath}},
},
})
installCursorFixture(t, fixture)
installCursorFixture(t, fixture)
settings := readCursorObject(t, fixture.configPath)
if settings["version"] != float64(1) || settings["description"] != "user-owned" {
t.Fatalf("Cursor settings = %#v", settings)
}
hooks := cursorObject(t, settings["hooks"])
if len(cursorArray(t, hooks["sessionStart"])) != 1 ||
len(cursorArray(t, hooks["preToolUse"])) != 2 ||
len(cursorArray(t, hooks["postToolUse"])) != 1 ||
len(cursorArray(t, hooks["postToolUseFailure"])) != 1 {
t.Fatalf("Cursor hooks = %#v", hooks)
}
if cursorObject(t, cursorArray(t, hooks["preToolUse"])[0])["command"] != "user-pre" {
t.Fatalf("first preToolUse handler = %#v", cursorArray(t, hooks["preToolUse"])[0])
}
assertNativeCursorHandler(t, managedCursorHandler(t, settings, "preToolUse", "guard.js"))
assertNativeCursorHandler(t, managedCursorHandler(t, settings, "postToolUse", "hook.js"))
assertNativeCursorHandler(t, managedCursorHandler(t, settings, "postToolUseFailure", "hook.js"))
runtimeConfig := readCursorObject(t, fixture.runtimeConfig)
if runtimeConfig["agent_name"] != "cursor" || runtimeConfig["agent_id"] != cursorTestAgentID {
t.Fatalf("runtime config = %#v", runtimeConfig)
}
}
func TestCursorCommandsBlockAndForwardOfficialPayloadByteForByte(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
installCursorFixture(t, fixture)
settings := readCursorObject(t, fixture.configPath)
guard := managedCursorHandler(t, settings, "preToolUse", "guard.js")
writeCursorObject(t, filepath.Join(fixture.agentDir, "status-cache.json"), map[string]any{
"status": "active", "cached_at": time.Now().UnixMilli(),
})
active := runCursorHandler(t, guard, "{}\n")
var allow map[string]any
if err := json.Unmarshal([]byte(active.stdout), &allow); err != nil || allow["permission"] != "allow" {
t.Fatalf("active guard output = %q, %v", active.stdout, err)
}
writeCursorObject(t, filepath.Join(fixture.agentDir, "status-cache.json"), map[string]any{
"status": "frozen", "cached_at": time.Now().UnixMilli(),
})
prePayload := `{"conversation_id":"conversation-1","generation_id":"generation-1","hook_event_name":"preToolUse","tool_name":"Shell","tool_input":{"command":"Get-ChildItem"},"tool_use_id":"call-1","cwd":"project"}` + "\n"
frozen := runCursorHandler(t, guard, prePayload)
if frozen.exitCode != 2 || !strings.Contains(frozen.stderr, "frozen by Elydora") {
t.Fatalf("frozen guard result = %#v", frozen)
}
writeCursorObject(t, filepath.Join(fixture.agentDir, "status-cache.json"), map[string]any{
"status": "revoked", "cached_at": time.Now().UnixMilli(),
})
revoked := runCursorHandler(t, guard, prePayload)
if revoked.exitCode != 2 || !strings.Contains(revoked.stderr, "revoked in Elydora") {
t.Fatalf("revoked guard result = %#v", revoked)
}
capturePath := filepath.Join(t.TempDir(), "captured.json")
captureScript := "const fs = require('node:fs');\n" +
"fs.writeFileSync(process.env.ELYDORA_CAPTURE, fs.readFileSync(0));\n" +
"process.stdout.write('{}\\n');\n"
if err := os.WriteFile(fixture.hookPath, []byte(captureScript), 0700); err != nil {
t.Fatalf("write capture runtime: %v", err)
}
postPayload := `{"conversation_id":"conversation-1","generation_id":"generation-1","hook_event_name":"postToolUse","tool_name":"Shell","tool_input":{"command":"Get-ChildItem"},"tool_output":"{\"exitCode\":0,\"stdout\":\"ok\"}","tool_use_id":"call-1","cwd":"project","duration":42}` + "\n"
audit := managedCursorHandler(t, settings, "postToolUse", "hook.js")
result := runCursorHandler(t, audit, postPayload, "ELYDORA_CAPTURE="+capturePath)
captured, err := os.ReadFile(capturePath)
if err != nil || result.exitCode != 0 || string(captured) != postPayload {
t.Fatalf("audit result = %#v, captured = %q, %v", result, captured, err)
}
failurePayload := `{"conversation_id":"conversation-1","generation_id":"generation-1","hook_event_name":"postToolUseFailure","tool_name":"Shell","tool_input":{"command":"exit 1"},"tool_use_id":"call-2","cwd":"project","error_message":"command failed","failure_type":"error","duration":21,"is_interrupt":false}` + "\n"
failureAudit := managedCursorHandler(t, settings, "postToolUseFailure", "hook.js")
failureResult := runCursorHandler(t, failureAudit, failurePayload, "ELYDORA_CAPTURE="+capturePath)
captured, err = os.ReadFile(capturePath)
if err != nil || failureResult.exitCode != 0 || string(captured) != failurePayload {
t.Fatalf("failure audit result = %#v, captured = %q, %v", failureResult, captured, err)
}
}
func TestCursorAuditPreservesNativeFailurePayload(t *testing.T) {
received := make(chan map[string]any, 1)
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
defer request.Body.Close()
var operation map[string]any
if err := json.NewDecoder(request.Body).Decode(&operation); err != nil {
t.Errorf("decode operation: %v", err)
}
received <- operation
response.WriteHeader(http.StatusCreated)
}))
defer server.Close()
fixture := prepareCursorFixture(t, nil, false)
fixture.config.BaseURL = server.URL
fixture.config.PrivateKey = base64.RawURLEncoding.EncodeToString(make([]byte, 32))
installCursorFixture(t, fixture)
payload := `{"conversation_id":"conversation-1","generation_id":"generation-1","hook_event_name":"postToolUseFailure","tool_name":"Shell","tool_input":{"command":"exit 1"},"tool_use_id":"call-1","cwd":"project","error_message":"command failed","failure_type":"error","duration":42,"is_interrupt":false}` + "\n"
handler := managedCursorHandler(
t,
readCursorObject(t, fixture.configPath),
"postToolUseFailure",
cursorAuditScript,
)
result := runCursorHandler(t, handler, payload)
if result.exitCode != 0 || result.stdout != "{}\n" || result.stderr != "" {
t.Fatalf("Cursor audit result = %#v", result)
}
operation := <-received
var expected map[string]any
if err := json.Unmarshal([]byte(payload), &expected); err != nil {
t.Fatalf("decode expected payload: %v", err)
}
if !reflect.DeepEqual(operation["payload"], expected) {
t.Fatalf("operation payload = %#v, want %#v", operation["payload"], expected)
}
subject := cursorObject(t, operation["subject"])
if subject["session_id"] != "conversation-1" {
t.Fatalf("operation subject = %#v", subject)
}
}
func TestCursorGeneratedRuntimesFailClosed(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
installCursorFixture(t, fixture)
settings := readCursorObject(t, fixture.configPath)
if err := os.Remove(fixture.runtimeConfig); err != nil {
t.Fatalf("remove runtime config: %v", err)
}
guard := runCursorHandler(
t,
managedCursorHandler(t, settings, "preToolUse", cursorGuardScript),
"{}\n",
)
if guard.exitCode != 1 || guard.stdout != "" || !strings.Contains(guard.stderr, "Failed to read agent config") {
t.Fatalf("guard failure = %#v", guard)
}
auditFixture := prepareCursorFixture(t, nil, false)
installCursorFixture(t, auditFixture)
auditSettings := readCursorObject(t, auditFixture.configPath)
if err := os.Remove(auditFixture.privateKey); err != nil {
t.Fatalf("remove private key: %v", err)
}
audit := runCursorHandler(
t,
managedCursorHandler(t, auditSettings, "postToolUse", cursorAuditScript),
`{"conversation_id":"conversation-1","tool_name":"Shell","tool_input":{}}`+"\n",
)
if audit.exitCode != 1 || audit.stdout != "" || !strings.Contains(audit.stderr, "Failed to read agent config/key") {
t.Fatalf("audit failure = %#v", audit)
}
}
func TestCursorStatusRequiresExactPairIdentityAndRuntimeFiles(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
installCursorFixture(t, fixture)
status, err := fixture.plugin.Status()
if err != nil || !status.Installed || !status.HookConfigured || !status.HookScriptExists {
t.Fatalf("installed status = %#v, %v", status, err)
}
settings := readCursorObject(t, fixture.configPath)
managedCursorHandler(t, settings, "preToolUse", "guard.js")["failClosed"] = false
writeCursorObject(t, fixture.configPath, settings)
status, err = fixture.plugin.Status()
if err != nil || status.HookConfigured || status.Installed {
t.Fatalf("modified handler status = %#v, %v", status, err)
}
installCursorFixture(t, fixture)
settings = readCursorObject(t, fixture.configPath)
delete(cursorObject(t, settings["hooks"]), "postToolUseFailure")
writeCursorObject(t, fixture.configPath, settings)
status, err = fixture.plugin.Status()
if err != nil || status.HookConfigured || status.Installed {
t.Fatalf("missing failure audit status = %#v, %v", status, err)
}
installCursorFixture(t, fixture)
if err := os.Remove(fixture.hookPath); err != nil {
t.Fatalf("remove audit runtime: %v", err)
}
status, err = fixture.plugin.Status()
if err != nil || !status.HookConfigured || status.HookScriptExists || status.Installed {
t.Fatalf("missing runtime status = %#v, %v", status, err)
}
installCursorFixture(t, fixture)
runtimeConfig := readCursorObject(t, fixture.runtimeConfig)
runtimeConfig["agent_id"] = "another-agent"
writeCursorObject(t, fixture.runtimeConfig, runtimeConfig)
status, err = fixture.plugin.Status()
if err != nil || status.HookScriptExists || status.Installed {
t.Fatalf("identity mismatch status = %#v, %v", status, err)
}
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
t.Fatalf("write malformed runtime config: %v", err)
}
if _, err := fixture.plugin.Status(); err == nil || !strings.Contains(err.Error(), "parse Elydora runtime config") {
t.Fatalf("malformed runtime status error = %v", err)
}
}
func TestCursorUninstallRemovesExactOwnershipAndPreservesUserEntries(t *testing.T) {
fixture := prepareCursorFixture(t, cursorJSON(map[string]any{
"version": float64(1),
"hooks": map[string]any{"sessionStart": []any{map[string]any{"command": "keep"}}},
}), false)
installCursorFixture(t, fixture)
settings := readCursorObject(t, fixture.configPath)
hooks := cursorObject(t, settings["hooks"])
pre := managedCursorHandler(t, settings, "preToolUse", "guard.js")
post := managedCursorHandler(t, settings, "postToolUse", "hook.js")
otherPre := map[string]any{}
otherPost := map[string]any{}
otherFailure := map[string]any{}
for key, value := range pre {
otherPre[key] = value
}
for key, value := range post {
otherPost[key] = value
otherFailure[key] = value
}
otherPre["command"] = strings.ReplaceAll(cursorStringValue(otherPre["command"]), "agent-1", "agent-10")
otherPost["command"] = strings.ReplaceAll(cursorStringValue(otherPost["command"]), "agent-1", "agent-10")
hooks["preToolUse"] = append(cursorArray(t, hooks["preToolUse"]), otherPre, map[string]any{
"command": "echo elydora agent-1 guard.js", "timeout": float64(10), "failClosed": true,
})
hooks["postToolUse"] = append(cursorArray(t, hooks["postToolUse"]), otherPost)
otherFailure["command"] = strings.ReplaceAll(
cursorStringValue(otherFailure["command"]),
"agent-1",
"agent-10",
)
hooks["postToolUseFailure"] = append(
cursorArray(t, hooks["postToolUseFailure"]),
otherFailure,
)
writeCursorObject(t, fixture.configPath, settings)
uninstallID := cursorTestAgentID
if runtime.GOOS == "windows" {
uninstallID = strings.ToUpper(uninstallID)
}
if err := fixture.plugin.Uninstall(uninstallID); err != nil {
t.Fatalf("uninstall Cursor hooks: %v", err)
}
remaining := readCursorObject(t, fixture.configPath)
remainingHooks := cursorObject(t, remaining["hooks"])
if len(cursorArray(t, remainingHooks["preToolUse"])) != 2 ||
len(cursorArray(t, remainingHooks["postToolUse"])) != 1 ||
len(cursorArray(t, remainingHooks["postToolUseFailure"])) != 1 ||
len(cursorArray(t, remainingHooks["sessionStart"])) != 1 {
t.Fatalf("remaining hooks = %#v", remainingHooks)
}
}
func TestCursorRejectsInvalidConfigBeforeRuntimeWrites(t *testing.T) {
cases := []string{
"{ malformed", "[]\n", `{"hooks":{}}`, `{"version":2,"hooks":{}}`,
`{"version":1,"hooks":null}`, `{"version":1,"hooks":{"preToolUse":null}}`,
`{"version":1,"hooks":{"preToolUse":[null]}}`,
`{"version":1,"version":1,"hooks":{}}`, `{"version":1,"hooks":{},}`,
`{"version":1,"hooks":{/* comments are outside Cursor's JSON contract */}}`,
}
for index, source := range cases {
t.Run(strings.ReplaceAll(source, "/", "_"), func(t *testing.T) {
fixture := prepareCursorFixture(t, cursorString(source), true)
originalGuard, readErr := os.ReadFile(fixture.guardPath)
if readErr != nil {
t.Fatalf("read original guard: %v", readErr)
}
if err := fixture.plugin.Install(fixture.config); err == nil {
t.Fatalf("invalid config %d was accepted: %s", index, source)
}
raw, err := os.ReadFile(fixture.configPath)
if err != nil || string(raw) != source {
t.Fatalf("config changed to %q, %v", raw, err)
}
guard, err := os.ReadFile(fixture.guardPath)
if err != nil || string(guard) != string(originalGuard) {
t.Fatalf("guard changed, %v", err)
}
for _, path := range []string{fixture.hookPath, fixture.runtimeConfig, fixture.privateKey} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("runtime write exists at %s: %v", path, err)
}
}
})
}
}
func TestCursorCreatesMissingGuardAndRejectsUnmanagedRuntimePaths(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
installCursorFixture(t, fixture)
if exists, err := regularFileExists(fixture.guardPath, "Cursor guard"); err != nil || !exists {
t.Fatalf("guard exists = %v, %v", exists, err)
}
for _, field := range []string{"guard", "audit"} {
t.Run(field, func(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
unmanaged := filepath.Join(fixture.homeDir, "unmanaged-"+field+".js")
if field == "guard" {
fixture.config.GuardScriptPath = unmanaged
} else {
fixture.config.HookScript = unmanaged
}
if err := fixture.plugin.Install(fixture.config); err == nil ||
!strings.Contains(err.Error(), "managed agent directory") {
t.Fatalf("unmanaged %s error = %v", field, err)
}
if _, err := os.Stat(fixture.configPath); !os.IsNotExist(err) {
t.Fatalf("Cursor config exists after rejected path: %v", err)
}
})
}
}
func TestCursorUninstallRemovesOwnedConfigAndPreservesUserEmptyConfig(t *testing.T) {
fixture := prepareCursorFixture(t, nil, false)
installCursorFixture(t, fixture)
if err := fixture.plugin.Uninstall(cursorTestAgentID); err != nil {
t.Fatalf("uninstall Cursor hooks: %v", err)
}
if _, err := os.Stat(fixture.configPath); !os.IsNotExist(err) {
t.Fatalf("owned Cursor config remains: %v", err)
}
if err := fixture.plugin.Uninstall(cursorTestAgentID); err != nil {
t.Fatalf("uninstall absent Cursor hooks: %v", err)
}
userSource := `{"version":1,"hooks":{}}` + "\n"
writeOptionalCursorFile(t, fixture.configPath, cursorString(userSource))
if err := fixture.plugin.Uninstall(cursorTestAgentID); err != nil {
t.Fatalf("uninstall user Cursor config: %v", err)
}
raw, err := os.ReadFile(fixture.configPath)
if err != nil || string(raw) != userSource {
t.Fatalf("user config = %q, %v", raw, err)
}
}
@@ -0,0 +1,201 @@
package plugins
import (
"bytes"
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
const cursorTestAgentID = "agent-1"
type cursorFixture struct {
plugin *CursorPlugin
config InstallConfig
homeDir string
agentDir string
configPath string
guardPath string
hookPath string
runtimeConfig string
privateKey string
}
type cursorCommandResult struct {
exitCode int
stdout string
stderr string
}
func cursorString(value string) *string {
return &value
}
func cursorJSON(value any) *string {
encoded, err := json.MarshalIndent(value, "", " ")
if err != nil {
panic(err)
}
return cursorString(string(encoded) + "\n")
}
func prepareCursorFixture(t *testing.T, existingRaw *string, createGuard bool) *cursorFixture {
t.Helper()
rootDir := t.TempDir()
homeDir := filepath.Join(rootDir, "home with spaces and 'quote")
agentDir := filepath.Join(homeDir, ".elydora", cursorTestAgentID)
configPath := filepath.Join(homeDir, ".cursor", "hooks.json")
guardPath := filepath.Join(agentDir, "guard.js")
hookPath := filepath.Join(agentDir, "hook.js")
if err := os.MkdirAll(agentDir, 0700); err != nil {
t.Fatalf("create Cursor fixture: %v", err)
}
if createGuard {
if err := os.WriteFile(
guardPath,
[]byte(GenerateGuardScript("cursor", cursorTestAgentID)),
0700,
); err != nil {
t.Fatalf("write guard runtime: %v", err)
}
}
writeOptionalCursorFile(t, configPath, existingRaw)
t.Setenv("HOME", homeDir)
t.Setenv("USERPROFILE", homeDir)
return &cursorFixture{
plugin: &CursorPlugin{},
homeDir: homeDir,
agentDir: agentDir,
configPath: configPath,
guardPath: guardPath,
hookPath: hookPath,
runtimeConfig: filepath.Join(agentDir, "config.json"),
privateKey: filepath.Join(agentDir, "private.key"),
config: InstallConfig{
AgentName: "cursor", OrgID: "org-1", AgentID: cursorTestAgentID,
PrivateKey: "test-key", KID: "kid-1", Token: "token-1",
BaseURL: "https://api.elydora.test", GuardScriptPath: guardPath,
},
}
}
func writeOptionalCursorFile(t *testing.T, path string, source *string) {
t.Helper()
if source == nil {
return
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
t.Fatalf("create directory for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(*source), 0600); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
func writeCursorObject(t *testing.T, path string, value map[string]any) {
t.Helper()
writeOptionalCursorFile(t, path, cursorJSON(value))
}
func readCursorObject(t *testing.T, path string) map[string]any {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
var value map[string]any
if err := json.Unmarshal(raw, &value); err != nil {
t.Fatalf("decode %s: %v", path, err)
}
return value
}
func cursorObject(t *testing.T, value any) map[string]any {
t.Helper()
object, ok := value.(map[string]any)
if !ok {
t.Fatalf("value is not an object: %#v", value)
}
return object
}
func cursorArray(t *testing.T, value any) []any {
t.Helper()
array, ok := value.([]any)
if !ok {
t.Fatalf("value is not an array: %#v", value)
}
return array
}
func managedCursorHandler(t *testing.T, settings map[string]any, event, script string) map[string]any {
t.Helper()
hooks := cursorObject(t, settings["hooks"])
for _, value := range cursorArray(t, hooks[event]) {
handler := cursorObject(t, value)
if strings.Contains(cursorStringValue(handler["command"]), script) {
return handler
}
}
t.Fatalf("managed Cursor handler for %s was not found", event)
return nil
}
func cursorStringValue(value any) string {
result, _ := value.(string)
return result
}
func assertNativeCursorHandler(t *testing.T, handler map[string]any) {
t.Helper()
if len(handler) != 3 || handler["timeout"] != float64(10) || handler["failClosed"] != true {
t.Fatalf("Cursor handler = %#v", handler)
}
command := cursorStringValue(handler["command"])
if !strings.Contains(strings.ToLower(command), "node") {
t.Fatalf("Cursor command = %q", command)
}
if runtime.GOOS == "windows" &&
(!strings.HasPrefix(command, "& '") || !strings.HasSuffix(command, "; exit $LASTEXITCODE")) {
t.Fatalf("Cursor PowerShell command = %q", command)
}
if runtime.GOOS != "windows" && !strings.HasPrefix(command, "'") {
t.Fatalf("Cursor POSIX command = %q", command)
}
}
func runCursorHandler(t *testing.T, handler map[string]any, payload string, environment ...string) cursorCommandResult {
t.Helper()
var process *exec.Cmd
if runtime.GOOS == "windows" {
process = exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-Command", cursorStringValue(handler["command"]))
} else {
process = exec.Command("sh", "-c", cursorStringValue(handler["command"]))
}
process.Env = append(os.Environ(), environment...)
process.Stdin = strings.NewReader(payload)
var stdout, stderr bytes.Buffer
process.Stdout, process.Stderr = &stdout, &stderr
err := process.Run()
if err == nil {
return cursorCommandResult{stdout: stdout.String(), stderr: stderr.String()}
}
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return cursorCommandResult{exitCode: exitError.ExitCode(), stdout: stdout.String(), stderr: stderr.String()}
}
t.Fatalf("run Cursor hook: %v", err)
return cursorCommandResult{exitCode: -1}
}
func installCursorFixture(t *testing.T, fixture *cursorFixture) {
t.Helper()
if err := fixture.plugin.Install(fixture.config); err != nil {
t.Fatalf("install Cursor hooks: %v", err)
}
}
+139
View File
@@ -0,0 +1,139 @@
package plugins
import "fmt"
// GenerateGuardScript returns a self-contained Node.js guard script that checks
// whether the agent is frozen and blocks tool execution if so.
// It caches the agent status locally (60s TTL) to avoid hitting the API on every
// tool call, and fails open if the API is unreachable or config is missing.
func GenerateGuardScript(agentName string, agentID string) string {
return generateGuardScript(agentName, agentID, "", false)
}
func generateGuardScript(agentName, agentID, successOutput string, failClosed bool) string {
return fmt.Sprintf(`#!/usr/bin/env node
'use strict';
// ---------------------------------------------------------------------------
// Elydora agent guard (PreToolUse) for: %s
// Generated by @elydora/sdk — DO NOT EDIT
// ---------------------------------------------------------------------------
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const AGENT_NAME = %q;
const AGENT_ID = %q;
const SUCCESS_OUTPUT = %q;
const FAIL_CLOSED = %t;
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'status-cache.json');
const CACHE_TTL_MS = 60000;
%s
process.stdin.resume();
process.stdin.on('data', () => {});
function blockAgent(status) {
const state = status === 'frozen' ? 'is frozen by Elydora' : 'is revoked in Elydora';
process.stderr.write(
'Agent "' + AGENT_NAME + '" ' + state + '. Tool execution blocked.\n',
);
process.exitCode = 2;
}
async function main() {
let config;
try {
config = JSON.parse(readProtectedFile(
CONFIG_PATH,
'Agent config',
MAX_PROTECTED_CONFIG_BYTES,
).toString('utf-8'));
} catch (err) {
if (FAIL_CLOSED) {
throw new Error('Failed to read agent config: ' + err.message);
}
process.stderr.write('[Elydora guard] Failed to read agent config: ' + err.message + '\n');
return;
}
try {
const raw = fs.readFileSync(STATUS_CACHE_PATH, 'utf-8');
const cache = JSON.parse(raw);
const age = Date.now() - (cache.cached_at || 0);
if (age < CACHE_TTL_MS) {
if (!['active', 'frozen', 'revoked'].includes(cache.status)) {
throw new Error('Status cache contains an invalid status');
}
if (cache.status !== 'active') {
blockAgent(cache.status);
}
return;
}
} catch (err) {
if (err.code !== 'ENOENT') {
if (FAIL_CLOSED) {
throw new Error('Failed to read status cache: ' + err.message);
}
process.stderr.write('[Elydora guard] Failed to read status cache: ' + err.message + '\n');
}
}
const baseURL = (config.base_url || 'https://api.elydora.com').replace(/\/+$/, '');
const token = config.token || '';
const agentID = config.agent_id;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const headers = { 'Accept': 'application/json' };
if (token) {
headers['Authorization'] = 'Bearer ' + token;
}
const response = await fetch(baseURL + '/v1/agents/' + encodeURIComponent(agentID), {
headers: headers,
signal: controller.signal,
});
if (!response.ok) {
throw new Error('Agent status API returned HTTP ' + response.status);
}
const data = await response.json();
const status = data.agent && data.agent.status;
if (!['active', 'frozen', 'revoked'].includes(status)) {
throw new Error('Agent status API returned an invalid status');
}
try {
fs.writeFileSync(STATUS_CACHE_PATH, JSON.stringify({
status: status,
cached_at: Date.now(),
}), 'utf-8');
} catch (err) {
process.stderr.write('[Elydora guard] Failed to update status cache: ' + err.message + '\n');
}
if (status !== 'active') {
blockAgent(status);
}
} catch (err) {
if (FAIL_CLOSED) {
throw new Error('Failed to resolve agent status: ' + err.message);
}
process.stderr.write('[Elydora guard] Failed to resolve agent status: ' + err.message + '\n');
} finally {
clearTimeout(timeout);
}
}
main().then(() => {
if ((!process.exitCode || process.exitCode === 0) && SUCCESS_OUTPUT) {
process.stdout.write(SUCCESS_OUTPUT);
}
}).catch((err) => {
process.stderr.write('[Elydora guard] ' + (err.stack || err.message || String(err)) + '\n');
process.exitCode = 1;
});
`, agentName, agentName, agentID, successOutput, failClosed, protectedRuntimeFileReader)
}
+59 -127
View File
@@ -1,118 +1,15 @@
package plugins
import (
"fmt"
)
// GenerateGuardScript returns a self-contained Node.js guard script that checks
// whether the agent is frozen and blocks tool execution if so.
// It caches the agent status locally (60s TTL) to avoid hitting the API on every
// tool call, and fails open if the API is unreachable or config is missing.
func GenerateGuardScript(agentName string, agentId string) string {
return fmt.Sprintf(`#!/usr/bin/env node
'use strict';
// ---------------------------------------------------------------------------
// Elydora agent guard (PreToolUse) for: %s
// Generated by @elydora/sdk — DO NOT EDIT
// ---------------------------------------------------------------------------
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const AGENT_NAME = %q;
const AGENT_ID = %q;
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'status-cache.json');
const CACHE_TTL_MS = 60000; // 60 seconds
%s
// Consume stdin so the parent process doesn't block on a full pipe
process.stdin.resume();
process.stdin.on('data', () => {});
async function main() {
// Read agent config
let config;
try {
config = JSON.parse(readProtectedFile(
CONFIG_PATH,
'Agent config',
MAX_PROTECTED_CONFIG_BYTES,
).toString('utf-8'));
} catch {
return; // Can't read config — fail-open (allow)
}
// Check cached status
try {
const raw = fs.readFileSync(STATUS_CACHE_PATH, 'utf-8');
const cache = JSON.parse(raw);
const age = Date.now() - (cache.cached_at || 0);
if (age < CACHE_TTL_MS) {
if (cache.status === 'frozen') {
process.stderr.write('Agent "' + AGENT_NAME + '" is frozen by Elydora. Tool execution blocked.\n');
process.exitCode = 2;
}
return; // Cache is fresh — use cached result
}
} catch {
// No cache or invalid — need to check API
}
// Fetch agent status from API
const baseUrl = (config.base_url || 'https://api.elydora.com').replace(/\/+$/, '');
const token = config.token || '';
const agentId = config.agent_id;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const headers = { 'Accept': 'application/json' };
if (token) {
headers['Authorization'] = 'Bearer ' + token;
}
const res = await fetch(baseUrl + '/v1/agents/' + encodeURIComponent(agentId), {
headers: headers,
signal: controller.signal,
});
clearTimeout(timeout);
if (res.ok) {
const data = await res.json();
const agentStatus = (data.agent && data.agent.status) || 'active';
// Update cache
try {
fs.writeFileSync(STATUS_CACHE_PATH, JSON.stringify({
status: agentStatus,
cached_at: Date.now(),
}), 'utf-8');
} catch {
// Ignore cache write failures
}
if (agentStatus === 'frozen') {
process.stderr.write('Agent "' + AGENT_NAME + '" is frozen by Elydora. Tool execution blocked.\n');
process.exitCode = 2;
}
}
} catch {
// API unreachable — fail-open (allow)
}
}
main();
`, agentName, agentName, agentId, protectedRuntimeFileReader)
}
import "fmt"
func buildHookScript(agentName string, agentId string) string {
return buildHookScriptWithOutput(agentName, agentId, "", false, false)
}
func buildHookScriptWithOutput(
agentName, agentId, successOutput string,
failClosed, nativePayload bool,
) string {
return fmt.Sprintf(`#!/usr/bin/env node
'use strict';
@@ -128,6 +25,9 @@ const os = require('node:os');
const AGENT_NAME = %q;
const AGENT_ID = %q;
const SUCCESS_OUTPUT = %q;
const FAIL_CLOSED = %t;
const NATIVE_PAYLOAD = %t;
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
const KEY_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'private.key');
@@ -254,7 +154,15 @@ function readChainState() {
const raw = fs.readFileSync(CHAIN_STATE_PATH, 'utf-8');
const state = JSON.parse(raw);
return state.prev_chain_hash || ZERO_CHAIN_HASH;
} catch {
} catch (err) {
if (err.code === 'ENOENT') {
return ZERO_CHAIN_HASH;
}
const failure = new Error('Failed to read chain state: ' + err.message);
if (FAIL_CLOSED) {
throw failure;
}
logError(failure);
return ZERO_CHAIN_HASH;
}
}
@@ -282,8 +190,10 @@ function logError(err) {
const ts = new Date().toISOString();
const msg = ts + ' [' + AGENT_NAME + '] ' + (err.stack || err.message || String(err)) + '\n';
fs.appendFileSync(ERROR_LOG_PATH, msg, 'utf-8');
} catch {
// Silently ignore logging failures
} catch (logFailure) {
process.stderr.write(
'[Elydora audit] Failed to write error log: ' + logFailure.message + '\n',
);
}
}
@@ -299,13 +209,15 @@ async function main() {
chunks.push(chunk);
}
const rawInput = Buffer.concat(chunks).toString('utf-8').trim();
if (!rawInput) return;
if (!rawInput) {
throw new Error('Hook input is empty');
}
let hookData;
try {
hookData = JSON.parse(rawInput);
} catch {
return;
} catch (err) {
throw new Error('Hook input is invalid JSON: ' + err.message);
}
// Extract provider-native tool fields without reshaping the hook input.
@@ -321,7 +233,8 @@ async function main() {
hookData.tool_input || hookData.toolInput || hookData.input || hookData.parameters ||
clineToolEvent.input || {};
const sessionId =
hookData.session_id || hookData.sessionId || hookData.session || hookData.taskId || 'unknown';
hookData.conversation_id || hookData.session_id || hookData.sessionId ||
hookData.session || hookData.taskId || 'unknown';
// Read agent config and private key
let config;
@@ -334,7 +247,11 @@ async function main() {
).toString('utf-8'));
privateKey = parsePrivateKey(readProtectedFile(KEY_PATH, 'Private key'));
} catch (err) {
logError(new Error('Failed to read agent config/key: ' + err.message));
const failure = new Error('Failed to read agent config/key: ' + err.message);
if (FAIL_CLOSED) {
throw failure;
}
logError(failure);
return;
}
@@ -343,6 +260,9 @@ async function main() {
const kid = config.kid;
const baseUrl = (config.base_url || 'https://api.elydora.com').replace(/\/+$/, '');
const token = config.token || '';
if (!orgId || !agentId || !kid) {
throw new Error('Agent config is missing org_id, agent_id, or kid');
}
// Read chain state
const prevChainHash = readChainState();
@@ -352,7 +272,7 @@ async function main() {
const issuedAt = Date.now();
const nonce = generateNonce();
const payload = {
const payload = NATIVE_PAYLOAD ? hookData : {
tool_name: toolName,
tool_input: toolInput,
session_id: sessionId,
@@ -406,9 +326,10 @@ async function main() {
});
if (res.ok) {
// Server accepted — safe to advance local chain state
writeChainState(chainHash);
} else if (res.status === 400) {
return;
}
if (res.status === 400) {
// Possible chain hash mismatch — try to resync from server response
try {
const errBody = await res.json();
@@ -420,18 +341,29 @@ async function main() {
logError(new Error('Chain hash resynced to server: ' + match[1]));
}
}
} catch { /* ignore parse errors */ }
} catch (err) {
logError(new Error('Failed to parse audit API error: ' + err.message));
}
}
} catch {
// Network error — don't advance chain state, will retry with same prev_chain_hash
throw new Error('Audit API returned HTTP ' + res.status);
} finally {
clearTimeout(timeout);
}
} catch (err) {
logError(err);
if (FAIL_CLOSED) {
throw err;
}
}
}
main();
`, agentName, agentName, agentId, protectedRuntimeFileReader)
main().then(() => {
if (SUCCESS_OUTPUT) {
process.stdout.write(SUCCESS_OUTPUT);
}
}).catch((err) => {
process.stderr.write('[Elydora audit] ' + (err.stack || err.message || String(err)) + '\n');
process.exitCode = 1;
});
`, agentName, agentName, agentId, successOutput, failClosed, nativePayload, protectedRuntimeFileReader)
}