Files
Elydora-Open-Source/sdks/go/cmd/elydora/plugins/guardtemplate.go
T

244 lines
7.4 KiB
Go

package plugins
import "fmt"
// GenerateGuardScript returns a self-contained agent status guard.
func GenerateGuardScript(agentName string, agentID string) string {
return generateGuardScript(agentName, agentID, "", false, "")
}
func generateGuardScript(
agentName, agentID, successOutput string,
failClosed bool,
denyProtocol string,
) string {
return fmt.Sprintf(`#!/usr/bin/env node
'use strict';
// Elydora agent guard for %s. 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 = %q;
const AGENT_ID = %q;
const SUCCESS_OUTPUT = %q;
const FAIL_CLOSED = %t;
const DENY_PROTOCOL = %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;
%s
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' ? 'is frozen by Elydora' : 'is revoked in Elydora';
const message = 'Agent "' + AGENT_NAME + '" ' + state + '.';
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;
}
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;
}
let value;
try {
value = 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',
);
return;
}
if (!value || typeof value !== 'object' || Array.isArray(value)) {
if (FAIL_CLOSED) throw new Error('Hook input must contain a JSON object');
process.stderr.write(
'[Elydora guard] Hook input must contain a JSON object; ' +
'enforcement continued in fail-open mode.\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' || Array.isArray(config)) {
throw new Error('Agent config must contain a JSON 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;
}
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' || Array.isArray(cache) ||
!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: status,
cached_at: Date.now(),
});
}
function resolveBaseURL(config) {
const value = config.base_url || 'https://api.elydora.com';
if (typeof value !== 'string' || /[\\\u0000-\u0020]/.test(value)) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
let parsed;
try {
parsed = new URL(value);
} catch (error) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
throw new Error(
'Agent config base_url must exclude credentials, query parameters, and fragments',
);
}
return value.replace(/\/+$/, '');
}
async function resolveRemoteStatus(config) {
const baseURL = resolveBaseURL(config);
const token = config.token || '';
if (typeof token !== 'string') throw new Error('Agent config token must be a string');
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: headers,
signal: controller.signal,
});
if (!response.ok) throw new Error('Agent status API returned HTTP ' + response.status);
const data = await response.json();
return validateStatus(data && data.agent && data.agent.status, 'Agent status API');
} finally {
clearTimeout(timeout);
}
}
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');
}
try {
const status = await resolveRemoteStatus(config);
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',
);
}
}
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;
});
`,
agentName,
agentName,
agentID,
successOutput,
failClosed,
denyProtocol,
protectedRuntimeFileReader,
)
}