openclaw/secure/agent.ts
Claude c7306b6721
feat: add Moltbot Secure edition for Railway deployment
A lean, secure, self-hosted AI assistant designed for Railway:

- Telegram-only channel (allowlist-based access control)
- Authenticated webhook receiver for external integrations
- Docker sandbox for isolated code execution
- Cron scheduler for recurring tasks
- Env-only configuration (no config files)
- Full audit logging

Core files:
- secure/config.ts - Environment-only configuration
- secure/audit.ts - Audit logging system
- secure/agent.ts - AI agent core (Anthropic/OpenAI)
- secure/telegram.ts - Telegram bot handler
- secure/webhooks.ts - Webhook receiver
- secure/sandbox.ts - Docker sandbox execution
- secure/scheduler.ts - Cron task scheduler
- secure/index.ts - Main entry point
- secure/Dockerfile - Minimal container image
- secure/railway.json - Railway deployment config

https://claude.ai/code/session_015VqJ7gN4vaxtYfYc92UjLs
2026-01-30 06:00:16 +00:00

178 lines
4.8 KiB
TypeScript

/**
* Moltbot Secure - Agent Core
*
* Minimal AI agent that handles conversations.
* Direct API calls to Anthropic or OpenAI - no intermediaries.
*/
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
import type { SecureConfig } from "./config.js";
import type { AuditLogger } from "./audit.js";
export type Message = {
role: "user" | "assistant";
content: string;
};
export type AgentResponse = {
text: string;
usage?: {
inputTokens: number;
outputTokens: number;
};
};
export type AgentCore = {
chat: (messages: Message[], systemPrompt?: string) => Promise<AgentResponse>;
provider: "anthropic" | "openai";
};
const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
const DEFAULT_OPENAI_MODEL = "gpt-4o";
const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant running as a secure, self-hosted bot.
You are direct, concise, and helpful. You can:
- Answer questions and have conversations
- Analyze images and documents shared with you
- Help with coding and technical tasks
- Summarize content and extract information
When you receive webhook notifications, summarize them helpfully for the user.
Be security-conscious:
- Never reveal API keys, tokens, or secrets
- Don't execute commands that could harm the system
- Warn users about potentially dangerous operations`;
function createAnthropicAgent(config: SecureConfig, audit: AuditLogger): AgentCore {
const client = new Anthropic({
apiKey: config.ai.apiKey,
});
const model = config.ai.model || DEFAULT_ANTHROPIC_MODEL;
return {
provider: "anthropic",
async chat(messages: Message[], systemPrompt?: string): Promise<AgentResponse> {
try {
const response = await client.messages.create({
model,
max_tokens: 4096,
system: systemPrompt || DEFAULT_SYSTEM_PROMPT,
messages: messages.map((m) => ({
role: m.role,
content: m.content,
})),
});
const text = response.content
.filter((block): block is Anthropic.TextBlock => block.type === "text")
.map((block) => block.text)
.join("\n");
return {
text,
usage: {
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
},
};
} catch (err) {
audit.error({
error: `Anthropic API error: ${err instanceof Error ? err.message : String(err)}`,
});
throw err;
}
},
};
}
function createOpenAIAgent(config: SecureConfig, audit: AuditLogger): AgentCore {
const client = new OpenAI({
apiKey: config.ai.apiKey,
});
const model = config.ai.model || DEFAULT_OPENAI_MODEL;
return {
provider: "openai",
async chat(messages: Message[], systemPrompt?: string): Promise<AgentResponse> {
try {
const response = await client.chat.completions.create({
model,
max_tokens: 4096,
messages: [
{ role: "system", content: systemPrompt || DEFAULT_SYSTEM_PROMPT },
...messages.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
],
});
const text = response.choices[0]?.message?.content || "";
return {
text,
usage: response.usage
? {
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
}
: undefined,
};
} catch (err) {
audit.error({
error: `OpenAI API error: ${err instanceof Error ? err.message : String(err)}`,
});
throw err;
}
},
};
}
export function createAgent(config: SecureConfig, audit: AuditLogger): AgentCore {
if (config.ai.provider === "anthropic") {
return createAnthropicAgent(config, audit);
}
return createOpenAIAgent(config, audit);
}
/**
* Simple in-memory conversation store
* For Railway, consider using Redis or persistent storage
*/
export type ConversationStore = {
get: (userId: number) => Message[];
add: (userId: number, message: Message) => void;
clear: (userId: number) => void;
};
const MAX_HISTORY = 20;
export function createConversationStore(): ConversationStore {
const conversations = new Map<number, Message[]>();
return {
get(userId: number): Message[] {
return conversations.get(userId) || [];
},
add(userId: number, message: Message): void {
const history = conversations.get(userId) || [];
history.push(message);
// Keep only last N messages
if (history.length > MAX_HISTORY) {
history.splice(0, history.length - MAX_HISTORY);
}
conversations.set(userId, history);
},
clear(userId: number): void {
conversations.delete(userId);
},
};
}