diff --git a/.env.example b/.env.example index 627883a..4e9cb93 100644 --- a/.env.example +++ b/.env.example @@ -47,3 +47,13 @@ OLLAMA_BASE_URL= # Create a bot via @BotFather, get chat ID via @userinfobot TELEGRAM_BOT_TOKEN= TELEGRAM_CHAT_ID= + +# === Aegis Content-Safety Sidecar (optional) === +# URL of the Aegis sidecar HTTP service +AEGIS_SIDECAR_URL=http://localhost:8080 +# Bearer token for the sidecar API +AEGIS_BEARER_TOKEN=local-dev-key +# Set to "true" to fail closed (block) when the sidecar is unreachable +AEGIS_STRICT_MODE=false +# Timeout in milliseconds for sidecar requests +AEGIS_TIMEOUT_MS=3000 diff --git a/feat/aegis-sdk-integration b/feat/aegis-sdk-integration new file mode 100644 index 0000000..36bf221 --- /dev/null +++ b/feat/aegis-sdk-integration @@ -0,0 +1,15 @@ +# Drop files into their correct locations: +cp aegis-adapter.mjs sdk/aegis-adapter.mjs +cp CLAUDE.md CLAUDE.md +cp adapter.test.mjs tests/aegis/adapter.test.mjs +cp aegis-health.mjs scripts/aegis-health.mjs +cp crucix-ci.yml .github/workflows/crucix-ci.yml +cp package.json package.json +cp .env.example .env.example + +mkdir -p .claude/skills docs/agent-guides +cp aegis-sdk.md .claude/skills/aegis-sdk.md +cp build-test-verify.md .claude/skills/build-test-verify.md +cp git-commit.md .claude/skills/git-commit.md +cp architecture.md docs/agent-guides/architecture.md +cp aegis-sdk-guide.md docs/agent-guides/aegis-sdk-guide.md \ No newline at end of file diff --git a/sdk/aegis-adapter.mjs b/sdk/aegis-adapter.mjs new file mode 100644 index 0000000..f123987 --- /dev/null +++ b/sdk/aegis-adapter.mjs @@ -0,0 +1,132 @@ +/** + * sdk/aegis-adapter.mjs + * Ethos Aegis integration layer for Crucix. + * Uses the native Node.js AegisClient for robust subprocess/HTTP management. + */ + +import { AegisClient } from './ethos-aegis/sdk/node/src/index.js'; + +// ─── Config ─────────────────────────────────────────────────────────────────── + +const SIDECAR_URL = process.env.AEGIS_SIDECAR_URL || 'http://localhost:8080'; +const BEARER_TOKEN = process.env.AEGIS_BEARER_TOKEN || 'local-dev-key'; +const STRICT_MODE = process.env.AEGIS_STRICT_MODE === 'true'; + +// Initialize the real AegisClient +const aegis = new AegisClient({ + baseUrl: SIDECAR_URL, + apiKey: BEARER_TOKEN, + throwOnCondemned: false, // We handle the escalation manually in Crucix + timeoutMs: parseInt(process.env.AEGIS_TIMEOUT_MS || '3000', 10) +}); + +// ─── Severity → Crucix tier mapping ────────────────────────────────────────── + +/** + * Maps an Aegis depth to a Crucix alert tier. + * @param {string} depth + * @returns {'FLASH'|'PRIORITY'|'ROUTINE'|'PASS'} + */ +export function depthToTier(depth) { + switch (depth?.toUpperCase()) { + case 'CONDEMNED': return 'FLASH'; + case 'GRAVE': return 'PRIORITY'; + case 'CAUTION': return 'ROUTINE'; + case 'TRACE': return 'ROUTINE'; + case 'VOID': + default: return 'PASS'; + } +} + +// ─── Guard Functions ───────────────────────────────────────────────────────── + +export async function guardInput(rawText, context = {}) { + try { + const verdict = await aegis.adjudicate(rawText, { guardPoint: 'input', ...context }); + return { + sanctified: verdict.sanctified, + payload: verdict.sanitized || rawText, // Fallback to raw if not sanitized + verdict + }; + } catch (err) { + if (STRICT_MODE) throw new Error(`[Aegis] Strict mode active. Blocked due to sidecar failure: ${err.message}`); + console.warn(`[Aegis] Warning: Sidecar unreachable. Failing open. (${err.message})`); + return { sanctified: true, payload: rawText, verdict: _syntheticPass() }; + } +} + +export async function guardOutput(llmResponse, context = {}) { + try { + const verdict = await aegis.adjudicate(llmResponse, { guardPoint: 'output', ...context }); + return { + sanctified: verdict.sanctified, + payload: verdict.sanitized || llmResponse, + verdict + }; + } catch (err) { + if (STRICT_MODE) throw new Error(`[Aegis] Output guard failed: ${err.message}`); + return { sanctified: true, payload: llmResponse, verdict: _syntheticPass() }; + } +} + +export async function guardBotCommand(commandText, userId) { + try { + const verdict = await aegis.adjudicate(commandText, { guardPoint: 'bot_command', userId }); + return { + sanctified: verdict.sanctified, + payload: verdict.sanitized || commandText, + verdict + }; + } catch (err) { + if (STRICT_MODE) throw new Error(`[Aegis] Bot command guard failed: ${err.message}`); + return { sanctified: true, payload: commandText, verdict: _syntheticPass() }; + } +} + +export async function evaluateSignal(signal) { + if (!signal?.content) { + return { + signal, + tier: null, + blocked: false, + verdict: _syntheticPass(), + }; + } + + const { sanctified, payload, verdict } = await guardInput(signal.content, { + source: signal.source, + feedId: signal.id + }); + + const tier = depthToTier(verdict.depth); + + if (!sanctified && verdict.condemned) { + console.error(`[Aegis] CONDEMNED Signal Blocked. RequestID: ${verdict.requestId}`); + return { + signal: { ...signal, content: '[CONTENT REDACTED BY AEGIS]' }, + tier: 'FLASH', + blocked: true, + verdict, + }; + } + + return { + signal: { ...signal, content: payload ?? signal.content }, + tier: tier === 'PASS' ? null : tier, + blocked: false, + verdict, + }; +} + +function _syntheticPass() { + return { + sanctified: true, + condemned: false, + depth: 'VOID', + malignaCount: 0, + sanitized: null, + report: 'Synthetic pass (sidecar offline)', + latencyMs: 0, + requestId: `synth-${Date.now()}` + }; +}