fix(go): sync hardened Cursor hooks

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 00:28:21 -04:00
1 parent b6112cffd9
commit ab54b75020
12 files changed
+1980 -340

No files matched your search

+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)
}