140 lines
4.5 KiB
Go
140 lines
4.5 KiB
Go
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)
|
|
}
|