fix(go): sync hardened Cursor hooks
This commit is contained in:
1 parent
b6112cffd9
commit
ab54b75020
12 files changed
+1980
-340
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user