import { PROTECTED_RUNTIME_READER } from './runtime-reader-template.js'; export interface GuardScriptOptions { readonly failClosed?: boolean; readonly successOutput?: string; readonly denyProtocol?: 'cursor' | 'grok'; } export function generateGuardScript( agentName: string, agentId: string, options: GuardScriptOptions = {}, ): string { const failClosed = options.failClosed ?? false; const successOutput = options.successOutput ?? ''; const denyProtocol = options.denyProtocol ?? ''; return `#!/usr/bin/env node 'use strict'; // Elydora agent guard for ${agentName}. Generated by @elydora/sdk. const crypto = require('node:crypto'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const AGENT_NAME = ${JSON.stringify(agentName)}; const AGENT_ID = ${JSON.stringify(agentId)}; const SUCCESS_OUTPUT = ${JSON.stringify(successOutput)}; const FAIL_CLOSED = ${JSON.stringify(failClosed)}; const DENY_PROTOCOL = ${JSON.stringify(denyProtocol)}; 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; ${PROTECTED_RUNTIME_READER} function validateStatus(value, source) { if (!['active', 'frozen', 'revoked'].includes(value)) { throw new Error(source + ' contains an invalid agent status'); } return value; } function blockAgent(status) { const state = status === 'frozen' ? 'frozen' : 'revoked'; const message = 'Agent "' + AGENT_NAME + '" is ' + state + ' in Elydora.'; if (DENY_PROTOCOL === 'cursor') { process.stdout.write(JSON.stringify({ permission: 'deny', userMessage: message, agentMessage: message + ' Tool execution is blocked.', }) + '\\n'); } else if (DENY_PROTOCOL === 'grok') { process.stdout.write(JSON.stringify({ decision: 'deny', reason: message }) + '\\n'); } process.stderr.write('[Elydora guard] ' + message + ' Tool execution blocked.\\n'); process.exitCode = 2; } function readStatusCache() { let raw; try { raw = readProtectedFile( STATUS_CACHE_PATH, 'Status cache', MAX_PROTECTED_CONFIG_BYTES, ).toString('utf-8'); } catch (error) { if (hasRuntimeErrorCode(error, 'ENOENT')) return undefined; throw new Error('Failed to read status cache: ' + error.message); } let cache; try { cache = JSON.parse(raw); } catch (error) { throw new Error('Status cache is invalid JSON: ' + error.message); } if (!cache || typeof cache !== 'object' || !Number.isFinite(cache.cached_at)) { throw new Error('Status cache has an invalid shape'); } const age = Date.now() - cache.cached_at; if (age < 0) throw new Error('Status cache timestamp is in the future'); return { status: validateStatus(cache.status, 'Status cache'), age }; } function writeStatusCache(status) { writeProtectedJson(STATUS_CACHE_PATH, 'Status cache', { status, cached_at: Date.now(), }); } async function readInput() { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const raw = Buffer.concat(chunks).toString('utf-8').trim(); if (!raw) { if (FAIL_CLOSED) throw new Error('Hook input is empty'); process.stderr.write('[Elydora guard] Hook input is empty; enforcement continued in fail-open mode.\\n'); return; } try { JSON.parse(raw); } catch (error) { if (FAIL_CLOSED) throw new Error('Hook input is invalid JSON: ' + error.message); process.stderr.write('[Elydora guard] Hook input is invalid JSON; enforcement continued in fail-open mode: ' + error.message + '\\n'); } } function readConfig() { const raw = readProtectedFile( CONFIG_PATH, 'Agent config', MAX_PROTECTED_CONFIG_BYTES, ).toString('utf-8'); let config; try { config = JSON.parse(raw); } catch (error) { throw new Error('Agent config is invalid JSON: ' + error.message); } if (!config || typeof config !== 'object') throw new Error('Agent config must be an object'); if (config.agent_id !== AGENT_ID || (config.agent_name !== undefined && config.agent_name !== AGENT_NAME)) { throw new Error('Agent config identity does not match the generated guard'); } return config; } async function main() { await readInput(); let config; try { config = readConfig(); } catch (error) { if (FAIL_CLOSED) throw new Error('Failed to read agent config: ' + error.message); process.stderr.write('[Elydora guard] Failed to read agent config: ' + error.message + '\\n'); return; } try { const cache = readStatusCache(); if (cache && cache.age < CACHE_TTL_MS) { if (cache.status !== 'active') blockAgent(cache.status); return; } } catch (error) { if (FAIL_CLOSED) throw error; process.stderr.write('[Elydora guard] ' + error.message + '\\n'); } const baseUrl = String(config.base_url || 'https://api.elydora.com').replace(/\\\/+$/, ''); const token = config.token || ''; 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(AGENT_ID), { headers, signal: controller.signal, }); if (!response.ok) throw new Error('Agent status API returned HTTP ' + response.status); const data = await response.json(); const status = validateStatus(data && data.agent && data.agent.status, 'Agent status API'); writeStatusCache(status); if (status !== 'active') blockAgent(status); } catch (error) { if (FAIL_CLOSED) throw new Error('Failed to resolve agent status: ' + error.message); process.stderr.write('[Elydora guard] Failed to resolve agent status: ' + error.message + '\\n'); } finally { clearTimeout(timeout); } } main().then(() => { if ((!process.exitCode || process.exitCode === 0) && SUCCESS_OUTPUT) { process.stdout.write(SUCCESS_OUTPUT); } }).catch((error) => { process.stderr.write('[Elydora guard] ' + (error.stack || error.message || String(error)) + '\\n'); process.exitCode = 1; }); `; }